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    }
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