1 /*
2  * Copyright (C) 2007 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.camera;
18 
19 import com.android.gallery.R;
20 
21 import com.android.camera.gallery.IImage;
22 import com.android.camera.gallery.IImageList;
23 
24 import android.app.Activity;
25 import android.app.Dialog;
26 import android.app.ProgressDialog;
27 import android.content.BroadcastReceiver;
28 import android.content.ContentResolver;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.content.IntentFilter;
32 import android.content.res.Resources;
33 import android.database.ContentObserver;
34 import android.graphics.Bitmap;
35 import android.graphics.Canvas;
36 import android.graphics.Matrix;
37 import android.graphics.Paint;
38 import android.graphics.PorterDuff;
39 import android.graphics.PorterDuffXfermode;
40 import android.graphics.Rect;
41 import android.graphics.drawable.Drawable;
42 import android.net.Uri;
43 import android.os.Bundle;
44 import android.os.Environment;
45 import android.os.Handler;
46 import android.os.StatFs;
47 import android.provider.MediaStore;
48 import android.provider.MediaStore.Images;
49 import android.util.Log;
50 import android.view.ContextMenu;
51 import android.view.LayoutInflater;
52 import android.view.Menu;
53 import android.view.MenuItem;
54 import android.view.View;
55 import android.view.ViewGroup;
56 import android.view.ContextMenu.ContextMenuInfo;
57 import android.view.MenuItem.OnMenuItemClickListener;
58 import android.widget.AdapterView;
59 import android.widget.BaseAdapter;
60 import android.widget.GridView;
61 import android.widget.TextView;
62 import android.widget.Toast;
63 import android.widget.AdapterView.AdapterContextMenuInfo;
64 
65 import java.util.ArrayList;
66 import java.util.HashMap;
67 import java.util.Map;
68 
69 /**
70  * The GalleryPicker activity.
71  */
72 public class GalleryPicker extends NoSearchActivity {
73     private static final String TAG = "GalleryPicker";
74 
75     Handler mHandler = new Handler();  // handler for the main thread
76     Thread mWorkerThread;
77     BroadcastReceiver mReceiver;
78     ContentObserver mDbObserver;
79     GridView mGridView;
80     GalleryPickerAdapter mAdapter;  // mAdapter is only accessed in main thread.
81     boolean mScanning;
82     boolean mUnmounted;
83 
84     @Override
onCreate(Bundle icicle)85     public void onCreate(Bundle icicle) {
86         super.onCreate(icicle);
87 
88         setContentView(R.layout.gallerypicker);
89 
90         mGridView = (GridView) findViewById(R.id.albums);
91 
92         mGridView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
93             public void onItemClick(AdapterView<?> parent, View view,
94                                     int position, long id) {
95                 launchFolderGallery(position);
96             }
97         });
98 
99         mGridView.setOnCreateContextMenuListener(
100                 new View.OnCreateContextMenuListener() {
101                     public void onCreateContextMenu(ContextMenu menu, View v,
102                         final ContextMenuInfo menuInfo) {
103                             onCreateGalleryPickerContextMenu(menu, menuInfo);
104                     }
105                 });
106 
107         mReceiver = new BroadcastReceiver() {
108             @Override
109             public void onReceive(Context context, Intent intent) {
110                 onReceiveMediaBroadcast(intent);
111             }
112         };
113 
114         mDbObserver = new ContentObserver(mHandler) {
115             @Override
116             public void onChange(boolean selfChange) {
117                 rebake(false, ImageManager.isMediaScannerScanning(
118                         getContentResolver()));
119             }
120         };
121 
122         ImageManager.ensureOSXCompatibleFolder();
123     }
124 
125     Dialog mMediaScanningDialog;
126 
127     // Display a dialog if the storage is being scanned now.
updateScanningDialog(boolean scanning)128     public void updateScanningDialog(boolean scanning) {
129         boolean prevScanning = (mMediaScanningDialog != null);
130         if (prevScanning == scanning && mAdapter.mItems.size() == 0) return;
131         // Now we are certain the state is changed.
132         if (prevScanning) {
133             mMediaScanningDialog.cancel();
134             mMediaScanningDialog = null;
135         } else if (scanning && mAdapter.mItems.size() == 0) {
136             mMediaScanningDialog = ProgressDialog.show(
137                     this,
138                     null,
139                     getResources().getString(R.string.wait),
140                     true,
141                     true);
142         }
143     }
144 
145     private View mNoImagesView;
146 
147     // Show/Hide the "no images" icon and text. Load resources on demand.
showNoImagesView()148     private void showNoImagesView() {
149         if (mNoImagesView == null) {
150             ViewGroup root  = (ViewGroup) findViewById(R.id.root);
151             getLayoutInflater().inflate(R.layout.gallerypicker_no_images, root);
152             mNoImagesView = findViewById(R.id.no_images);
153         }
154         mNoImagesView.setVisibility(View.VISIBLE);
155     }
156 
hideNoImagesView()157     private void hideNoImagesView() {
158         if (mNoImagesView != null) {
159             mNoImagesView.setVisibility(View.GONE);
160         }
161     }
162 
163     // The storage status is changed, restart the worker or show "no images".
rebake(boolean unmounted, boolean scanning)164     private void rebake(boolean unmounted, boolean scanning) {
165         if (unmounted == mUnmounted && scanning == mScanning) return;
166         abortWorker();
167         mUnmounted = unmounted;
168         mScanning = scanning;
169         updateScanningDialog(mScanning);
170         if (mUnmounted) {
171             showNoImagesView();
172         } else {
173             hideNoImagesView();
174             startWorker();
175         }
176     }
177 
178     // This is called when we receive media-related broadcast.
onReceiveMediaBroadcast(Intent intent)179     private void onReceiveMediaBroadcast(Intent intent) {
180         String action = intent.getAction();
181         if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) {
182             // SD card available
183             // TODO put up a "please wait" message
184         } else if (action.equals(Intent.ACTION_MEDIA_UNMOUNTED)) {
185             // SD card unavailable
186             rebake(true, false);
187         } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_STARTED)) {
188             rebake(false, true);
189         } else if (action.equals(
190                 Intent.ACTION_MEDIA_SCANNER_FINISHED)) {
191             rebake(false, false);
192         } else if (action.equals(Intent.ACTION_MEDIA_EJECT)) {
193             rebake(true, false);
194         }
195     }
196 
launchFolderGallery(int position)197     private void launchFolderGallery(int position) {
198         mAdapter.mItems.get(position).launch(this);
199     }
200 
onCreateGalleryPickerContextMenu(ContextMenu menu, final ContextMenuInfo menuInfo)201     private void onCreateGalleryPickerContextMenu(ContextMenu menu,
202             final ContextMenuInfo menuInfo) {
203         int position = ((AdapterContextMenuInfo) menuInfo).position;
204         menu.setHeaderTitle(mAdapter.baseTitleForPosition(position));
205         // "Slide Show"
206         if ((mAdapter.getIncludeMediaTypes(position)
207                 & ImageManager.INCLUDE_IMAGES) != 0) {
208             menu.add(R.string.slide_show)
209                     .setOnMenuItemClickListener(new OnMenuItemClickListener() {
210                         public boolean onMenuItemClick(MenuItem item) {
211                             return onSlideShowClicked(menuInfo);
212                         }
213                     });
214         }
215         // "View"
216         menu.add(R.string.view)
217                 .setOnMenuItemClickListener(new OnMenuItemClickListener() {
218                     public boolean onMenuItemClick(MenuItem item) {
219                             return onViewClicked(menuInfo);
220                     }
221                 });
222     }
223 
224     // This is called when the user clicks "Slideshow" from the context menu.
onSlideShowClicked(ContextMenuInfo menuInfo)225     private boolean onSlideShowClicked(ContextMenuInfo menuInfo) {
226         AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
227         int position = info.position;
228 
229         if (position < 0 || position >= mAdapter.mItems.size()) {
230             return true;
231         }
232         // Slide show starts from the first image on the list.
233         Item item = mAdapter.mItems.get(position);
234         Uri targetUri = item.mFirstImageUri;
235 
236         if (targetUri != null && item.mBucketId != null) {
237             targetUri = targetUri.buildUpon()
238                     .appendQueryParameter("bucketId", item.mBucketId)
239                     .build();
240         }
241         Intent intent = new Intent(Intent.ACTION_VIEW, targetUri);
242         intent.putExtra("slideshow", true);
243         startActivity(intent);
244         return true;
245     }
246 
247     // This is called when the user clicks "View" from the context menu.
onViewClicked(ContextMenuInfo menuInfo)248     private boolean onViewClicked(ContextMenuInfo menuInfo) {
249         AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
250         launchFolderGallery(info.position);
251         return true;
252     }
253 
254     @Override
onStop()255     public void onStop() {
256         super.onStop();
257 
258         abortWorker();
259 
260         unregisterReceiver(mReceiver);
261         getContentResolver().unregisterContentObserver(mDbObserver);
262 
263         // free up some ram
264         mAdapter = null;
265         mGridView.setAdapter(null);
266         unloadDrawable();
267     }
268 
269     @Override
onStart()270     public void onStart() {
271         super.onStart();
272 
273         mAdapter = new GalleryPickerAdapter(getLayoutInflater());
274         mGridView.setAdapter(mAdapter);
275 
276         // install an intent filter to receive SD card related events.
277         IntentFilter intentFilter = new IntentFilter();
278         intentFilter.addAction(Intent.ACTION_MEDIA_MOUNTED);
279         intentFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
280         intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED);
281         intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED);
282         intentFilter.addAction(Intent.ACTION_MEDIA_EJECT);
283         intentFilter.addDataScheme("file");
284 
285         registerReceiver(mReceiver, intentFilter,
286                 Context.RECEIVER_EXPORTED_UNAUDITED);
287 
288         getContentResolver().registerContentObserver(
289                 MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
290                 true, mDbObserver);
291 
292         // Assume the storage is mounted and not scanning.
293         mUnmounted = false;
294         mScanning = false;
295         startWorker();
296     }
297 
298     // This is used to stop the worker thread.
299     volatile boolean mAbort = false;
300 
301     // Create the worker thread.
startWorker()302     private void startWorker() {
303         mAbort = false;
304         mWorkerThread = new Thread("GalleryPicker Worker") {
305             @Override
306             public void run() {
307                 workerRun();
308             }
309         };
310         BitmapManager.instance().allowThreadDecoding(mWorkerThread);
311         mWorkerThread.start();
312     }
313 
abortWorker()314     private void abortWorker() {
315         if (mWorkerThread != null) {
316             BitmapManager.instance().cancelThreadDecoding(mWorkerThread, getContentResolver());
317             mAbort = true;
318             try {
319                 mWorkerThread.join();
320             } catch (InterruptedException ex) {
321                 Log.e(TAG, "join interrupted");
322             }
323             mWorkerThread = null;
324             // Remove all runnables in mHandler.
325             // (We assume that the "what" field in the messages are 0
326             // for runnables).
327             mHandler.removeMessages(0);
328             mAdapter.clear();
329             mAdapter.updateDisplay();
330             clearImageLists();
331         }
332     }
333 
334     // This is run in the worker thread.
workerRun()335     private void workerRun() {
336         // We collect items from checkImageList() and checkBucketIds() and
337         // put them in allItems. Later we give allItems to checkThumbBitmap()
338         // and generated thumbnail bitmaps for each item. We do this instead of
339         // generating thumbnail bitmaps in checkImageList() and checkBucketIds()
340         // because we want to show all the folders first, then update them with
341         // the thumb bitmaps. (Generating thumbnail bitmaps takes some time.)
342         ArrayList<Item> allItems = new ArrayList<Item>();
343 
344         checkScanning();
345         if (mAbort) return;
346 
347         checkImageList(allItems);
348         if (mAbort) return;
349 
350         checkBucketIds(allItems);
351         if (mAbort) return;
352 
353         checkThumbBitmap(allItems);
354         if (mAbort) return;
355 
356         checkLowStorage();
357     }
358 
359     // This is run in the worker thread.
checkScanning()360     private void checkScanning() {
361         ContentResolver cr = getContentResolver();
362         final boolean scanning =
363                 ImageManager.isMediaScannerScanning(cr);
364         mHandler.post(new Runnable() {
365                     public void run() {
366                         checkScanningFinished(scanning);
367                     }
368                 });
369     }
370 
371     // This is run in the main thread.
checkScanningFinished(boolean scanning)372     private void checkScanningFinished(boolean scanning) {
373         updateScanningDialog(scanning);
374     }
375 
376     // This is run in the worker thread.
checkImageList(ArrayList<Item> allItems)377     private void checkImageList(ArrayList<Item> allItems) {
378         int length = IMAGE_LIST_DATA.length;
379         IImageList[] lists = new IImageList[length];
380         for (int i = 0; i < length; i++) {
381             ImageListData data = IMAGE_LIST_DATA[i];
382             lists[i] = createImageList(data.mInclude, data.mBucketId,
383                     getContentResolver());
384             if (mAbort) return;
385             Item item = null;
386 
387             if (lists[i].isEmpty()) continue;
388 
389             // i >= 3 means we are looking at All Images/All Videos.
390             // lists[i-3] is the corresponding Camera Images/Camera Videos.
391             // We want to add the "All" list only if it's different from
392             // the "Camera" list.
393             if (i >= 3 && lists[i].getCount() == lists[i - 3].getCount()) {
394                 continue;
395             }
396 
397             item = new Item(data.mType,
398                             data.mBucketId,
399                             getResources().getString(data.mStringId),
400                             lists[i]);
401 
402             allItems.add(item);
403 
404             final Item finalItem = item;
405             mHandler.post(new Runnable() {
406                         public void run() {
407                             updateItem(finalItem);
408                         }
409                     });
410         }
411     }
412 
413     // This is run in the main thread.
updateItem(Item item)414     private void updateItem(Item item) {
415         // Hide NoImageView if we are going to add the first item
416         if (mAdapter.getCount() == 0) {
417             hideNoImagesView();
418         }
419         mAdapter.addItem(item);
420         mAdapter.updateDisplay();
421     }
422 
423     private static final String CAMERA_BUCKET =
424             ImageManager.CAMERA_IMAGE_BUCKET_ID;
425 
426     // This is run in the worker thread.
checkBucketIds(ArrayList<Item> allItems)427     private void checkBucketIds(ArrayList<Item> allItems) {
428         final IImageList allImages;
429         if (!mScanning && !mUnmounted) {
430             allImages = ImageManager.makeImageList(
431                     getContentResolver(),
432                     ImageManager.DataLocation.ALL,
433                     ImageManager.INCLUDE_IMAGES | ImageManager.INCLUDE_VIDEOS,
434                     ImageManager.SORT_DESCENDING,
435                     null);
436         } else {
437             allImages = ImageManager.makeEmptyImageList();
438         }
439 
440         if (mAbort) {
441             allImages.close();
442             return;
443         }
444 
445         HashMap<String, String> hashMap = allImages.getBucketIds();
446         allImages.close();
447         if (mAbort) return;
448 
449         for (Map.Entry<String, String> entry : hashMap.entrySet()) {
450             String key = entry.getKey();
451             if (key == null) {
452                 continue;
453             }
454             if (!key.equals(CAMERA_BUCKET)) {
455                 IImageList list = createImageList(
456                         ImageManager.INCLUDE_IMAGES
457                         | ImageManager.INCLUDE_VIDEOS, key,
458                         getContentResolver());
459                 if (mAbort) return;
460 
461                 Item item = new Item(Item.TYPE_NORMAL_FOLDERS, key,
462                         entry.getValue(), list);
463 
464                 allItems.add(item);
465 
466                 final Item finalItem = item;
467                 mHandler.post(new Runnable() {
468                             public void run() {
469                                 updateItem(finalItem);
470                             }
471                         });
472             }
473         }
474 
475         mHandler.post(new Runnable() {
476                     public void run() {
477                         checkBucketIdsFinished();
478                     }
479                 });
480     }
481 
482     // This is run in the main thread.
checkBucketIdsFinished()483     private void checkBucketIdsFinished() {
484 
485         // If we just have one folder, open it.
486         // If we have zero folder, show the "no images" icon.
487         if (!mScanning) {
488             int numItems = mAdapter.mItems.size();
489             if (numItems == 0) {
490                 showNoImagesView();
491             } else if (numItems == 1) {
492                 mAdapter.mItems.get(0).launch(this);
493                 finish();
494                 return;
495             }
496         }
497     }
498 
499     private static final int THUMB_SIZE = 142;
500     // This is run in the worker thread.
checkThumbBitmap(ArrayList<Item> allItems)501     private void checkThumbBitmap(ArrayList<Item> allItems) {
502         for (Item item : allItems) {
503             final Bitmap b = makeMiniThumbBitmap(THUMB_SIZE, THUMB_SIZE,
504                     item.mImageList);
505             if (mAbort) {
506                 if (b != null) b.recycle();
507                 return;
508             }
509 
510             final Item finalItem = item;
511             mHandler.post(new Runnable() {
512                         public void run() {
513                             updateThumbBitmap(finalItem, b);
514                         }
515                     });
516         }
517     }
518 
519     // This is run in the main thread.
updateThumbBitmap(Item item, Bitmap b)520     private void updateThumbBitmap(Item item, Bitmap b) {
521         item.setThumbBitmap(b);
522         mAdapter.updateDisplay();
523     }
524 
525     private static final long LOW_STORAGE_THRESHOLD = 1024 * 1024 * 2;
526 
527     // This is run in the worker thread.
checkLowStorage()528     private void checkLowStorage() {
529         // Check available space only if we are writable
530         if (ImageManager.hasStorage()) {
531             String storageDirectory = Environment
532                     .getExternalStorageDirectory().toString();
533             StatFs stat = new StatFs(storageDirectory);
534             long remaining = (long) stat.getAvailableBlocks()
535                     * (long) stat.getBlockSize();
536             if (remaining < LOW_STORAGE_THRESHOLD) {
537                 mHandler.post(new Runnable() {
538                     public void run() {
539                         checkLowStorageFinished();
540                     }
541                 });
542             }
543         }
544     }
545 
546     // This is run in the main thread.
547     // This is called only if the storage is low.
checkLowStorageFinished()548     private void checkLowStorageFinished() {
549         Toast.makeText(GalleryPicker.this, R.string.not_enough_space, 5000)
550                 .show();
551     }
552 
553     // IMAGE_LIST_DATA stores the parameters for the four image lists
554     // we are interested in. The order of the IMAGE_LIST_DATA array is
555     // significant (See the implementation of GalleryPickerAdapter.init).
556     private static final class ImageListData {
ImageListData(int type, int include, String bucketId, int stringId)557         ImageListData(int type, int include, String bucketId, int stringId) {
558             mType = type;
559             mInclude = include;
560             mBucketId = bucketId;
561             mStringId = stringId;
562         }
563         int mType;
564         int mInclude;
565         String mBucketId;
566         int mStringId;
567     }
568 
569     private static final ImageListData[] IMAGE_LIST_DATA = {
570         // Camera Images
571         new ImageListData(Item.TYPE_CAMERA_IMAGES,
572                           ImageManager.INCLUDE_IMAGES,
573                           ImageManager.CAMERA_IMAGE_BUCKET_ID,
574                           R.string.gallery_camera_bucket_name),
575         // Camera Videos
576         new ImageListData(Item.TYPE_CAMERA_VIDEOS,
577                           ImageManager.INCLUDE_VIDEOS,
578                           ImageManager.CAMERA_IMAGE_BUCKET_ID,
579                           R.string.gallery_camera_videos_bucket_name),
580 
581         // Camera Medias
582         new ImageListData(Item.TYPE_CAMERA_MEDIAS,
583                 ImageManager.INCLUDE_VIDEOS | ImageManager.INCLUDE_IMAGES,
584                 ImageManager.CAMERA_IMAGE_BUCKET_ID,
585                 R.string.gallery_camera_media_bucket_name),
586 
587         // All Images
588         new ImageListData(Item.TYPE_ALL_IMAGES,
589                           ImageManager.INCLUDE_IMAGES,
590                           null,
591                           R.string.all_images),
592 
593         // All Videos
594         new ImageListData(Item.TYPE_ALL_VIDEOS,
595                           ImageManager.INCLUDE_VIDEOS,
596                           null,
597                           R.string.all_videos),
598     };
599 
600 
601     // These drawables are loaded on-demand.
602     Drawable mFrameGalleryMask;
603     Drawable mCellOutline;
604     Drawable mVideoOverlay;
605 
loadDrawableIfNeeded()606     private void loadDrawableIfNeeded() {
607         if (mFrameGalleryMask != null) return;  // already loaded
608         Resources r = getResources();
609         mFrameGalleryMask = r.getDrawable(
610                 R.drawable.frame_gallery_preview_album_mask);
611         mCellOutline = r.getDrawable(android.R.drawable.gallery_thumb);
612         mVideoOverlay = r.getDrawable(R.drawable.ic_gallery_video_overlay);
613     }
614 
unloadDrawable()615     private void unloadDrawable() {
616         mFrameGalleryMask = null;
617         mCellOutline = null;
618         mVideoOverlay = null;
619     }
620 
placeImage(Bitmap image, Canvas c, Paint paint, int imageWidth, int widthPadding, int imageHeight, int heightPadding, int offsetX, int offsetY, int pos)621     private static void placeImage(Bitmap image, Canvas c, Paint paint,
622             int imageWidth, int widthPadding, int imageHeight,
623             int heightPadding, int offsetX, int offsetY,
624             int pos) {
625         int row = pos / 2;
626         int col = pos - (row * 2);
627 
628         int xPos = (col * (imageWidth + widthPadding)) - offsetX;
629         int yPos = (row * (imageHeight + heightPadding)) - offsetY;
630 
631         c.drawBitmap(image, xPos, yPos, paint);
632     }
633 
634     // This is run in worker thread.
makeMiniThumbBitmap(int width, int height, IImageList images)635     private Bitmap makeMiniThumbBitmap(int width, int height,
636             IImageList images) {
637         int count = images.getCount();
638         // We draw three different version of the folder image depending on the
639         // number of images in the folder.
640         //    For a single image, that image draws over the whole folder.
641         //    For two or three images, we draw the two most recent photos.
642         //    For four or more images, we draw four photos.
643         final int padding = 4;
644         int imageWidth = width;
645         int imageHeight = height;
646         int offsetWidth = 0;
647         int offsetHeight = 0;
648 
649         imageWidth = (imageWidth - padding) / 2;  // 2 here because we show two
650                                                   // images
651         imageHeight = (imageHeight - padding) / 2;  // per row and column
652 
653         final Paint p = new Paint();
654         final Bitmap b = Bitmap.createBitmap(width, height,
655                 Bitmap.Config.ARGB_8888);
656         final Canvas c = new Canvas(b);
657         final Matrix m = new Matrix();
658 
659         // draw the whole canvas as transparent
660         p.setColor(0x00000000);
661         c.drawPaint(p);
662 
663         // load the drawables
664         loadDrawableIfNeeded();
665 
666         // draw the mask normally
667         p.setColor(0xFFFFFFFF);
668         mFrameGalleryMask.setBounds(0, 0, width, height);
669         mFrameGalleryMask.draw(c);
670 
671         Paint pdpaint = new Paint();
672         pdpaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
673 
674         pdpaint.setStyle(Paint.Style.FILL);
675         c.drawRect(0, 0, width, height, pdpaint);
676 
677         for (int i = 0; i < 4; i++) {
678             if (mAbort) {
679                 return null;
680             }
681 
682             Bitmap temp = null;
683             IImage image = i < count ? images.getImageAt(i) : null;
684 
685             if (image != null) {
686                 temp = image.miniThumbBitmap();
687             }
688 
689             if (temp != null) {
690                 if (ImageManager.isVideo(image)) {
691                     Bitmap newMap = temp.copy(temp.getConfig(), true);
692                     Canvas overlayCanvas = new Canvas(newMap);
693                     int overlayWidth = mVideoOverlay.getIntrinsicWidth();
694                     int overlayHeight = mVideoOverlay.getIntrinsicHeight();
695                     int left = (newMap.getWidth() - overlayWidth) / 2;
696                     int top = (newMap.getHeight() - overlayHeight) / 2;
697                     Rect newBounds = new Rect(left, top, left + overlayWidth,
698                             top + overlayHeight);
699                     mVideoOverlay.setBounds(newBounds);
700                     mVideoOverlay.draw(overlayCanvas);
701                     temp.recycle();
702                     temp = newMap;
703                 }
704 
705                 temp = Util.transform(m, temp, imageWidth,
706                         imageHeight, true, Util.RECYCLE_INPUT);
707             }
708 
709             Bitmap thumb = Bitmap.createBitmap(imageWidth, imageHeight,
710                                                Bitmap.Config.ARGB_8888);
711             Canvas tempCanvas = new Canvas(thumb);
712             if (temp != null) {
713                 tempCanvas.drawBitmap(temp, new Matrix(), new Paint());
714             }
715             mCellOutline.setBounds(0, 0, imageWidth, imageHeight);
716             mCellOutline.draw(tempCanvas);
717 
718             placeImage(thumb, c, pdpaint, imageWidth, padding, imageHeight,
719                        padding, offsetWidth, offsetHeight, i);
720 
721             thumb.recycle();
722 
723             if (temp != null) {
724                 temp.recycle();
725             }
726         }
727 
728         return b;
729     }
730 
731     @Override
732     public boolean onCreateOptionsMenu(Menu menu) {
733         super.onCreateOptionsMenu(menu);
734 
735         MenuHelper.addCaptureMenuItems(menu, this);
736 
737         menu.add(Menu.NONE, Menu.NONE, MenuHelper.POSITION_GALLERY_SETTING,
738                 R.string.camerasettings)
739                 .setOnMenuItemClickListener(new OnMenuItemClickListener() {
740                     public boolean onMenuItemClick(MenuItem item) {
741                         Intent preferences = new Intent();
742                         preferences.setClass(GalleryPicker.this,
743                                              GallerySettings.class);
744                         startActivity(preferences);
745                         return true;
746                     }
747                 })
748                 .setAlphabeticShortcut('p')
749                 .setIcon(android.R.drawable.ic_menu_preferences);
750 
751         return true;
752     }
753 
754     // image lists created by createImageList() are collected in mAllLists.
755     // They will be closed in clearImageList, so they don't hold open files
756     // on SD card. We will be killed if we don't close files when the SD card
757     // is unmounted.
758     ArrayList<IImageList> mAllLists = new ArrayList<IImageList>();
759 
760     private IImageList createImageList(int mediaTypes, String bucketId,
761             ContentResolver cr) {
762         IImageList list = ImageManager.makeImageList(
763                 cr,
764                 ImageManager.DataLocation.ALL,
765                 mediaTypes,
766                 ImageManager.SORT_DESCENDING,
767                 bucketId);
768         mAllLists.add(list);
769         return list;
770     }
771 
772     private void clearImageLists() {
773         for (IImageList list : mAllLists) {
774             list.close();
775         }
776         mAllLists.clear();
777     }
778 }
779 
780 // Item is the underlying data for GalleryPickerAdapter.
781 // It is passed from the activity to the adapter.
782 class Item {
783     public static final int TYPE_NONE = -1;
784     public static final int TYPE_ALL_IMAGES = 0;
785     public static final int TYPE_ALL_VIDEOS = 1;
786     public static final int TYPE_CAMERA_IMAGES = 2;
787     public static final int TYPE_CAMERA_VIDEOS = 3;
788     public static final int TYPE_CAMERA_MEDIAS = 4;
789     public static final int TYPE_NORMAL_FOLDERS = 5;
790 
791     public final int mType;
792     public final String mBucketId;
793     public final String mName;
794     public final IImageList mImageList;
795     public final int mCount;
796     public final Uri mFirstImageUri;  // could be null if the list is empty
797 
798     // The thumbnail bitmap is set by setThumbBitmap() later because we want
799     // to let the user sees the folder icon as soon as possible (and possibly
800     // select them), then present more detailed information when we have it.
801     public Bitmap mThumbBitmap;  // the thumbnail bitmap for the image list
802 
803     public Item(int type, String bucketId, String name, IImageList list) {
804         mType = type;
805         mBucketId = bucketId;
806         mName = name;
807         mImageList = list;
808         mCount = list.getCount();
809         if (mCount > 0) {
810             mFirstImageUri = list.getImageAt(0).fullSizeImageUri();
811         } else {
812             mFirstImageUri = null;
813         }
814     }
815 
816     public void setThumbBitmap(Bitmap thumbBitmap) {
817         mThumbBitmap = thumbBitmap;
818     }
819 
820     public boolean needsBucketId() {
821         return mType >= TYPE_CAMERA_IMAGES;
822     }
823 
824     public void launch(Activity activity) {
825         Uri uri = Images.Media.INTERNAL_CONTENT_URI;
826         if (needsBucketId()) {
827             uri = uri.buildUpon()
828                     .appendQueryParameter("bucketId", mBucketId).build();
829         }
830         Intent intent = new Intent(Intent.ACTION_VIEW, uri);
831         intent.putExtra("windowTitle", mName);
832         intent.putExtra("mediaTypes", getIncludeMediaTypes());
833         activity.startActivity(intent);
834     }
835 
836     public int getIncludeMediaTypes() {
837         return convertItemTypeToIncludedMediaType(mType);
838     }
839 
840     public static int convertItemTypeToIncludedMediaType(int itemType) {
841         switch (itemType) {
842         case TYPE_ALL_IMAGES:
843         case TYPE_CAMERA_IMAGES:
844             return ImageManager.INCLUDE_IMAGES;
845         case TYPE_ALL_VIDEOS:
846         case TYPE_CAMERA_VIDEOS:
847             return ImageManager.INCLUDE_VIDEOS;
848         case TYPE_NORMAL_FOLDERS:
849         case TYPE_CAMERA_MEDIAS:
850         default:
851             return ImageManager.INCLUDE_IMAGES
852                     | ImageManager.INCLUDE_VIDEOS;
853         }
854     }
855 
856     public int getOverlay() {
857         switch (mType) {
858             case TYPE_ALL_IMAGES:
859             case TYPE_CAMERA_IMAGES:
860                 return R.drawable.frame_overlay_gallery_camera;
861             case TYPE_ALL_VIDEOS:
862             case TYPE_CAMERA_VIDEOS:
863             case TYPE_CAMERA_MEDIAS:
864                 return R.drawable.frame_overlay_gallery_video;
865             case TYPE_NORMAL_FOLDERS:
866             default:
867                 return R.drawable.frame_overlay_gallery_folder;
868         }
869     }
870 }
871 
872 class GalleryPickerAdapter extends BaseAdapter {
873     ArrayList<Item> mItems = new ArrayList<Item>();
874     LayoutInflater mInflater;
875 
876     GalleryPickerAdapter(LayoutInflater inflater) {
877         mInflater = inflater;
878     }
879 
880     public void addItem(Item item) {
881         mItems.add(item);
882     }
883 
884     public void updateDisplay() {
885         notifyDataSetChanged();
886     }
887 
888     public void clear() {
889         mItems.clear();
890     }
891 
892     public int getCount() {
893         return mItems.size();
894     }
895 
896     public Object getItem(int position) {
897         return null;
898     }
899 
900     public long getItemId(int position) {
901         return position;
902     }
903 
904     public String baseTitleForPosition(int position) {
905         return mItems.get(position).mName;
906     }
907 
908     public int getIncludeMediaTypes(int position) {
909         return mItems.get(position).getIncludeMediaTypes();
910     }
911 
912     public View getView(final int position, View convertView,
913                         ViewGroup parent) {
914         View v;
915 
916         if (convertView == null) {
917             v = mInflater.inflate(R.layout.gallery_picker_item, null);
918         } else {
919             v = convertView;
920         }
921 
922         TextView titleView = (TextView) v.findViewById(R.id.title);
923 
924         GalleryPickerItem iv =
925                 (GalleryPickerItem) v.findViewById(R.id.thumbnail);
926         Item item = mItems.get(position);
927         iv.setOverlay(item.getOverlay());
928         if (item.mThumbBitmap != null) {
929             iv.setImageBitmap(item.mThumbBitmap);
930             String title = item.mName + " (" + item.mCount + ")";
931             titleView.setText(title);
932         } else {
933             iv.setImageResource(android.R.color.transparent);
934             titleView.setText(item.mName);
935         }
936 
937         // An workaround due to a bug in TextView. If the length of text is
938         // different from the previous in convertView, the layout would be
939         // wrong.
940         titleView.requestLayout();
941 
942         return v;
943     }
944 }
945