ImageGallery.java revision 666ea1b28a76aeba74744148b15099254d918671
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 android.app.Activity;
20import android.app.AlertDialog;
21import android.app.Dialog;
22import android.app.ProgressDialog;
23import android.content.ActivityNotFoundException;
24import android.content.BroadcastReceiver;
25import android.content.Context;
26import android.content.DialogInterface;
27import android.content.Intent;
28import android.content.IntentFilter;
29import android.content.SharedPreferences;
30import android.content.pm.ActivityInfo;
31import android.content.res.Configuration;
32import android.graphics.Bitmap;
33import android.graphics.BitmapFactory;
34import android.graphics.Canvas;
35import android.graphics.Paint;
36import android.graphics.Rect;
37import android.graphics.drawable.Drawable;
38import android.net.Uri;
39import android.os.Bundle;
40import android.os.Handler;
41import android.os.Parcelable;
42import android.preference.PreferenceManager;
43import android.provider.MediaStore;
44import android.util.Log;
45import android.view.ContextMenu;
46import android.view.KeyEvent;
47import android.view.Menu;
48import android.view.MenuItem;
49import android.view.View;
50import android.view.Window;
51import android.view.View.OnClickListener;
52import android.view.animation.Animation;
53import android.view.animation.AnimationUtils;
54import android.widget.Button;
55import android.widget.TextView;
56import android.widget.Toast;
57
58import com.android.camera.gallery.IImage;
59import com.android.camera.gallery.IImageList;
60import com.android.camera.gallery.VideoObject;
61
62import java.util.ArrayList;
63import java.util.HashSet;
64
65public class ImageGallery extends Activity implements
66        GridViewSpecial.Listener, GridViewSpecial.DrawAdapter {
67    private static final String STATE_SCROLL_POSITION = "scroll_position";
68    private static final String STATE_SELECTED_INDEX = "first_index";
69
70    private static final String TAG = "ImageGallery";
71    private static final float INVALID_POSITION = -1f;
72    private ImageManager.ImageListParam mParam;
73    private IImageList mAllImages;
74    private int mInclusion;
75    boolean mSortAscending = false;
76    private View mNoImagesView;
77    public static final int CROP_MSG = 2;
78
79    private Dialog mMediaScanningDialog;
80    private MenuItem mSlideShowItem;
81    private SharedPreferences mPrefs;
82    private long mVideoSizeLimit = Long.MAX_VALUE;
83    private View mFooterOrganizeView;
84
85    private BroadcastReceiver mReceiver = null;
86
87    private final Handler mHandler = new Handler();
88    private boolean mLayoutComplete;
89    private boolean mPausing = true;
90    private ImageLoader mLoader;
91    private GridViewSpecial mGvs;
92
93    private Uri mCropResultUri;
94
95    // The index of the first picture in GridViewSpecial.
96    private int mSelectedIndex = GridViewSpecial.INDEX_NONE;
97    private float mScrollPosition = INVALID_POSITION;
98    private boolean mConfigurationChanged = false;
99
100    private HashSet<IImage> mMultiSelected = null;
101
102    @Override
103    public void onCreate(Bundle icicle) {
104        super.onCreate(icicle);
105
106        mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
107
108        // Must be called before setContentView().
109        requestWindowFeature(Window.FEATURE_CUSTOM_TITLE);
110
111        setContentView(R.layout.image_gallery);
112
113        getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE,
114                R.layout.custom_gallery_title);
115
116        mNoImagesView = findViewById(R.id.no_images);
117
118        mGvs = (GridViewSpecial) findViewById(R.id.grid);
119        mGvs.setListener(this);
120
121        mFooterOrganizeView = findViewById(R.id.footer_organize);
122
123        // consume all click events on the footer view
124        mFooterOrganizeView.setOnClickListener(Util.getNullOnClickListener());
125        initializeFooterButtons();
126
127        if (isPickIntent()) {
128            mVideoSizeLimit = getIntent().getLongExtra(
129                    MediaStore.EXTRA_SIZE_LIMIT, Long.MAX_VALUE);
130        } else {
131            mVideoSizeLimit = Long.MAX_VALUE;
132            mGvs.setOnCreateContextMenuListener(
133                    new CreateContextMenuListener());
134        }
135
136        setupInclusion();
137
138        mLoader = new ImageLoader(getContentResolver(), mHandler);
139    }
140
141    private void initializeFooterButtons() {
142        Button deleteButton = (Button) findViewById(R.id.button_delete);
143        deleteButton.setOnClickListener(new OnClickListener() {
144            public void onClick(View v) {
145                onDeleteMultipleClicked();
146            }
147        });
148
149        Button shareButton = (Button) findViewById(R.id.button_share);
150        shareButton.setOnClickListener(new OnClickListener() {
151            public void onClick(View v) {
152                onShareMultipleClicked();
153            }
154        });
155
156        Button closeButton = (Button) findViewById(R.id.button_close);
157        closeButton.setOnClickListener(new OnClickListener() {
158            public void onClick(View v) {
159                closeMultiSelectMode();
160            }
161        });
162    }
163
164    private MenuItem addSlideShowMenu(Menu menu) {
165        return menu.add(Menu.NONE, Menu.NONE, MenuHelper.POSITION_SLIDESHOW,
166                R.string.slide_show)
167                .setOnMenuItemClickListener(
168                new MenuItem.OnMenuItemClickListener() {
169                    public boolean onMenuItemClick(MenuItem item) {
170                        return onSlideShowClicked();
171                    }
172                }).setIcon(android.R.drawable.ic_menu_slideshow);
173    }
174
175    public boolean onSlideShowClicked() {
176        if (!canHandleEvent()) {
177            return false;
178        }
179        IImage img = getCurrentImage();
180        if (img == null) {
181            img = mAllImages.getImageAt(0);
182            if (img == null) {
183                return true;
184            }
185        }
186        Uri targetUri = img.fullSizeImageUri();
187        Uri thisUri = getIntent().getData();
188        if (thisUri != null) {
189            String bucket = thisUri.getQueryParameter("bucketId");
190            if (bucket != null) {
191                targetUri = targetUri.buildUpon()
192                        .appendQueryParameter("bucketId", bucket)
193                        .build();
194            }
195        }
196        Intent intent = new Intent(Intent.ACTION_VIEW, targetUri);
197        intent.putExtra("slideshow", true);
198        startActivity(intent);
199        return true;
200    }
201
202    private final Runnable mDeletePhotoRunnable = new Runnable() {
203        public void run() {
204            if (!canHandleEvent()) return;
205
206            IImage currentImage = getCurrentImage();
207
208            // The selection will be cleared when mGvs.stop() is called, so
209            // we need to call getCurrentImage() before mGvs.stop().
210            mGvs.stop();
211
212            if (currentImage != null) {
213                mAllImages.removeImage(currentImage);
214            }
215            mGvs.setImageList(mAllImages);
216            mGvs.start();
217
218            mNoImagesView.setVisibility(mAllImages.isEmpty()
219                    ? View.VISIBLE
220                    : View.GONE);
221        }
222    };
223
224    private Uri getCurrentImageUri() {
225        IImage image = getCurrentImage();
226        if (image != null) {
227            return image.fullSizeImageUri();
228        } else {
229            return null;
230        }
231    }
232
233    private IImage getCurrentImage() {
234        int currentSelection = mGvs.getCurrentSelection();
235        if (currentSelection < 0
236                || currentSelection >= mAllImages.getCount()) {
237            return null;
238        } else {
239            return mAllImages.getImageAt(currentSelection);
240        }
241    }
242
243    @Override
244    public void onConfigurationChanged(Configuration newConfig) {
245        super.onConfigurationChanged(newConfig);
246        mConfigurationChanged = true;
247    }
248
249    boolean canHandleEvent() {
250        // Don't process event in pause state.
251        return (!mPausing) && (mLayoutComplete);
252    }
253
254    @Override
255    public boolean onKeyDown(int keyCode, KeyEvent event) {
256        if (!canHandleEvent()) return false;
257        switch (keyCode) {
258            case KeyEvent.KEYCODE_DEL:
259                IImage image = getCurrentImage();
260                if (image != null) {
261                    MenuHelper.deleteImage(
262                            this, mDeletePhotoRunnable, getCurrentImage());
263                }
264                return true;
265        }
266        return super.onKeyDown(keyCode, event);
267    }
268
269    private boolean isPickIntent() {
270        String action = getIntent().getAction();
271        return (Intent.ACTION_PICK.equals(action)
272                || Intent.ACTION_GET_CONTENT.equals(action));
273    }
274
275    private void launchCropperOrFinish(IImage img) {
276        Bundle myExtras = getIntent().getExtras();
277
278        long size = MenuHelper.getImageFileSize(img);
279        if (size < 0) {
280            // Return if the image file is not available.
281            return;
282        }
283
284        if (size > mVideoSizeLimit) {
285            DialogInterface.OnClickListener buttonListener =
286                    new DialogInterface.OnClickListener() {
287                public void onClick(DialogInterface dialog, int which) {
288                    dialog.dismiss();
289                }
290            };
291            new AlertDialog.Builder(this)
292                    .setIcon(android.R.drawable.ic_dialog_info)
293                    .setTitle(R.string.file_info_title)
294                    .setMessage(R.string.video_exceed_mms_limit)
295                    .setNeutralButton(R.string.details_ok, buttonListener)
296                    .show();
297            return;
298        }
299
300        String cropValue = myExtras != null ? myExtras.getString("crop") : null;
301        if (cropValue != null) {
302            Bundle newExtras = new Bundle();
303            if (cropValue.equals("circle")) {
304                newExtras.putString("circleCrop", "true");
305            }
306
307            Intent cropIntent = new Intent();
308            cropIntent.setData(img.fullSizeImageUri());
309            cropIntent.setClass(this, CropImage.class);
310            cropIntent.putExtras(newExtras);
311
312            /* pass through any extras that were passed in */
313            cropIntent.putExtras(myExtras);
314            startActivityForResult(cropIntent, CROP_MSG);
315        } else {
316            Intent result = new Intent(null, img.fullSizeImageUri());
317            if (myExtras != null && myExtras.getBoolean("return-data")) {
318                // The size of a transaction should be below 100K.
319                Bitmap bitmap = img.fullSizeBitmap(
320                        IImage.UNCONSTRAINED, 100 * 1024);
321                if (bitmap != null) {
322                    result.putExtra("data", bitmap);
323                }
324            }
325            setResult(RESULT_OK, result);
326            finish();
327        }
328    }
329
330    @Override
331    protected void onActivityResult(int requestCode, int resultCode,
332            Intent data) {
333        switch (requestCode) {
334            case MenuHelper.RESULT_COMMON_MENU_CROP: {
335                if (resultCode == RESULT_OK) {
336
337                    // The CropImage activity passes back the Uri of the cropped
338                    // image as the Action rather than the Data.
339                    // We store this URI so we can move the selection box to it
340                    // later.
341                    mCropResultUri = Uri.parse(data.getAction());
342                }
343                break;
344            }
345            case CROP_MSG: {
346                if (resultCode == RESULT_OK) {
347                    setResult(resultCode, data);
348                    finish();
349                }
350                break;
351            }
352        }
353    }
354
355    @Override
356    public void onPause() {
357        super.onPause();
358        mPausing = true;
359
360        mLoader.stop();
361
362        mGvs.stop();
363
364        if (mReceiver != null) {
365            unregisterReceiver(mReceiver);
366            mReceiver = null;
367        }
368
369        // Now that we've paused the threads that are using the cursor it is
370        // safe to close it.
371        mAllImages.close();
372        mAllImages = null;
373    }
374
375    private void rebake(boolean unmounted, boolean scanning) {
376        mGvs.stop();
377        if (mAllImages != null) {
378            mAllImages.close();
379            mAllImages = null;
380        }
381
382        if (mMediaScanningDialog != null) {
383            mMediaScanningDialog.cancel();
384            mMediaScanningDialog = null;
385        }
386
387        if (scanning) {
388            mMediaScanningDialog = ProgressDialog.show(
389                    this,
390                    null,
391                    getResources().getString(R.string.wait),
392                    true,
393                    true);
394        }
395
396        mParam = allImages(!unmounted && !scanning);
397        mAllImages = ImageManager.makeImageList(getContentResolver(), mParam);
398
399        mGvs.setImageList(mAllImages);
400        mGvs.setDrawAdapter(this);
401        mGvs.setLoader(mLoader);
402        mGvs.start();
403        mNoImagesView.setVisibility(mAllImages.getCount() > 0
404                ? View.GONE
405                : View.VISIBLE);
406    }
407
408    @Override
409    protected void onSaveInstanceState(Bundle state) {
410        super.onSaveInstanceState(state);
411        state.putFloat(STATE_SCROLL_POSITION, mScrollPosition);
412        state.putInt(STATE_SELECTED_INDEX, mSelectedIndex);
413    }
414
415    @Override
416    protected void onRestoreInstanceState(Bundle state) {
417        super.onRestoreInstanceState(state);
418        mScrollPosition = state.getFloat(
419                STATE_SCROLL_POSITION, INVALID_POSITION);
420        mSelectedIndex = state.getInt(STATE_SELECTED_INDEX, 0);
421    }
422
423    @Override
424    public void onResume() {
425        super.onResume();
426
427        mGvs.setSizeChoice(Integer.parseInt(
428                mPrefs.getString("pref_gallery_size_key", "1")));
429        mGvs.requestFocus();
430
431        String sortOrder = mPrefs.getString("pref_gallery_sort_key", null);
432        if (sortOrder != null) {
433            mSortAscending = sortOrder.equals("ascending");
434        }
435
436        mPausing = false;
437
438        // install an intent filter to receive SD card related events.
439        IntentFilter intentFilter =
440                new IntentFilter(Intent.ACTION_MEDIA_MOUNTED);
441        intentFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
442        intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED);
443        intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED);
444        intentFilter.addAction(Intent.ACTION_MEDIA_EJECT);
445        intentFilter.addDataScheme("file");
446
447        mReceiver = new BroadcastReceiver() {
448            @Override
449            public void onReceive(Context context, Intent intent) {
450                String action = intent.getAction();
451                if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) {
452                    // SD card available
453                    // TODO put up a "please wait" message
454                    // TODO also listen for the media scanner finished message
455                } else if (action.equals(Intent.ACTION_MEDIA_UNMOUNTED)) {
456                    // SD card unavailable
457                    rebake(true, false);
458                } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_STARTED)) {
459                    rebake(false, true);
460                } else if (action.equals(
461                        Intent.ACTION_MEDIA_SCANNER_FINISHED)) {
462                    rebake(false, false);
463                } else if (action.equals(Intent.ACTION_MEDIA_EJECT)) {
464                    rebake(true, false);
465                }
466            }
467        };
468        registerReceiver(mReceiver, intentFilter);
469        rebake(false, ImageManager.isMediaScannerScanning(
470                getContentResolver()));
471    }
472
473    @Override
474    public boolean onCreateOptionsMenu(Menu menu) {
475        if (isPickIntent()) {
476            String type = getIntent().resolveType(this);
477            if (type != null) {
478                if (isImageType(type)) {
479                    MenuHelper.addCapturePictureMenuItems(menu, this);
480                } else if (isVideoType(type)) {
481                    MenuHelper.addCaptureVideoMenuItems(menu, this);
482                }
483            }
484        } else {
485            MenuHelper.addCaptureMenuItems(menu, this);
486            if ((mInclusion & ImageManager.INCLUDE_IMAGES) != 0) {
487                mSlideShowItem = addSlideShowMenu(menu);
488            }
489
490            MenuItem item = menu.add(Menu.NONE, Menu.NONE,
491                    MenuHelper.POSITION_GALLERY_SETTING,
492                    R.string.camerasettings);
493            item.setOnMenuItemClickListener(
494                    new MenuItem.OnMenuItemClickListener() {
495                public boolean onMenuItemClick(MenuItem item) {
496                    Intent preferences = new Intent();
497                    preferences.setClass(ImageGallery.this,
498                            GallerySettings.class);
499                    startActivity(preferences);
500                    return true;
501                }
502            });
503            item.setAlphabeticShortcut('p');
504            item.setIcon(android.R.drawable.ic_menu_preferences);
505
506            item = menu.add(Menu.NONE, Menu.NONE,
507                    MenuHelper.POSITION_MULTISELECT,
508                    R.string.multiselect);
509            item.setOnMenuItemClickListener(
510                    new MenuItem.OnMenuItemClickListener() {
511                public boolean onMenuItemClick(MenuItem item) {
512                    if (isInMultiSelectMode()) {
513                        closeMultiSelectMode();
514                    } else {
515                        openMultiSelectMode();
516                    }
517                    return true;
518                }
519            });
520            item.setIcon(R.drawable.ic_menu_multiselect_gallery);
521        }
522        return true;
523    }
524
525    @Override
526    public boolean onPrepareOptionsMenu(Menu menu) {
527        if (!canHandleEvent()) return false;
528        if ((mInclusion & ImageManager.INCLUDE_IMAGES) != 0) {
529            boolean videoSelected = isVideoSelected();
530            // TODO: Only enable slide show if there is at least one image in
531            // the folder.
532            if (mSlideShowItem != null) {
533                mSlideShowItem.setEnabled(!videoSelected);
534            }
535        }
536
537        return true;
538    }
539
540    private boolean isVideoSelected() {
541        IImage image = getCurrentImage();
542        return (image != null) && ImageManager.isVideo(image);
543    }
544
545    private boolean isImageType(String type) {
546        return type.equals("vnd.android.cursor.dir/image")
547                || type.equals("image/*");
548    }
549
550    private boolean isVideoType(String type) {
551        return type.equals("vnd.android.cursor.dir/video")
552                || type.equals("video/*");
553    }
554
555    // According to the intent, setup what we include (image/video) in the
556    // gallery and the title of the gallery.
557    private void setupInclusion() {
558        mInclusion = ImageManager.INCLUDE_IMAGES | ImageManager.INCLUDE_VIDEOS;
559
560        Intent intent = getIntent();
561        if (intent != null) {
562            String type = intent.resolveType(this);
563            TextView leftText = (TextView) findViewById(R.id.left_text);
564            if (type != null) {
565                if (isImageType(type)) {
566                    mInclusion = ImageManager.INCLUDE_IMAGES;
567                    if (isPickIntent()) {
568                        leftText.setText(R.string.pick_photos_gallery_title);
569                    } else {
570                        leftText.setText(R.string.photos_gallery_title);
571                    }
572                }
573                if (isVideoType(type)) {
574                    mInclusion = ImageManager.INCLUDE_VIDEOS;
575                    if (isPickIntent()) {
576                        leftText.setText(R.string.pick_videos_gallery_title);
577                    } else {
578                        leftText.setText(R.string.videos_gallery_title);
579                    }
580                }
581            }
582            Bundle extras = intent.getExtras();
583            String title = (extras != null)
584                    ? extras.getString("windowTitle")
585                    : null;
586            if (title != null && title.length() > 0) {
587                leftText.setText(title);
588            }
589
590            if (extras != null) {
591                mInclusion = (ImageManager.INCLUDE_IMAGES
592                        | ImageManager.INCLUDE_VIDEOS)
593                        & extras.getInt("mediaTypes", mInclusion);
594            }
595
596            if (extras != null && extras.getBoolean("pick-drm")) {
597                Log.d(TAG, "pick-drm is true");
598                mInclusion = ImageManager.INCLUDE_DRM_IMAGES;
599            }
600        }
601    }
602
603    // Returns the image list parameter which contains the subset of image/video
604    // we want.
605    private ImageManager.ImageListParam allImages(boolean storageAvailable) {
606        if (!storageAvailable) {
607            return ImageManager.getEmptyImageListParam();
608        } else {
609            Uri uri = getIntent().getData();
610            return ImageManager.getImageListParam(
611                    ImageManager.DataLocation.EXTERNAL,
612                    mInclusion,
613                    mSortAscending
614                    ? ImageManager.SORT_ASCENDING
615                    : ImageManager.SORT_DESCENDING,
616                    (uri != null)
617                    ? uri.getQueryParameter("bucketId")
618                    : null);
619        }
620    }
621
622    private void toggleMultiSelected(IImage image) {
623        int original = mMultiSelected.size();
624        if (!mMultiSelected.add(image)) {
625            mMultiSelected.remove(image);
626        }
627        mGvs.invalidate();
628        if (original == 0) showFooter();
629        if (mMultiSelected.size() == 0) hideFooter();
630    }
631
632    public void onImageClicked(int index) {
633        if (index < 0 || index >= mAllImages.getCount()) {
634            return;
635        }
636        mSelectedIndex = index;
637        mGvs.setSelectedIndex(index);
638
639        IImage image = mAllImages.getImageAt(index);
640
641        if (isInMultiSelectMode()) {
642            toggleMultiSelected(image);
643            return;
644        }
645
646        if (isPickIntent()) {
647            launchCropperOrFinish(image);
648        } else {
649            Intent intent;
650            if (image instanceof VideoObject) {
651                intent = new Intent(
652                        Intent.ACTION_VIEW, image.fullSizeImageUri());
653                intent.putExtra(MediaStore.EXTRA_SCREEN_ORIENTATION,
654                        ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
655            } else {
656                intent = new Intent(this, ViewImage.class);
657                intent.putExtra(ViewImage.KEY_IMAGE_LIST, mParam);
658                intent.setData(image.fullSizeImageUri());
659            }
660            startActivity(intent);
661        }
662    }
663
664    public void onImageTapped(int index) {
665        // In the multiselect mode, once the finger finishes tapping, we hide
666        // the selection box by setting the selected index to none. However, if
667        // we use the dpad center key, we will keep the selected index in order
668        // to show the the selection box. We do this because we have the
669        // multiselect marker on the images to indicate which of them are
670        // selected, so we don't need the selection box, but in the dpad case
671        // we still need the selection box to show as a "cursor".
672
673        if (isInMultiSelectMode()) {
674            mGvs.setSelectedIndex(GridViewSpecial.INDEX_NONE);
675            toggleMultiSelected(mAllImages.getImageAt(index));
676        } else {
677            onImageClicked(index);
678        }
679    }
680
681    private class CreateContextMenuListener implements
682            View.OnCreateContextMenuListener {
683        public void onCreateContextMenu(ContextMenu menu, View v,
684                ContextMenu.ContextMenuInfo menuInfo) {
685            if (!canHandleEvent()) return;
686
687            IImage image = getCurrentImage();
688
689            if (image == null) {
690                return;
691            }
692
693            boolean isImage = ImageManager.isImage(image);
694            if (isImage) {
695                menu.add(R.string.view)
696                        .setOnMenuItemClickListener(
697                        new MenuItem.OnMenuItemClickListener() {
698                            public boolean onMenuItemClick(MenuItem item) {
699                                if (!canHandleEvent()) return false;
700                                onImageClicked(mGvs.getCurrentSelection());
701                                return true;
702                            }
703                        });
704            }
705
706            menu.setHeaderTitle(isImage
707                    ? R.string.context_menu_header
708                    : R.string.video_context_menu_header);
709            if ((mInclusion & (ImageManager.INCLUDE_IMAGES
710                    | ImageManager.INCLUDE_VIDEOS)) != 0) {
711                MenuHelper.MenuItemsResult r = MenuHelper.addImageMenuItems(
712                        menu,
713                        MenuHelper.INCLUDE_ALL,
714                        ImageGallery.this,
715                        mHandler,
716                        mDeletePhotoRunnable,
717                        new MenuHelper.MenuInvoker() {
718                            public void run(MenuHelper.MenuCallback cb) {
719                                if (!canHandleEvent()) {
720                                    return;
721                                }
722                                cb.run(getCurrentImageUri(), getCurrentImage());
723                                mGvs.invalidateImage(mGvs.getCurrentSelection());
724                            }
725                        });
726
727                if (r != null) {
728                    r.gettingReadyToOpen(menu, image);
729                }
730
731                if (isImage) {
732                    MenuHelper.enableShowOnMapMenuItem(
733                            menu, MenuHelper.hasLatLngData(image));
734                    addSlideShowMenu(menu);
735                }
736            }
737        }
738    }
739
740    public void onLayoutComplete(boolean changed) {
741        mLayoutComplete = true;
742        if (mCropResultUri != null) {
743            IImage image = mAllImages.getImageForUri(mCropResultUri);
744            mCropResultUri = null;
745            if (image != null) {
746                mSelectedIndex = mAllImages.getImageIndex(image);
747            }
748        }
749        mGvs.setSelectedIndex(mSelectedIndex);
750        if (mScrollPosition == INVALID_POSITION) {
751            if (mSortAscending) {
752                mGvs.scrollTo(0, mGvs.getHeight());
753            } else {
754                mGvs.scrollToImage(0);
755            }
756        } else if (mConfigurationChanged) {
757            mConfigurationChanged = false;
758            mGvs.scrollTo(mScrollPosition);
759            if (mGvs.getCurrentSelection() != GridViewSpecial.INDEX_NONE) {
760                mGvs.scrollToVisible(mSelectedIndex);
761            }
762        } else {
763            mGvs.scrollTo(mScrollPosition);
764        }
765    }
766
767    public void onScroll(float scrollPosition) {
768        mScrollPosition = scrollPosition;
769    }
770
771    private Drawable mVideoOverlay;
772    private Drawable mVideoMmsErrorOverlay;
773    private Drawable mMultiSelectTrue;
774    private Drawable mMultiSelectFalse;
775
776    // mSrcRect and mDstRect are only used in drawImage, but we put them as
777    // instance variables to reduce the memory allocation overhead because
778    // drawImage() is called a lot.
779    private final Rect mSrcRect = new Rect();
780    private final Rect mDstRect = new Rect();
781
782    private final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
783
784    public void drawImage(Canvas canvas, IImage image,
785            Bitmap b, int xPos, int yPos, int w, int h) {
786        if (b != null) {
787            // if the image is close to the target size then crop,
788            // otherwise scale both the bitmap and the view should be
789            // square but I suppose that could change in the future.
790
791            int bw = b.getWidth();
792            int bh = b.getHeight();
793
794            int deltaW = bw - w;
795            int deltaH = bh - h;
796
797            if (deltaW >= 0 && deltaW < 10 &&
798                deltaH >= 0 && deltaH < 10) {
799                int halfDeltaW = deltaW / 2;
800                int halfDeltaH = deltaH / 2;
801                mSrcRect.set(0 + halfDeltaW, 0 + halfDeltaH,
802                        bw - halfDeltaW, bh - halfDeltaH);
803                mDstRect.set(xPos, yPos, xPos + w, yPos + h);
804                canvas.drawBitmap(b, mSrcRect, mDstRect, null);
805            } else {
806                mSrcRect.set(0, 0, bw, bh);
807                mDstRect.set(xPos, yPos, xPos + w, yPos + h);
808                canvas.drawBitmap(b, mSrcRect, mDstRect, mPaint);
809            }
810        } else {
811            // If the thumbnail cannot be drawn, put up an error icon
812            // instead
813            Bitmap error = getErrorBitmap(image);
814            int width = error.getWidth();
815            int height = error.getHeight();
816            mSrcRect.set(0, 0, width, height);
817            int left = (w - width) / 2 + xPos;
818            int top = (w - height) / 2 + yPos;
819            mDstRect.set(left, top, left + width, top + height);
820            canvas.drawBitmap(error, mSrcRect, mDstRect, null);
821        }
822
823        if (ImageManager.isVideo(image)) {
824            Drawable overlay = null;
825            long size = MenuHelper.getImageFileSize(image);
826            if (size >= 0 && size <= mVideoSizeLimit) {
827                if (mVideoOverlay == null) {
828                    mVideoOverlay = getResources().getDrawable(
829                            R.drawable.ic_gallery_video_overlay);
830                }
831                overlay = mVideoOverlay;
832            } else {
833                if (mVideoMmsErrorOverlay == null) {
834                    mVideoMmsErrorOverlay = getResources().getDrawable(
835                            R.drawable.ic_error_mms_video_overlay);
836                }
837                overlay = mVideoMmsErrorOverlay;
838                Paint paint = new Paint();
839                paint.setARGB(0x80, 0x00, 0x00, 0x00);
840                canvas.drawRect(xPos, yPos, xPos + w, yPos + h, paint);
841            }
842            int width = overlay.getIntrinsicWidth();
843            int height = overlay.getIntrinsicHeight();
844            int left = (w - width) / 2 + xPos;
845            int top = (h - height) / 2 + yPos;
846            mSrcRect.set(left, top, left + width, top + height);
847            overlay.setBounds(mSrcRect);
848            overlay.draw(canvas);
849        }
850    }
851
852    public boolean needsDecoration() {
853        return (mMultiSelected != null);
854    }
855
856    public void drawDecoration(Canvas canvas, IImage image,
857            int xPos, int yPos, int w, int h) {
858        if (mMultiSelected != null) {
859            initializeMultiSelectDrawables();
860
861            Drawable checkBox = mMultiSelected.contains(image)
862                    ? mMultiSelectTrue
863                    : mMultiSelectFalse;
864            int width = checkBox.getIntrinsicWidth();
865            int height = checkBox.getIntrinsicHeight();
866            int left = 5 + xPos;
867            int top = h - height - 5 + yPos;
868            mSrcRect.set(left, top, left + width, top + height);
869            checkBox.setBounds(mSrcRect);
870            checkBox.draw(canvas);
871        }
872    }
873
874    private void initializeMultiSelectDrawables() {
875        if (mMultiSelectTrue == null) {
876            mMultiSelectTrue = getResources()
877                    .getDrawable(R.drawable.btn_check_buttonless_on);
878        }
879        if (mMultiSelectFalse == null) {
880            mMultiSelectFalse = getResources()
881                    .getDrawable(R.drawable.btn_check_buttonless_off);
882        }
883    }
884
885    private Bitmap mMissingImageThumbnailBitmap;
886    private Bitmap mMissingVideoThumbnailBitmap;
887
888    // Create this bitmap lazily, and only once for all the ImageBlocks to
889    // use
890    public Bitmap getErrorBitmap(IImage image) {
891        if (ImageManager.isImage(image)) {
892            if (mMissingImageThumbnailBitmap == null) {
893                mMissingImageThumbnailBitmap = BitmapFactory.decodeResource(
894                        getResources(),
895                        R.drawable.ic_missing_thumbnail_picture);
896            }
897            return mMissingImageThumbnailBitmap;
898        } else {
899            if (mMissingVideoThumbnailBitmap == null) {
900                mMissingVideoThumbnailBitmap = BitmapFactory.decodeResource(
901                        getResources(), R.drawable.ic_missing_thumbnail_video);
902            }
903            return mMissingVideoThumbnailBitmap;
904        }
905    }
906
907    private Animation mFooterAppear;
908    private Animation mFooterDisappear;
909
910    private void showFooter() {
911        mFooterOrganizeView.setVisibility(View.VISIBLE);
912        if (mFooterAppear == null) {
913            mFooterAppear = AnimationUtils.loadAnimation(
914                    this, R.anim.footer_appear);
915        }
916        mFooterOrganizeView.startAnimation(mFooterAppear);
917    }
918
919    private void hideFooter() {
920        if (mFooterOrganizeView.getVisibility() != View.GONE) {
921            mFooterOrganizeView.setVisibility(View.GONE);
922            if (mFooterDisappear == null) {
923                mFooterDisappear = AnimationUtils.loadAnimation(
924                        this, R.anim.footer_disappear);
925            }
926            mFooterOrganizeView.startAnimation(mFooterDisappear);
927        }
928    }
929
930    private String getShareMultipleMimeType() {
931        final int FLAG_IMAGE = 1, FLAG_VIDEO = 2;
932        int flag = 0;
933        for (IImage image : mMultiSelected) {
934            flag |= ImageManager.isImage(image) ? FLAG_IMAGE : FLAG_VIDEO;
935        }
936        return flag == FLAG_IMAGE
937                ? "image/*"
938                : flag == FLAG_VIDEO ? "video/*" : "*/*";
939    }
940
941    private void onShareMultipleClicked() {
942        if (mMultiSelected.size() > 1) {
943            Intent intent = new Intent();
944            intent.setAction(Intent.ACTION_SEND_MULTIPLE);
945
946            String mimeType = getShareMultipleMimeType();
947            intent.setType(mimeType);
948            ArrayList<Parcelable> list = new ArrayList<Parcelable>();
949            for (IImage image : mMultiSelected) {
950                list.add(image.fullSizeImageUri());
951            }
952            intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, list);
953            try {
954                startActivity(Intent.createChooser(
955                        intent, getText(R.string.send_media_files)));
956            } catch (android.content.ActivityNotFoundException ex) {
957                Toast.makeText(this, R.string.no_way_to_share,
958                        Toast.LENGTH_SHORT).show();
959            }
960        } else if (mMultiSelected.size() == 1) {
961            IImage image = mMultiSelected.iterator().next();
962            Intent intent = new Intent();
963            intent.setAction(Intent.ACTION_SEND);
964            String mimeType = image.getMimeType();
965            intent.setType(mimeType);
966            intent.putExtra(Intent.EXTRA_STREAM, image.fullSizeImageUri());
967            boolean isImage = ImageManager.isImage(image);
968            try {
969                startActivity(Intent.createChooser(intent, getText(
970                        isImage ? R.string.sendImage : R.string.sendVideo)));
971            } catch (android.content.ActivityNotFoundException ex) {
972                Toast.makeText(this, isImage
973                        ? R.string.no_way_to_share_image
974                        : R.string.no_way_to_share_video,
975                        Toast.LENGTH_SHORT).show();
976            }
977        }
978    }
979
980    private void onDeleteMultipleClicked() {
981        Runnable action = new Runnable() {
982            public void run() {
983                ArrayList<Uri> uriList = new ArrayList<Uri>();
984                for (IImage image : mMultiSelected) {
985                    uriList.add(image.fullSizeImageUri());
986                }
987                closeMultiSelectMode();
988                Intent intent = new Intent(ImageGallery.this,
989                        DeleteImage.class);
990                intent.putExtra("delete-uris", uriList);
991                try {
992                    startActivity(intent);
993                } catch (ActivityNotFoundException ex) {
994                    Log.e(TAG, "Delete images fail", ex);
995                }
996            }
997        };
998        MenuHelper.deleteMultiple(this, action);
999    }
1000
1001    private boolean isInMultiSelectMode() {
1002        return mMultiSelected != null;
1003    }
1004
1005    private void closeMultiSelectMode() {
1006        if (mMultiSelected == null) return;
1007        mMultiSelected = null;
1008        mGvs.invalidate();
1009        hideFooter();
1010    }
1011
1012    private void openMultiSelectMode() {
1013        if (mMultiSelected != null) return;
1014        mMultiSelected = new HashSet<IImage>();
1015        mGvs.invalidate();
1016    }
1017
1018}
1019