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