1/*
2 * Copyright (C) 2007 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.camera;
18
19import com.android.gallery.R;
20
21import android.app.Activity;
22import android.app.AlertDialog;
23import android.app.Dialog;
24import android.app.ProgressDialog;
25import android.content.ActivityNotFoundException;
26import android.content.BroadcastReceiver;
27import android.content.Context;
28import android.content.DialogInterface;
29import android.content.Intent;
30import android.content.IntentFilter;
31import android.content.SharedPreferences;
32import android.content.pm.ActivityInfo;
33import android.content.res.Configuration;
34import android.graphics.Bitmap;
35import android.graphics.BitmapFactory;
36import android.graphics.Canvas;
37import android.graphics.Paint;
38import android.graphics.Rect;
39import android.graphics.drawable.Drawable;
40import android.net.Uri;
41import android.os.Bundle;
42import android.os.Handler;
43import android.os.Parcelable;
44import android.preference.PreferenceManager;
45import android.provider.MediaStore;
46import android.util.Log;
47import android.view.ContextMenu;
48import android.view.KeyEvent;
49import android.view.Menu;
50import android.view.MenuItem;
51import android.view.View;
52import android.view.Window;
53import android.view.View.OnClickListener;
54import android.view.animation.Animation;
55import android.view.animation.AnimationUtils;
56import android.widget.Button;
57import android.widget.TextView;
58import android.widget.Toast;
59
60import com.android.camera.gallery.IImage;
61import com.android.camera.gallery.IImageList;
62import com.android.camera.gallery.VideoObject;
63
64import java.util.ArrayList;
65import java.util.HashSet;
66
67public 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            if (extras != null && extras.getBoolean("pick-drm")) {
599                Log.d(TAG, "pick-drm is true");
600                mInclusion = ImageManager.INCLUDE_DRM_IMAGES;
601            }
602        }
603    }
604
605    // Returns the image list parameter which contains the subset of image/video
606    // we want.
607    private ImageManager.ImageListParam allImages(boolean storageAvailable) {
608        if (!storageAvailable) {
609            return ImageManager.getEmptyImageListParam();
610        } else {
611            Uri uri = getIntent().getData();
612            return ImageManager.getImageListParam(
613                    ImageManager.DataLocation.EXTERNAL,
614                    mInclusion,
615                    mSortAscending
616                    ? ImageManager.SORT_ASCENDING
617                    : ImageManager.SORT_DESCENDING,
618                    (uri != null)
619                    ? uri.getQueryParameter("bucketId")
620                    : null);
621        }
622    }
623
624    private void toggleMultiSelected(IImage image) {
625        int original = mMultiSelected.size();
626        if (!mMultiSelected.add(image)) {
627            mMultiSelected.remove(image);
628        }
629        mGvs.invalidate();
630        if (original == 0) showFooter();
631        if (mMultiSelected.size() == 0) hideFooter();
632    }
633
634    public void onImageClicked(int index) {
635        if (index < 0 || index >= mAllImages.getCount()) {
636            return;
637        }
638        mSelectedIndex = index;
639        mGvs.setSelectedIndex(index);
640
641        IImage image = mAllImages.getImageAt(index);
642
643        if (isInMultiSelectMode()) {
644            toggleMultiSelected(image);
645            return;
646        }
647
648        if (isPickIntent()) {
649            launchCropperOrFinish(image);
650        } else {
651            Intent intent;
652            if (image instanceof VideoObject) {
653                intent = new Intent(
654                        Intent.ACTION_VIEW, image.fullSizeImageUri());
655                intent.putExtra(MediaStore.EXTRA_SCREEN_ORIENTATION,
656                        ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
657            } else {
658                intent = new Intent(this, ViewImage.class);
659                intent.putExtra(ViewImage.KEY_IMAGE_LIST, mParam);
660                intent.setData(image.fullSizeImageUri());
661            }
662            startActivity(intent);
663        }
664    }
665
666    public void onImageTapped(int index) {
667        // In the multiselect mode, once the finger finishes tapping, we hide
668        // the selection box by setting the selected index to none. However, if
669        // we use the dpad center key, we will keep the selected index in order
670        // to show the the selection box. We do this because we have the
671        // multiselect marker on the images to indicate which of them are
672        // selected, so we don't need the selection box, but in the dpad case
673        // we still need the selection box to show as a "cursor".
674
675        if (isInMultiSelectMode()) {
676            mGvs.setSelectedIndex(GridViewSpecial.INDEX_NONE);
677            toggleMultiSelected(mAllImages.getImageAt(index));
678        } else {
679            onImageClicked(index);
680        }
681    }
682
683    private class CreateContextMenuListener implements
684            View.OnCreateContextMenuListener {
685        public void onCreateContextMenu(ContextMenu menu, View v,
686                ContextMenu.ContextMenuInfo menuInfo) {
687            if (!canHandleEvent()) return;
688
689            IImage image = getCurrentImage();
690
691            if (image == null) {
692                return;
693            }
694
695            boolean isImage = ImageManager.isImage(image);
696            if (isImage) {
697                menu.add(R.string.view)
698                        .setOnMenuItemClickListener(
699                        new MenuItem.OnMenuItemClickListener() {
700                            public boolean onMenuItemClick(MenuItem item) {
701                                if (!canHandleEvent()) return false;
702                                onImageClicked(mGvs.getCurrentSelection());
703                                return true;
704                            }
705                        });
706            }
707
708            menu.setHeaderTitle(isImage
709                    ? R.string.context_menu_header
710                    : R.string.video_context_menu_header);
711            if ((mInclusion & (ImageManager.INCLUDE_IMAGES
712                    | ImageManager.INCLUDE_VIDEOS)) != 0) {
713                MenuHelper.MenuItemsResult r = MenuHelper.addImageMenuItems(
714                        menu,
715                        MenuHelper.INCLUDE_ALL,
716                        ImageGallery.this,
717                        mHandler,
718                        mDeletePhotoRunnable,
719                        new MenuHelper.MenuInvoker() {
720                            public void run(MenuHelper.MenuCallback cb) {
721                                if (!canHandleEvent()) {
722                                    return;
723                                }
724                                cb.run(getCurrentImageUri(), getCurrentImage());
725                                mGvs.invalidateImage(mGvs.getCurrentSelection());
726                            }
727                        });
728
729                if (r != null) {
730                    r.gettingReadyToOpen(menu, image);
731                }
732
733                if (isImage) {
734                    MenuHelper.enableShowOnMapMenuItem(
735                            menu, MenuHelper.hasLatLngData(image));
736                    addSlideShowMenu(menu);
737                }
738            }
739        }
740    }
741
742    public void onLayoutComplete(boolean changed) {
743        mLayoutComplete = true;
744        if (mCropResultUri != null) {
745            IImage image = mAllImages.getImageForUri(mCropResultUri);
746            mCropResultUri = null;
747            if (image != null) {
748                mSelectedIndex = mAllImages.getImageIndex(image);
749            }
750        }
751        mGvs.setSelectedIndex(mSelectedIndex);
752        if (mScrollPosition == INVALID_POSITION) {
753            if (mSortAscending) {
754                mGvs.scrollTo(0, mGvs.getHeight());
755            } else {
756                mGvs.scrollToImage(0);
757            }
758        } else if (mConfigurationChanged) {
759            mConfigurationChanged = false;
760            mGvs.scrollTo(mScrollPosition);
761            if (mGvs.getCurrentSelection() != GridViewSpecial.INDEX_NONE) {
762                mGvs.scrollToVisible(mSelectedIndex);
763            }
764        } else {
765            mGvs.scrollTo(mScrollPosition);
766        }
767    }
768
769    public void onScroll(float scrollPosition) {
770        mScrollPosition = scrollPosition;
771    }
772
773    private Drawable mVideoOverlay;
774    private Drawable mVideoMmsErrorOverlay;
775    private Drawable mMultiSelectTrue;
776    private Drawable mMultiSelectFalse;
777
778    // mSrcRect and mDstRect are only used in drawImage, but we put them as
779    // instance variables to reduce the memory allocation overhead because
780    // drawImage() is called a lot.
781    private final Rect mSrcRect = new Rect();
782    private final Rect mDstRect = new Rect();
783
784    private final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
785
786    public void drawImage(Canvas canvas, IImage image,
787            Bitmap b, int xPos, int yPos, int w, int h) {
788        if (b != null) {
789            // if the image is close to the target size then crop,
790            // otherwise scale both the bitmap and the view should be
791            // square but I suppose that could change in the future.
792
793            int bw = b.getWidth();
794            int bh = b.getHeight();
795
796            int deltaW = bw - w;
797            int deltaH = bh - h;
798
799            if (deltaW >= 0 && deltaW < 10 &&
800                deltaH >= 0 && deltaH < 10) {
801                int halfDeltaW = deltaW / 2;
802                int halfDeltaH = deltaH / 2;
803                mSrcRect.set(0 + halfDeltaW, 0 + halfDeltaH,
804                        bw - halfDeltaW, bh - halfDeltaH);
805                mDstRect.set(xPos, yPos, xPos + w, yPos + h);
806                canvas.drawBitmap(b, mSrcRect, mDstRect, null);
807            } else {
808                mSrcRect.set(0, 0, bw, bh);
809                mDstRect.set(xPos, yPos, xPos + w, yPos + h);
810                canvas.drawBitmap(b, mSrcRect, mDstRect, mPaint);
811            }
812        } else {
813            // If the thumbnail cannot be drawn, put up an error icon
814            // instead
815            Bitmap error = getErrorBitmap(image);
816            int width = error.getWidth();
817            int height = error.getHeight();
818            mSrcRect.set(0, 0, width, height);
819            int left = (w - width) / 2 + xPos;
820            int top = (w - height) / 2 + yPos;
821            mDstRect.set(left, top, left + width, top + height);
822            canvas.drawBitmap(error, mSrcRect, mDstRect, null);
823        }
824
825        if (ImageManager.isVideo(image)) {
826            Drawable overlay = null;
827            long size = MenuHelper.getImageFileSize(image);
828            if (size >= 0 && size <= mVideoSizeLimit) {
829                if (mVideoOverlay == null) {
830                    mVideoOverlay = getResources().getDrawable(
831                            R.drawable.ic_gallery_video_overlay);
832                }
833                overlay = mVideoOverlay;
834            } else {
835                if (mVideoMmsErrorOverlay == null) {
836                    mVideoMmsErrorOverlay = getResources().getDrawable(
837                            R.drawable.ic_error_mms_video_overlay);
838                }
839                overlay = mVideoMmsErrorOverlay;
840                Paint paint = new Paint();
841                paint.setARGB(0x80, 0x00, 0x00, 0x00);
842                canvas.drawRect(xPos, yPos, xPos + w, yPos + h, paint);
843            }
844            int width = overlay.getIntrinsicWidth();
845            int height = overlay.getIntrinsicHeight();
846            int left = (w - width) / 2 + xPos;
847            int top = (h - height) / 2 + yPos;
848            mSrcRect.set(left, top, left + width, top + height);
849            overlay.setBounds(mSrcRect);
850            overlay.draw(canvas);
851        }
852    }
853
854    public boolean needsDecoration() {
855        return (mMultiSelected != null);
856    }
857
858    public void drawDecoration(Canvas canvas, IImage image,
859            int xPos, int yPos, int w, int h) {
860        if (mMultiSelected != null) {
861            initializeMultiSelectDrawables();
862
863            Drawable checkBox = mMultiSelected.contains(image)
864                    ? mMultiSelectTrue
865                    : mMultiSelectFalse;
866            int width = checkBox.getIntrinsicWidth();
867            int height = checkBox.getIntrinsicHeight();
868            int left = 5 + xPos;
869            int top = h - height - 5 + yPos;
870            mSrcRect.set(left, top, left + width, top + height);
871            checkBox.setBounds(mSrcRect);
872            checkBox.draw(canvas);
873        }
874    }
875
876    private void initializeMultiSelectDrawables() {
877        if (mMultiSelectTrue == null) {
878            mMultiSelectTrue = getResources()
879                    .getDrawable(R.drawable.btn_check_buttonless_on);
880        }
881        if (mMultiSelectFalse == null) {
882            mMultiSelectFalse = getResources()
883                    .getDrawable(R.drawable.btn_check_buttonless_off);
884        }
885    }
886
887    private Bitmap mMissingImageThumbnailBitmap;
888    private Bitmap mMissingVideoThumbnailBitmap;
889
890    // Create this bitmap lazily, and only once for all the ImageBlocks to
891    // use
892    public Bitmap getErrorBitmap(IImage image) {
893        if (ImageManager.isImage(image)) {
894            if (mMissingImageThumbnailBitmap == null) {
895                mMissingImageThumbnailBitmap = BitmapFactory.decodeResource(
896                        getResources(),
897                        R.drawable.ic_missing_thumbnail_picture);
898            }
899            return mMissingImageThumbnailBitmap;
900        } else {
901            if (mMissingVideoThumbnailBitmap == null) {
902                mMissingVideoThumbnailBitmap = BitmapFactory.decodeResource(
903                        getResources(), R.drawable.ic_missing_thumbnail_video);
904            }
905            return mMissingVideoThumbnailBitmap;
906        }
907    }
908
909    private Animation mFooterAppear;
910    private Animation mFooterDisappear;
911
912    private void showFooter() {
913        mFooterOrganizeView.setVisibility(View.VISIBLE);
914        if (mFooterAppear == null) {
915            mFooterAppear = AnimationUtils.loadAnimation(
916                    this, R.anim.footer_appear);
917        }
918        mFooterOrganizeView.startAnimation(mFooterAppear);
919    }
920
921    private void hideFooter() {
922        if (mFooterOrganizeView.getVisibility() != View.GONE) {
923            mFooterOrganizeView.setVisibility(View.GONE);
924            if (mFooterDisappear == null) {
925                mFooterDisappear = AnimationUtils.loadAnimation(
926                        this, R.anim.footer_disappear);
927            }
928            mFooterOrganizeView.startAnimation(mFooterDisappear);
929        }
930    }
931
932    private String getShareMultipleMimeType() {
933        final int FLAG_IMAGE = 1, FLAG_VIDEO = 2;
934        int flag = 0;
935        for (IImage image : mMultiSelected) {
936            flag |= ImageManager.isImage(image) ? FLAG_IMAGE : FLAG_VIDEO;
937        }
938        return flag == FLAG_IMAGE
939                ? "image/*"
940                : flag == FLAG_VIDEO ? "video/*" : "*/*";
941    }
942
943    private void onShareMultipleClicked() {
944        if (mMultiSelected == null) return;
945        if (mMultiSelected.size() > 1) {
946            Intent intent = new Intent();
947            intent.setAction(Intent.ACTION_SEND_MULTIPLE);
948
949            String mimeType = getShareMultipleMimeType();
950            intent.setType(mimeType);
951            ArrayList<Parcelable> list = new ArrayList<Parcelable>();
952            for (IImage image : mMultiSelected) {
953                list.add(image.fullSizeImageUri());
954            }
955            intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, list);
956            try {
957                startActivity(Intent.createChooser(
958                        intent, getText(R.string.send_media_files)));
959            } catch (android.content.ActivityNotFoundException ex) {
960                Toast.makeText(this, R.string.no_way_to_share,
961                        Toast.LENGTH_SHORT).show();
962            }
963        } else if (mMultiSelected.size() == 1) {
964            IImage image = mMultiSelected.iterator().next();
965            Intent intent = new Intent();
966            intent.setAction(Intent.ACTION_SEND);
967            String mimeType = image.getMimeType();
968            intent.setType(mimeType);
969            intent.putExtra(Intent.EXTRA_STREAM, image.fullSizeImageUri());
970            boolean isImage = ImageManager.isImage(image);
971            try {
972                startActivity(Intent.createChooser(intent, getText(
973                        isImage ? R.string.sendImage : R.string.sendVideo)));
974            } catch (android.content.ActivityNotFoundException ex) {
975                Toast.makeText(this, isImage
976                        ? R.string.no_way_to_share_image
977                        : R.string.no_way_to_share_video,
978                        Toast.LENGTH_SHORT).show();
979            }
980        }
981    }
982
983    private void onDeleteMultipleClicked() {
984        if (mMultiSelected == null) return;
985        Runnable action = new Runnable() {
986            public void run() {
987                ArrayList<Uri> uriList = new ArrayList<Uri>();
988                for (IImage image : mMultiSelected) {
989                    uriList.add(image.fullSizeImageUri());
990                }
991                closeMultiSelectMode();
992                Intent intent = new Intent(ImageGallery.this,
993                        DeleteImage.class);
994                intent.putExtra("delete-uris", uriList);
995                try {
996                    startActivity(intent);
997                } catch (ActivityNotFoundException ex) {
998                    Log.e(TAG, "Delete images fail", ex);
999                }
1000            }
1001        };
1002        MenuHelper.deleteMultiple(this, action);
1003    }
1004
1005    private boolean isInMultiSelectMode() {
1006        return mMultiSelected != null;
1007    }
1008
1009    private void closeMultiSelectMode() {
1010        if (mMultiSelected == null) return;
1011        mMultiSelected = null;
1012        mGvs.invalidate();
1013        hideFooter();
1014    }
1015
1016    private void openMultiSelectMode() {
1017        if (mMultiSelected != null) return;
1018        mMultiSelected = new HashSet<IImage>();
1019        mGvs.invalidate();
1020    }
1021
1022}
1023