Home | History | Annotate | Download | only in camera
      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
    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 
    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 
    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