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