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