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 
287         getContentResolver().registerContentObserver(
288                 MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
289                 true, mDbObserver);
290 
291         // Assume the storage is mounted and not scanning.
292         mUnmounted = false;
293         mScanning = false;
294         startWorker();
295     }
296 
297     // This is used to stop the worker thread.
298     volatile boolean mAbort = false;
299 
300     // Create the worker thread.
startWorker()301     private void startWorker() {
302         mAbort = false;
303         mWorkerThread = new Thread("GalleryPicker Worker") {
304             @Override
305             public void run() {
306                 workerRun();
307             }
308         };
309         BitmapManager.instance().allowThreadDecoding(mWorkerThread);
310         mWorkerThread.start();
311     }
312 
abortWorker()313     private void abortWorker() {
314         if (mWorkerThread != null) {
315             BitmapManager.instance().cancelThreadDecoding(mWorkerThread, getContentResolver());
316             mAbort = true;
317             try {
318                 mWorkerThread.join();
319             } catch (InterruptedException ex) {
320                 Log.e(TAG, "join interrupted");
321             }
322             mWorkerThread = null;
323             // Remove all runnables in mHandler.
324             // (We assume that the "what" field in the messages are 0
325             // for runnables).
326             mHandler.removeMessages(0);
327             mAdapter.clear();
328             mAdapter.updateDisplay();
329             clearImageLists();
330         }
331     }
332 
333     // This is run in the worker thread.
workerRun()334     private void workerRun() {
335         // We collect items from checkImageList() and checkBucketIds() and
336         // put them in allItems. Later we give allItems to checkThumbBitmap()
337         // and generated thumbnail bitmaps for each item. We do this instead of
338         // generating thumbnail bitmaps in checkImageList() and checkBucketIds()
339         // because we want to show all the folders first, then update them with
340         // the thumb bitmaps. (Generating thumbnail bitmaps takes some time.)
341         ArrayList<Item> allItems = new ArrayList<Item>();
342 
343         checkScanning();
344         if (mAbort) return;
345 
346         checkImageList(allItems);
347         if (mAbort) return;
348 
349         checkBucketIds(allItems);
350         if (mAbort) return;
351 
352         checkThumbBitmap(allItems);
353         if (mAbort) return;
354 
355         checkLowStorage();
356     }
357 
358     // This is run in the worker thread.
checkScanning()359     private void checkScanning() {
360         ContentResolver cr = getContentResolver();
361         final boolean scanning =
362                 ImageManager.isMediaScannerScanning(cr);
363         mHandler.post(new Runnable() {
364                     public void run() {
365                         checkScanningFinished(scanning);
366                     }
367                 });
368     }
369 
370     // This is run in the main thread.
checkScanningFinished(boolean scanning)371     private void checkScanningFinished(boolean scanning) {
372         updateScanningDialog(scanning);
373     }
374 
375     // This is run in the worker thread.
checkImageList(ArrayList<Item> allItems)376     private void checkImageList(ArrayList<Item> allItems) {
377         int length = IMAGE_LIST_DATA.length;
378         IImageList[] lists = new IImageList[length];
379         for (int i = 0; i < length; i++) {
380             ImageListData data = IMAGE_LIST_DATA[i];
381             lists[i] = createImageList(data.mInclude, data.mBucketId,
382                     getContentResolver());
383             if (mAbort) return;
384             Item item = null;
385 
386             if (lists[i].isEmpty()) continue;
387 
388             // i >= 3 means we are looking at All Images/All Videos.
389             // lists[i-3] is the corresponding Camera Images/Camera Videos.
390             // We want to add the "All" list only if it's different from
391             // the "Camera" list.
392             if (i >= 3 && lists[i].getCount() == lists[i - 3].getCount()) {
393                 continue;
394             }
395 
396             item = new Item(data.mType,
397                             data.mBucketId,
398                             getResources().getString(data.mStringId),
399                             lists[i]);
400 
401             allItems.add(item);
402 
403             final Item finalItem = item;
404             mHandler.post(new Runnable() {
405                         public void run() {
406                             updateItem(finalItem);
407                         }
408                     });
409         }
410     }
411 
412     // This is run in the main thread.
updateItem(Item item)413     private void updateItem(Item item) {
414         // Hide NoImageView if we are going to add the first item
415         if (mAdapter.getCount() == 0) {
416             hideNoImagesView();
417         }
418         mAdapter.addItem(item);
419         mAdapter.updateDisplay();
420     }
421 
422     private static final String CAMERA_BUCKET =
423             ImageManager.CAMERA_IMAGE_BUCKET_ID;
424 
425     // This is run in the worker thread.
checkBucketIds(ArrayList<Item> allItems)426     private void checkBucketIds(ArrayList<Item> allItems) {
427         final IImageList allImages;
428         if (!mScanning && !mUnmounted) {
429             allImages = ImageManager.makeImageList(
430                     getContentResolver(),
431                     ImageManager.DataLocation.ALL,
432                     ImageManager.INCLUDE_IMAGES | ImageManager.INCLUDE_VIDEOS,
433                     ImageManager.SORT_DESCENDING,
434                     null);
435         } else {
436             allImages = ImageManager.makeEmptyImageList();
437         }
438 
439         if (mAbort) {
440             allImages.close();
441             return;
442         }
443 
444         HashMap<String, String> hashMap = allImages.getBucketIds();
445         allImages.close();
446         if (mAbort) return;
447 
448         for (Map.Entry<String, String> entry : hashMap.entrySet()) {
449             String key = entry.getKey();
450             if (key == null) {
451                 continue;
452             }
453             if (!key.equals(CAMERA_BUCKET)) {
454                 IImageList list = createImageList(
455                         ImageManager.INCLUDE_IMAGES
456                         | ImageManager.INCLUDE_VIDEOS, key,
457                         getContentResolver());
458                 if (mAbort) return;
459 
460                 Item item = new Item(Item.TYPE_NORMAL_FOLDERS, key,
461                         entry.getValue(), list);
462 
463                 allItems.add(item);
464 
465                 final Item finalItem = item;
466                 mHandler.post(new Runnable() {
467                             public void run() {
468                                 updateItem(finalItem);
469                             }
470                         });
471             }
472         }
473 
474         mHandler.post(new Runnable() {
475                     public void run() {
476                         checkBucketIdsFinished();
477                     }
478                 });
479     }
480 
481     // This is run in the main thread.
checkBucketIdsFinished()482     private void checkBucketIdsFinished() {
483 
484         // If we just have one folder, open it.
485         // If we have zero folder, show the "no images" icon.
486         if (!mScanning) {
487             int numItems = mAdapter.mItems.size();
488             if (numItems == 0) {
489                 showNoImagesView();
490             } else if (numItems == 1) {
491                 mAdapter.mItems.get(0).launch(this);
492                 finish();
493                 return;
494             }
495         }
496     }
497 
498     private static final int THUMB_SIZE = 142;
499     // This is run in the worker thread.
checkThumbBitmap(ArrayList<Item> allItems)500     private void checkThumbBitmap(ArrayList<Item> allItems) {
501         for (Item item : allItems) {
502             final Bitmap b = makeMiniThumbBitmap(THUMB_SIZE, THUMB_SIZE,
503                     item.mImageList);
504             if (mAbort) {
505                 if (b != null) b.recycle();
506                 return;
507             }
508 
509             final Item finalItem = item;
510             mHandler.post(new Runnable() {
511                         public void run() {
512                             updateThumbBitmap(finalItem, b);
513                         }
514                     });
515         }
516     }
517 
518     // This is run in the main thread.
updateThumbBitmap(Item item, Bitmap b)519     private void updateThumbBitmap(Item item, Bitmap b) {
520         item.setThumbBitmap(b);
521         mAdapter.updateDisplay();
522     }
523 
524     private static final long LOW_STORAGE_THRESHOLD = 1024 * 1024 * 2;
525 
526     // This is run in the worker thread.
checkLowStorage()527     private void checkLowStorage() {
528         // Check available space only if we are writable
529         if (ImageManager.hasStorage()) {
530             String storageDirectory = Environment
531                     .getExternalStorageDirectory().toString();
532             StatFs stat = new StatFs(storageDirectory);
533             long remaining = (long) stat.getAvailableBlocks()
534                     * (long) stat.getBlockSize();
535             if (remaining < LOW_STORAGE_THRESHOLD) {
536                 mHandler.post(new Runnable() {
537                     public void run() {
538                         checkLowStorageFinished();
539                     }
540                 });
541             }
542         }
543     }
544 
545     // This is run in the main thread.
546     // This is called only if the storage is low.
checkLowStorageFinished()547     private void checkLowStorageFinished() {
548         Toast.makeText(GalleryPicker.this, R.string.not_enough_space, 5000)
549                 .show();
550     }
551 
552     // IMAGE_LIST_DATA stores the parameters for the four image lists
553     // we are interested in. The order of the IMAGE_LIST_DATA array is
554     // significant (See the implementation of GalleryPickerAdapter.init).
555     private static final class ImageListData {
ImageListData(int type, int include, String bucketId, int stringId)556         ImageListData(int type, int include, String bucketId, int stringId) {
557             mType = type;
558             mInclude = include;
559             mBucketId = bucketId;
560             mStringId = stringId;
561         }
562         int mType;
563         int mInclude;
564         String mBucketId;
565         int mStringId;
566     }
567 
568     private static final ImageListData[] IMAGE_LIST_DATA = {
569         // Camera Images
570         new ImageListData(Item.TYPE_CAMERA_IMAGES,
571                           ImageManager.INCLUDE_IMAGES,
572                           ImageManager.CAMERA_IMAGE_BUCKET_ID,
573                           R.string.gallery_camera_bucket_name),
574         // Camera Videos
575         new ImageListData(Item.TYPE_CAMERA_VIDEOS,
576                           ImageManager.INCLUDE_VIDEOS,
577                           ImageManager.CAMERA_IMAGE_BUCKET_ID,
578                           R.string.gallery_camera_videos_bucket_name),
579 
580         // Camera Medias
581         new ImageListData(Item.TYPE_CAMERA_MEDIAS,
582                 ImageManager.INCLUDE_VIDEOS | ImageManager.INCLUDE_IMAGES,
583                 ImageManager.CAMERA_IMAGE_BUCKET_ID,
584                 R.string.gallery_camera_media_bucket_name),
585 
586         // All Images
587         new ImageListData(Item.TYPE_ALL_IMAGES,
588                           ImageManager.INCLUDE_IMAGES,
589                           null,
590                           R.string.all_images),
591 
592         // All Videos
593         new ImageListData(Item.TYPE_ALL_VIDEOS,
594                           ImageManager.INCLUDE_VIDEOS,
595                           null,
596                           R.string.all_videos),
597     };
598 
599 
600     // These drawables are loaded on-demand.
601     Drawable mFrameGalleryMask;
602     Drawable mCellOutline;
603     Drawable mVideoOverlay;
604 
loadDrawableIfNeeded()605     private void loadDrawableIfNeeded() {
606         if (mFrameGalleryMask != null) return;  // already loaded
607         Resources r = getResources();
608         mFrameGalleryMask = r.getDrawable(
609                 R.drawable.frame_gallery_preview_album_mask);
610         mCellOutline = r.getDrawable(android.R.drawable.gallery_thumb);
611         mVideoOverlay = r.getDrawable(R.drawable.ic_gallery_video_overlay);
612     }
613 
unloadDrawable()614     private void unloadDrawable() {
615         mFrameGalleryMask = null;
616         mCellOutline = null;
617         mVideoOverlay = null;
618     }
619 
placeImage(Bitmap image, Canvas c, Paint paint, int imageWidth, int widthPadding, int imageHeight, int heightPadding, int offsetX, int offsetY, int pos)620     private static void placeImage(Bitmap image, Canvas c, Paint paint,
621             int imageWidth, int widthPadding, int imageHeight,
622             int heightPadding, int offsetX, int offsetY,
623             int pos) {
624         int row = pos / 2;
625         int col = pos - (row * 2);
626 
627         int xPos = (col * (imageWidth + widthPadding)) - offsetX;
628         int yPos = (row * (imageHeight + heightPadding)) - offsetY;
629 
630         c.drawBitmap(image, xPos, yPos, paint);
631     }
632 
633     // This is run in worker thread.
makeMiniThumbBitmap(int width, int height, IImageList images)634     private Bitmap makeMiniThumbBitmap(int width, int height,
635             IImageList images) {
636         int count = images.getCount();
637         // We draw three different version of the folder image depending on the
638         // number of images in the folder.
639         //    For a single image, that image draws over the whole folder.
640         //    For two or three images, we draw the two most recent photos.
641         //    For four or more images, we draw four photos.
642         final int padding = 4;
643         int imageWidth = width;
644         int imageHeight = height;
645         int offsetWidth = 0;
646         int offsetHeight = 0;
647 
648         imageWidth = (imageWidth - padding) / 2;  // 2 here because we show two
649                                                   // images
650         imageHeight = (imageHeight - padding) / 2;  // per row and column
651 
652         final Paint p = new Paint();
653         final Bitmap b = Bitmap.createBitmap(width, height,
654                 Bitmap.Config.ARGB_8888);
655         final Canvas c = new Canvas(b);
656         final Matrix m = new Matrix();
657 
658         // draw the whole canvas as transparent
659         p.setColor(0x00000000);
660         c.drawPaint(p);
661 
662         // load the drawables
663         loadDrawableIfNeeded();
664 
665         // draw the mask normally
666         p.setColor(0xFFFFFFFF);
667         mFrameGalleryMask.setBounds(0, 0, width, height);
668         mFrameGalleryMask.draw(c);
669 
670         Paint pdpaint = new Paint();
671         pdpaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
672 
673         pdpaint.setStyle(Paint.Style.FILL);
674         c.drawRect(0, 0, width, height, pdpaint);
675 
676         for (int i = 0; i < 4; i++) {
677             if (mAbort) {
678                 return null;
679             }
680 
681             Bitmap temp = null;
682             IImage image = i < count ? images.getImageAt(i) : null;
683 
684             if (image != null) {
685                 temp = image.miniThumbBitmap();
686             }
687 
688             if (temp != null) {
689                 if (ImageManager.isVideo(image)) {
690                     Bitmap newMap = temp.copy(temp.getConfig(), true);
691                     Canvas overlayCanvas = new Canvas(newMap);
692                     int overlayWidth = mVideoOverlay.getIntrinsicWidth();
693                     int overlayHeight = mVideoOverlay.getIntrinsicHeight();
694                     int left = (newMap.getWidth() - overlayWidth) / 2;
695                     int top = (newMap.getHeight() - overlayHeight) / 2;
696                     Rect newBounds = new Rect(left, top, left + overlayWidth,
697                             top + overlayHeight);
698                     mVideoOverlay.setBounds(newBounds);
699                     mVideoOverlay.draw(overlayCanvas);
700                     temp.recycle();
701                     temp = newMap;
702                 }
703 
704                 temp = Util.transform(m, temp, imageWidth,
705                         imageHeight, true, Util.RECYCLE_INPUT);
706             }
707 
708             Bitmap thumb = Bitmap.createBitmap(imageWidth, imageHeight,
709                                                Bitmap.Config.ARGB_8888);
710             Canvas tempCanvas = new Canvas(thumb);
711             if (temp != null) {
712                 tempCanvas.drawBitmap(temp, new Matrix(), new Paint());
713             }
714             mCellOutline.setBounds(0, 0, imageWidth, imageHeight);
715             mCellOutline.draw(tempCanvas);
716 
717             placeImage(thumb, c, pdpaint, imageWidth, padding, imageHeight,
718                        padding, offsetWidth, offsetHeight, i);
719 
720             thumb.recycle();
721 
722             if (temp != null) {
723                 temp.recycle();
724             }
725         }
726 
727         return b;
728     }
729 
730     @Override
731     public boolean onCreateOptionsMenu(Menu menu) {
732         super.onCreateOptionsMenu(menu);
733 
734         MenuHelper.addCaptureMenuItems(menu, this);
735 
736         menu.add(Menu.NONE, Menu.NONE, MenuHelper.POSITION_GALLERY_SETTING,
737                 R.string.camerasettings)
738                 .setOnMenuItemClickListener(new OnMenuItemClickListener() {
739                     public boolean onMenuItemClick(MenuItem item) {
740                         Intent preferences = new Intent();
741                         preferences.setClass(GalleryPicker.this,
742                                              GallerySettings.class);
743                         startActivity(preferences);
744                         return true;
745                     }
746                 })
747                 .setAlphabeticShortcut('p')
748                 .setIcon(android.R.drawable.ic_menu_preferences);
749 
750         return true;
751     }
752 
753     // image lists created by createImageList() are collected in mAllLists.
754     // They will be closed in clearImageList, so they don't hold open files
755     // on SD card. We will be killed if we don't close files when the SD card
756     // is unmounted.
757     ArrayList<IImageList> mAllLists = new ArrayList<IImageList>();
758 
759     private IImageList createImageList(int mediaTypes, String bucketId,
760             ContentResolver cr) {
761         IImageList list = ImageManager.makeImageList(
762                 cr,
763                 ImageManager.DataLocation.ALL,
764                 mediaTypes,
765                 ImageManager.SORT_DESCENDING,
766                 bucketId);
767         mAllLists.add(list);
768         return list;
769     }
770 
771     private void clearImageLists() {
772         for (IImageList list : mAllLists) {
773             list.close();
774         }
775         mAllLists.clear();
776     }
777 }
778 
779 // Item is the underlying data for GalleryPickerAdapter.
780 // It is passed from the activity to the adapter.
781 class Item {
782     public static final int TYPE_NONE = -1;
783     public static final int TYPE_ALL_IMAGES = 0;
784     public static final int TYPE_ALL_VIDEOS = 1;
785     public static final int TYPE_CAMERA_IMAGES = 2;
786     public static final int TYPE_CAMERA_VIDEOS = 3;
787     public static final int TYPE_CAMERA_MEDIAS = 4;
788     public static final int TYPE_NORMAL_FOLDERS = 5;
789 
790     public final int mType;
791     public final String mBucketId;
792     public final String mName;
793     public final IImageList mImageList;
794     public final int mCount;
795     public final Uri mFirstImageUri;  // could be null if the list is empty
796 
797     // The thumbnail bitmap is set by setThumbBitmap() later because we want
798     // to let the user sees the folder icon as soon as possible (and possibly
799     // select them), then present more detailed information when we have it.
800     public Bitmap mThumbBitmap;  // the thumbnail bitmap for the image list
801 
802     public Item(int type, String bucketId, String name, IImageList list) {
803         mType = type;
804         mBucketId = bucketId;
805         mName = name;
806         mImageList = list;
807         mCount = list.getCount();
808         if (mCount > 0) {
809             mFirstImageUri = list.getImageAt(0).fullSizeImageUri();
810         } else {
811             mFirstImageUri = null;
812         }
813     }
814 
815     public void setThumbBitmap(Bitmap thumbBitmap) {
816         mThumbBitmap = thumbBitmap;
817     }
818 
819     public boolean needsBucketId() {
820         return mType >= TYPE_CAMERA_IMAGES;
821     }
822 
823     public void launch(Activity activity) {
824         Uri uri = Images.Media.INTERNAL_CONTENT_URI;
825         if (needsBucketId()) {
826             uri = uri.buildUpon()
827                     .appendQueryParameter("bucketId", mBucketId).build();
828         }
829         Intent intent = new Intent(Intent.ACTION_VIEW, uri);
830         intent.putExtra("windowTitle", mName);
831         intent.putExtra("mediaTypes", getIncludeMediaTypes());
832         activity.startActivity(intent);
833     }
834 
835     public int getIncludeMediaTypes() {
836         return convertItemTypeToIncludedMediaType(mType);
837     }
838 
839     public static int convertItemTypeToIncludedMediaType(int itemType) {
840         switch (itemType) {
841         case TYPE_ALL_IMAGES:
842         case TYPE_CAMERA_IMAGES:
843             return ImageManager.INCLUDE_IMAGES;
844         case TYPE_ALL_VIDEOS:
845         case TYPE_CAMERA_VIDEOS:
846             return ImageManager.INCLUDE_VIDEOS;
847         case TYPE_NORMAL_FOLDERS:
848         case TYPE_CAMERA_MEDIAS:
849         default:
850             return ImageManager.INCLUDE_IMAGES
851                     | ImageManager.INCLUDE_VIDEOS;
852         }
853     }
854 
855     public int getOverlay() {
856         switch (mType) {
857             case TYPE_ALL_IMAGES:
858             case TYPE_CAMERA_IMAGES:
859                 return R.drawable.frame_overlay_gallery_camera;
860             case TYPE_ALL_VIDEOS:
861             case TYPE_CAMERA_VIDEOS:
862             case TYPE_CAMERA_MEDIAS:
863                 return R.drawable.frame_overlay_gallery_video;
864             case TYPE_NORMAL_FOLDERS:
865             default:
866                 return R.drawable.frame_overlay_gallery_folder;
867         }
868     }
869 }
870 
871 class GalleryPickerAdapter extends BaseAdapter {
872     ArrayList<Item> mItems = new ArrayList<Item>();
873     LayoutInflater mInflater;
874 
875     GalleryPickerAdapter(LayoutInflater inflater) {
876         mInflater = inflater;
877     }
878 
879     public void addItem(Item item) {
880         mItems.add(item);
881     }
882 
883     public void updateDisplay() {
884         notifyDataSetChanged();
885     }
886 
887     public void clear() {
888         mItems.clear();
889     }
890 
891     public int getCount() {
892         return mItems.size();
893     }
894 
895     public Object getItem(int position) {
896         return null;
897     }
898 
899     public long getItemId(int position) {
900         return position;
901     }
902 
903     public String baseTitleForPosition(int position) {
904         return mItems.get(position).mName;
905     }
906 
907     public int getIncludeMediaTypes(int position) {
908         return mItems.get(position).getIncludeMediaTypes();
909     }
910 
911     public View getView(final int position, View convertView,
912                         ViewGroup parent) {
913         View v;
914 
915         if (convertView == null) {
916             v = mInflater.inflate(R.layout.gallery_picker_item, null);
917         } else {
918             v = convertView;
919         }
920 
921         TextView titleView = (TextView) v.findViewById(R.id.title);
922 
923         GalleryPickerItem iv =
924                 (GalleryPickerItem) v.findViewById(R.id.thumbnail);
925         Item item = mItems.get(position);
926         iv.setOverlay(item.getOverlay());
927         if (item.mThumbBitmap != null) {
928             iv.setImageBitmap(item.mThumbBitmap);
929             String title = item.mName + " (" + item.mCount + ")";
930             titleView.setText(title);
931         } else {
932             iv.setImageResource(android.R.color.transparent);
933             titleView.setText(item.mName);
934         }
935 
936         // An workaround due to a bug in TextView. If the length of text is
937         // different from the previous in convertView, the layout would be
938         // wrong.
939         titleView.requestLayout();
940 
941         return v;
942     }
943 }
944