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                 Context.RECEIVER_EXPORTED_UNAUDITED);
472         rebake(false, ImageManager.isMediaScannerScanning(
473                 getContentResolver()));
474     }
475 
476     @Override
477     public boolean onCreateOptionsMenu(Menu menu) {
478         if (isPickIntent()) {
479             String type = getIntent().resolveType(this);
480             if (type != null) {
481                 if (isImageType(type)) {
482                     MenuHelper.addCapturePictureMenuItems(menu, this);
483                 } else if (isVideoType(type)) {
484                     MenuHelper.addCaptureVideoMenuItems(menu, this);
485                 }
486             }
487         } else {
488             MenuHelper.addCaptureMenuItems(menu, this);
489             if ((mInclusion & ImageManager.INCLUDE_IMAGES) != 0) {
490                 mSlideShowItem = addSlideShowMenu(menu);
491             }
492 
493             MenuItem item = menu.add(Menu.NONE, Menu.NONE,
494                     MenuHelper.POSITION_GALLERY_SETTING,
495                     R.string.camerasettings);
496             item.setOnMenuItemClickListener(
497                     new MenuItem.OnMenuItemClickListener() {
498                 public boolean onMenuItemClick(MenuItem item) {
499                     Intent preferences = new Intent();
500                     preferences.setClass(ImageGallery.this,
501                             GallerySettings.class);
502                     startActivity(preferences);
503                     return true;
504                 }
505             });
506             item.setAlphabeticShortcut('p');
507             item.setIcon(android.R.drawable.ic_menu_preferences);
508 
509             item = menu.add(Menu.NONE, Menu.NONE,
510                     MenuHelper.POSITION_MULTISELECT,
511                     R.string.multiselect);
512             item.setOnMenuItemClickListener(
513                     new MenuItem.OnMenuItemClickListener() {
514                 public boolean onMenuItemClick(MenuItem item) {
515                     if (isInMultiSelectMode()) {
516                         closeMultiSelectMode();
517                     } else {
518                         openMultiSelectMode();
519                     }
520                     return true;
521                 }
522             });
523             item.setIcon(R.drawable.ic_menu_multiselect_gallery);
524         }
525         return true;
526     }
527 
528     @Override
529     public boolean onPrepareOptionsMenu(Menu menu) {
530         if (!canHandleEvent()) return false;
531         if ((mInclusion & ImageManager.INCLUDE_IMAGES) != 0) {
532             boolean videoSelected = isVideoSelected();
533             // TODO: Only enable slide show if there is at least one image in
534             // the folder.
535             if (mSlideShowItem != null) {
536                 mSlideShowItem.setEnabled(!videoSelected);
537             }
538         }
539 
540         return true;
541     }
542 
543     private boolean isVideoSelected() {
544         IImage image = getCurrentImage();
545         return (image != null) && ImageManager.isVideo(image);
546     }
547 
548     private boolean isImageType(String type) {
549         return type.equals("vnd.android.cursor.dir/image")
550                 || type.equals("image/*");
551     }
552 
553     private boolean isVideoType(String type) {
554         return type.equals("vnd.android.cursor.dir/video")
555                 || type.equals("video/*");
556     }
557 
558     // According to the intent, setup what we include (image/video) in the
559     // gallery and the title of the gallery.
560     private void setupInclusion() {
561         mInclusion = ImageManager.INCLUDE_IMAGES | ImageManager.INCLUDE_VIDEOS;
562 
563         Intent intent = getIntent();
564         if (intent != null) {
565             String type = intent.resolveType(this);
566             TextView leftText = (TextView) findViewById(R.id.left_text);
567             if (type != null) {
568                 if (isImageType(type)) {
569                     mInclusion = ImageManager.INCLUDE_IMAGES;
570                     if (isPickIntent()) {
571                         leftText.setText(R.string.pick_photos_gallery_title);
572                     } else {
573                         leftText.setText(R.string.photos_gallery_title);
574                     }
575                 }
576                 if (isVideoType(type)) {
577                     mInclusion = ImageManager.INCLUDE_VIDEOS;
578                     if (isPickIntent()) {
579                         leftText.setText(R.string.pick_videos_gallery_title);
580                     } else {
581                         leftText.setText(R.string.videos_gallery_title);
582                     }
583                 }
584             }
585             Bundle extras = intent.getExtras();
586             String title = (extras != null)
587                     ? extras.getString("windowTitle")
588                     : null;
589             if (title != null && title.length() > 0) {
590                 leftText.setText(title);
591             }
592 
593             if (extras != null) {
594                 mInclusion = (ImageManager.INCLUDE_IMAGES
595                         | ImageManager.INCLUDE_VIDEOS)
596                         & extras.getInt("mediaTypes", mInclusion);
597             }
598         }
599     }
600 
601     // Returns the image list parameter which contains the subset of image/video
602     // we want.
603     private ImageManager.ImageListParam allImages(boolean storageAvailable) {
604         if (!storageAvailable) {
605             return ImageManager.getEmptyImageListParam();
606         } else {
607             Uri uri = getIntent().getData();
608             return ImageManager.getImageListParam(
609                     ImageManager.DataLocation.EXTERNAL,
610                     mInclusion,
611                     mSortAscending
612                     ? ImageManager.SORT_ASCENDING
613                     : ImageManager.SORT_DESCENDING,
614                     (uri != null)
615                     ? uri.getQueryParameter("bucketId")
616                     : null);
617         }
618     }
619 
620     private void toggleMultiSelected(IImage image) {
621         int original = mMultiSelected.size();
622         if (!mMultiSelected.add(image)) {
623             mMultiSelected.remove(image);
624         }
625         mGvs.invalidate();
626         if (original == 0) showFooter();
627         if (mMultiSelected.size() == 0) hideFooter();
628     }
629 
630     public void onImageClicked(int index) {
631         if (index < 0 || index >= mAllImages.getCount()) {
632             return;
633         }
634         mSelectedIndex = index;
635         mGvs.setSelectedIndex(index);
636 
637         IImage image = mAllImages.getImageAt(index);
638 
639         if (isInMultiSelectMode()) {
640             toggleMultiSelected(image);
641             return;
642         }
643 
644         if (isPickIntent()) {
645             launchCropperOrFinish(image);
646         } else {
647             Intent intent;
648             if (image instanceof VideoObject) {
649                 intent = new Intent(
650                         Intent.ACTION_VIEW, image.fullSizeImageUri());
651                 intent.putExtra(MediaStore.EXTRA_SCREEN_ORIENTATION,
652                         ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
653             } else {
654                 intent = new Intent(this, ViewImage.class);
655                 intent.putExtra(ViewImage.KEY_IMAGE_LIST, mParam);
656                 intent.setData(image.fullSizeImageUri());
657             }
658             startActivity(intent);
659         }
660     }
661 
662     public void onImageTapped(int index) {
663         // In the multiselect mode, once the finger finishes tapping, we hide
664         // the selection box by setting the selected index to none. However, if
665         // we use the dpad center key, we will keep the selected index in order
666         // to show the the selection box. We do this because we have the
667         // multiselect marker on the images to indicate which of them are
668         // selected, so we don't need the selection box, but in the dpad case
669         // we still need the selection box to show as a "cursor".
670 
671         if (isInMultiSelectMode()) {
672             mGvs.setSelectedIndex(GridViewSpecial.INDEX_NONE);
673             toggleMultiSelected(mAllImages.getImageAt(index));
674         } else {
675             onImageClicked(index);
676         }
677     }
678 
679     private class CreateContextMenuListener implements
680             View.OnCreateContextMenuListener {
681         public void onCreateContextMenu(ContextMenu menu, View v,
682                 ContextMenu.ContextMenuInfo menuInfo) {
683             if (!canHandleEvent()) return;
684 
685             IImage image = getCurrentImage();
686 
687             if (image == null) {
688                 return;
689             }
690 
691             boolean isImage = ImageManager.isImage(image);
692             if (isImage) {
693                 menu.add(R.string.view)
694                         .setOnMenuItemClickListener(
695                         new MenuItem.OnMenuItemClickListener() {
696                             public boolean onMenuItemClick(MenuItem item) {
697                                 if (!canHandleEvent()) return false;
698                                 onImageClicked(mGvs.getCurrentSelection());
699                                 return true;
700                             }
701                         });
702             }
703 
704             menu.setHeaderTitle(isImage
705                     ? R.string.context_menu_header
706                     : R.string.video_context_menu_header);
707             if ((mInclusion & (ImageManager.INCLUDE_IMAGES
708                     | ImageManager.INCLUDE_VIDEOS)) != 0) {
709                 MenuHelper.MenuItemsResult r = MenuHelper.addImageMenuItems(
710                         menu,
711                         MenuHelper.INCLUDE_ALL,
712                         ImageGallery.this,
713                         mHandler,
714                         mDeletePhotoRunnable,
715                         new MenuHelper.MenuInvoker() {
716                             public void run(MenuHelper.MenuCallback cb) {
717                                 if (!canHandleEvent()) {
718                                     return;
719                                 }
720                                 cb.run(getCurrentImageUri(), getCurrentImage());
721                                 mGvs.invalidateImage(mGvs.getCurrentSelection());
722                             }
723                         });
724 
725                 if (r != null) {
726                     r.gettingReadyToOpen(menu, image);
727                 }
728 
729                 if (isImage) {
730                     MenuHelper.enableShowOnMapMenuItem(
731                             menu, MenuHelper.hasLatLngData(image));
732                     addSlideShowMenu(menu);
733                 }
734             }
735         }
736     }
737 
738     public void onLayoutComplete(boolean changed) {
739         mLayoutComplete = true;
740         if (mCropResultUri != null) {
741             IImage image = mAllImages.getImageForUri(mCropResultUri);
742             mCropResultUri = null;
743             if (image != null) {
744                 mSelectedIndex = mAllImages.getImageIndex(image);
745             }
746         }
747         mGvs.setSelectedIndex(mSelectedIndex);
748         if (mScrollPosition == INVALID_POSITION) {
749             if (mSortAscending) {
750                 mGvs.scrollTo(0, mGvs.getHeight());
751             } else {
752                 mGvs.scrollToImage(0);
753             }
754         } else if (mConfigurationChanged) {
755             mConfigurationChanged = false;
756             mGvs.scrollTo(mScrollPosition);
757             if (mGvs.getCurrentSelection() != GridViewSpecial.INDEX_NONE) {
758                 mGvs.scrollToVisible(mSelectedIndex);
759             }
760         } else {
761             mGvs.scrollTo(mScrollPosition);
762         }
763     }
764 
765     public void onScroll(float scrollPosition) {
766         mScrollPosition = scrollPosition;
767     }
768 
769     private Drawable mVideoOverlay;
770     private Drawable mVideoMmsErrorOverlay;
771     private Drawable mMultiSelectTrue;
772     private Drawable mMultiSelectFalse;
773 
774     // mSrcRect and mDstRect are only used in drawImage, but we put them as
775     // instance variables to reduce the memory allocation overhead because
776     // drawImage() is called a lot.
777     private final Rect mSrcRect = new Rect();
778     private final Rect mDstRect = new Rect();
779 
780     private final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
781 
782     public void drawImage(Canvas canvas, IImage image,
783             Bitmap b, int xPos, int yPos, int w, int h) {
784         if (b != null) {
785             // if the image is close to the target size then crop,
786             // otherwise scale both the bitmap and the view should be
787             // square but I suppose that could change in the future.
788 
789             int bw = b.getWidth();
790             int bh = b.getHeight();
791 
792             int deltaW = bw - w;
793             int deltaH = bh - h;
794 
795             if (deltaW >= 0 && deltaW < 10 &&
796                 deltaH >= 0 && deltaH < 10) {
797                 int halfDeltaW = deltaW / 2;
798                 int halfDeltaH = deltaH / 2;
799                 mSrcRect.set(0 + halfDeltaW, 0 + halfDeltaH,
800                         bw - halfDeltaW, bh - halfDeltaH);
801                 mDstRect.set(xPos, yPos, xPos + w, yPos + h);
802                 canvas.drawBitmap(b, mSrcRect, mDstRect, null);
803             } else {
804                 mSrcRect.set(0, 0, bw, bh);
805                 mDstRect.set(xPos, yPos, xPos + w, yPos + h);
806                 canvas.drawBitmap(b, mSrcRect, mDstRect, mPaint);
807             }
808         } else {
809             // If the thumbnail cannot be drawn, put up an error icon
810             // instead
811             Bitmap error = getErrorBitmap(image);
812             int width = error.getWidth();
813             int height = error.getHeight();
814             mSrcRect.set(0, 0, width, height);
815             int left = (w - width) / 2 + xPos;
816             int top = (w - height) / 2 + yPos;
817             mDstRect.set(left, top, left + width, top + height);
818             canvas.drawBitmap(error, mSrcRect, mDstRect, null);
819         }
820 
821         if (ImageManager.isVideo(image)) {
822             Drawable overlay = null;
823             long size = MenuHelper.getImageFileSize(image);
824             if (size >= 0 && size <= mVideoSizeLimit) {
825                 if (mVideoOverlay == null) {
826                     mVideoOverlay = getResources().getDrawable(
827                             R.drawable.ic_gallery_video_overlay);
828                 }
829                 overlay = mVideoOverlay;
830             } else {
831                 if (mVideoMmsErrorOverlay == null) {
832                     mVideoMmsErrorOverlay = getResources().getDrawable(
833                             R.drawable.ic_error_mms_video_overlay);
834                 }
835                 overlay = mVideoMmsErrorOverlay;
836                 Paint paint = new Paint();
837                 paint.setARGB(0x80, 0x00, 0x00, 0x00);
838                 canvas.drawRect(xPos, yPos, xPos + w, yPos + h, paint);
839             }
840             int width = overlay.getIntrinsicWidth();
841             int height = overlay.getIntrinsicHeight();
842             int left = (w - width) / 2 + xPos;
843             int top = (h - height) / 2 + yPos;
844             mSrcRect.set(left, top, left + width, top + height);
845             overlay.setBounds(mSrcRect);
846             overlay.draw(canvas);
847         }
848     }
849 
850     public boolean needsDecoration() {
851         return (mMultiSelected != null);
852     }
853 
854     public void drawDecoration(Canvas canvas, IImage image,
855             int xPos, int yPos, int w, int h) {
856         if (mMultiSelected != null) {
857             initializeMultiSelectDrawables();
858 
859             Drawable checkBox = mMultiSelected.contains(image)
860                     ? mMultiSelectTrue
861                     : mMultiSelectFalse;
862             int width = checkBox.getIntrinsicWidth();
863             int height = checkBox.getIntrinsicHeight();
864             int left = 5 + xPos;
865             int top = h - height - 5 + yPos;
866             mSrcRect.set(left, top, left + width, top + height);
867             checkBox.setBounds(mSrcRect);
868             checkBox.draw(canvas);
869         }
870     }
871 
872     private void initializeMultiSelectDrawables() {
873         if (mMultiSelectTrue == null) {
874             mMultiSelectTrue = getResources()
875                     .getDrawable(R.drawable.btn_check_buttonless_on);
876         }
877         if (mMultiSelectFalse == null) {
878             mMultiSelectFalse = getResources()
879                     .getDrawable(R.drawable.btn_check_buttonless_off);
880         }
881     }
882 
883     private Bitmap mMissingImageThumbnailBitmap;
884     private Bitmap mMissingVideoThumbnailBitmap;
885 
886     // Create this bitmap lazily, and only once for all the ImageBlocks to
887     // use
888     public Bitmap getErrorBitmap(IImage image) {
889         if (ImageManager.isImage(image)) {
890             if (mMissingImageThumbnailBitmap == null) {
891                 mMissingImageThumbnailBitmap = BitmapFactory.decodeResource(
892                         getResources(),
893                         R.drawable.ic_missing_thumbnail_picture);
894             }
895             return mMissingImageThumbnailBitmap;
896         } else {
897             if (mMissingVideoThumbnailBitmap == null) {
898                 mMissingVideoThumbnailBitmap = BitmapFactory.decodeResource(
899                         getResources(), R.drawable.ic_missing_thumbnail_video);
900             }
901             return mMissingVideoThumbnailBitmap;
902         }
903     }
904 
905     private Animation mFooterAppear;
906     private Animation mFooterDisappear;
907 
908     private void showFooter() {
909         mFooterOrganizeView.setVisibility(View.VISIBLE);
910         if (mFooterAppear == null) {
911             mFooterAppear = AnimationUtils.loadAnimation(
912                     this, R.anim.footer_appear);
913         }
914         mFooterOrganizeView.startAnimation(mFooterAppear);
915     }
916 
917     private void hideFooter() {
918         if (mFooterOrganizeView.getVisibility() != View.GONE) {
919             mFooterOrganizeView.setVisibility(View.GONE);
920             if (mFooterDisappear == null) {
921                 mFooterDisappear = AnimationUtils.loadAnimation(
922                         this, R.anim.footer_disappear);
923             }
924             mFooterOrganizeView.startAnimation(mFooterDisappear);
925         }
926     }
927 
928     private String getShareMultipleMimeType() {
929         final int FLAG_IMAGE = 1, FLAG_VIDEO = 2;
930         int flag = 0;
931         for (IImage image : mMultiSelected) {
932             flag |= ImageManager.isImage(image) ? FLAG_IMAGE : FLAG_VIDEO;
933         }
934         return flag == FLAG_IMAGE
935                 ? "image/*"
936                 : flag == FLAG_VIDEO ? "video/*" : "*/*";
937     }
938 
939     private void onShareMultipleClicked() {
940         if (mMultiSelected == null) return;
941         if (mMultiSelected.size() > 1) {
942             Intent intent = new Intent();
943             intent.setAction(Intent.ACTION_SEND_MULTIPLE);
944 
945             String mimeType = getShareMultipleMimeType();
946             intent.setType(mimeType);
947             ArrayList<Parcelable> list = new ArrayList<Parcelable>();
948             for (IImage image : mMultiSelected) {
949                 list.add(image.fullSizeImageUri());
950             }
951             intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, list);
952             try {
953                 startActivity(Intent.createChooser(
954                         intent, getText(R.string.send_media_files)));
955             } catch (android.content.ActivityNotFoundException ex) {
956                 Toast.makeText(this, R.string.no_way_to_share,
957                         Toast.LENGTH_SHORT).show();
958             }
959         } else if (mMultiSelected.size() == 1) {
960             IImage image = mMultiSelected.iterator().next();
961             Intent intent = new Intent();
962             intent.setAction(Intent.ACTION_SEND);
963             String mimeType = image.getMimeType();
964             intent.setType(mimeType);
965             intent.putExtra(Intent.EXTRA_STREAM, image.fullSizeImageUri());
966             boolean isImage = ImageManager.isImage(image);
967             try {
968                 startActivity(Intent.createChooser(intent, getText(
969                         isImage ? R.string.sendImage : R.string.sendVideo)));
970             } catch (android.content.ActivityNotFoundException ex) {
971                 Toast.makeText(this, isImage
972                         ? R.string.no_way_to_share_image
973                         : R.string.no_way_to_share_video,
974                         Toast.LENGTH_SHORT).show();
975             }
976         }
977     }
978 
979     private void onDeleteMultipleClicked() {
980         if (mMultiSelected == null) return;
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