ViewImage.java revision d6c2fb7a38fcdb58742fcfffd67a4594487ec71c
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.content.Context;
23import android.content.Intent;
24import android.content.SharedPreferences;
25import android.graphics.Bitmap;
26import android.net.Uri;
27import android.os.Bundle;
28import android.preference.PreferenceManager;
29import android.provider.MediaStore;
30import android.util.AttributeSet;
31import android.util.Log;
32import android.view.GestureDetector;
33import android.view.KeyEvent;
34import android.view.Menu;
35import android.view.MenuItem;
36import android.view.MotionEvent;
37import android.view.View;
38import android.view.Window;
39import android.view.WindowManager;
40import android.view.View.OnTouchListener;
41import android.view.animation.AlphaAnimation;
42import android.view.animation.Animation;
43import android.view.animation.AnimationUtils;
44import android.widget.Toast;
45import android.widget.ZoomButtonsController;
46
47import com.android.camera.gallery.IImage;
48import com.android.camera.gallery.IImageList;
49import com.android.camera.gallery.VideoObject;
50
51import java.util.Random;
52
53// This activity can display a whole picture and navigate them in a specific
54// gallery. It has two modes: normal mode and slide show mode. In normal mode
55// the user view one image at a time, and can click "previous" and "next"
56// button to see the previous or next image. In slide show mode it shows one
57// image after another, with some transition effect.
58public class ViewImage extends Activity implements View.OnClickListener {
59    private static final String PREF_SLIDESHOW_REPEAT =
60            "pref_gallery_slideshow_repeat_key";
61    private static final String PREF_SHUFFLE_SLIDESHOW =
62            "pref_gallery_slideshow_shuffle_key";
63    private static final String STATE_URI = "uri";
64    private static final String STATE_SLIDESHOW = "slideshow";
65    private static final String EXTRA_SLIDESHOW = "slideshow";
66    private static final String TAG = "ViewImage";
67
68    private ImageGetter mGetter;
69    private Uri mSavedUri;
70    boolean mPaused = true;
71    private boolean mShowControls = true;
72
73    // Choices for what adjacents to load.
74    private static final int[] sOrderAdjacents = new int[] {0, 1, -1};
75    private static final int[] sOrderSlideshow = new int[] {0};
76
77    final GetterHandler mHandler = new GetterHandler();
78
79    private final Random mRandom = new Random(System.currentTimeMillis());
80    private int [] mShuffleOrder = null;
81    private boolean mUseShuffleOrder = false;
82    private boolean mSlideShowLoop = false;
83
84    static final int MODE_NORMAL = 1;
85    static final int MODE_SLIDESHOW = 2;
86    private int mMode = MODE_NORMAL;
87
88    private boolean mFullScreenInNormalMode;
89    private boolean mShowActionIcons;
90    private View mActionIconPanel;
91
92    private int mSlideShowInterval;
93    private int mLastSlideShowImage;
94    int mCurrentPosition = 0;
95
96    // represents which style animation to use
97    private int mAnimationIndex;
98    private Animation [] mSlideShowInAnimation;
99    private Animation [] mSlideShowOutAnimation;
100
101    private SharedPreferences mPrefs;
102
103    private View mNextImageView;
104    private View mPrevImageView;
105    private final Animation mHideNextImageViewAnimation =
106            new AlphaAnimation(1F, 0F);
107    private final Animation mHidePrevImageViewAnimation =
108            new AlphaAnimation(1F, 0F);
109    private final Animation mShowNextImageViewAnimation =
110            new AlphaAnimation(0F, 1F);
111    private final Animation mShowPrevImageViewAnimation =
112            new AlphaAnimation(0F, 1F);
113
114    public static final String KEY_IMAGE_LIST = "image_list";
115    private static final String STATE_SHOW_CONTROLS = "show_controls";
116
117    IImageList mAllImages;
118
119    private ImageManager.ImageListParam mParam;
120
121    private int mSlideShowImageCurrent = 0;
122    private final ImageViewTouchBase [] mSlideShowImageViews =
123            new ImageViewTouchBase[2];
124
125    GestureDetector mGestureDetector;
126    private ZoomButtonsController mZoomButtonsController;
127
128    // The image view displayed for normal mode.
129    private ImageViewTouch mImageView;
130    // This is the cache for thumbnail bitmaps.
131    private BitmapCache mCache;
132    private MenuHelper.MenuItemsResult mImageMenuRunnable;
133    private final Runnable mDismissOnScreenControlRunner = new Runnable() {
134        public void run() {
135            hideOnScreenControls();
136        }
137    };
138
139    private void updateNextPrevControls() {
140        boolean showPrev = mCurrentPosition > 0;
141        boolean showNext = mCurrentPosition < mAllImages.getCount() - 1;
142
143        boolean prevIsVisible = mPrevImageView.getVisibility() == View.VISIBLE;
144        boolean nextIsVisible = mNextImageView.getVisibility() == View.VISIBLE;
145
146        if (showPrev && !prevIsVisible) {
147            Animation a = mShowPrevImageViewAnimation;
148            a.setDuration(500);
149            mPrevImageView.startAnimation(a);
150            mPrevImageView.setVisibility(View.VISIBLE);
151        } else if (!showPrev && prevIsVisible) {
152            Animation a = mHidePrevImageViewAnimation;
153            a.setDuration(500);
154            mPrevImageView.startAnimation(a);
155            mPrevImageView.setVisibility(View.GONE);
156        }
157
158        if (showNext && !nextIsVisible) {
159            Animation a = mShowNextImageViewAnimation;
160            a.setDuration(500);
161            mNextImageView.startAnimation(a);
162            mNextImageView.setVisibility(View.VISIBLE);
163        } else if (!showNext && nextIsVisible) {
164            Animation a = mHideNextImageViewAnimation;
165            a.setDuration(500);
166            mNextImageView.startAnimation(a);
167            mNextImageView.setVisibility(View.GONE);
168        }
169    }
170
171    private void hideOnScreenControls() {
172        if (mShowActionIcons
173                && mActionIconPanel.getVisibility() == View.VISIBLE) {
174            Animation animation = new AlphaAnimation(1, 0);
175            animation.setDuration(500);
176            mActionIconPanel.startAnimation(animation);
177            mActionIconPanel.setVisibility(View.INVISIBLE);
178        }
179
180        if (mNextImageView.getVisibility() == View.VISIBLE) {
181            Animation a = mHideNextImageViewAnimation;
182            a.setDuration(500);
183            mNextImageView.startAnimation(a);
184            mNextImageView.setVisibility(View.INVISIBLE);
185        }
186
187        if (mPrevImageView.getVisibility() == View.VISIBLE) {
188            Animation a = mHidePrevImageViewAnimation;
189            a.setDuration(500);
190            mPrevImageView.startAnimation(a);
191            mPrevImageView.setVisibility(View.INVISIBLE);
192        }
193
194        mZoomButtonsController.setVisible(false);
195    }
196
197    private void showOnScreenControls() {
198        if (mPaused) return;
199        // If the view has not been attached to the window yet, the
200        // zoomButtonControls will not able to show up. So delay it until the
201        // view has attached to window.
202        if (mActionIconPanel.getWindowToken() == null) {
203            mHandler.postGetterCallback(new Runnable() {
204                public void run() {
205                    showOnScreenControls();
206                }
207            });
208            return;
209        }
210        updateNextPrevControls();
211
212        IImage image = mAllImages.getImageAt(mCurrentPosition);
213        if (image instanceof VideoObject) {
214            mZoomButtonsController.setVisible(false);
215        } else {
216            updateZoomButtonsEnabled();
217            mZoomButtonsController.setVisible(true);
218        }
219
220        if (mShowActionIcons
221                && mActionIconPanel.getVisibility() != View.VISIBLE) {
222            Animation animation = new AlphaAnimation(0, 1);
223            animation.setDuration(500);
224            mActionIconPanel.startAnimation(animation);
225            mActionIconPanel.setVisibility(View.VISIBLE);
226        }
227    }
228
229    @Override
230    public boolean dispatchTouchEvent(MotionEvent m) {
231        if (mPaused) return true;
232        if (mZoomButtonsController.isVisible()) {
233            scheduleDismissOnScreenControls();
234        }
235        return super.dispatchTouchEvent(m);
236    }
237
238    private void updateZoomButtonsEnabled() {
239        ImageViewTouch imageView = mImageView;
240        float scale = imageView.getScale();
241        mZoomButtonsController.setZoomInEnabled(scale < imageView.mMaxZoom);
242        mZoomButtonsController.setZoomOutEnabled(scale > 1);
243    }
244
245    @Override
246    protected void onDestroy() {
247        // This is necessary to make the ZoomButtonsController unregister
248        // its configuration change receiver.
249        if (mZoomButtonsController != null) {
250            mZoomButtonsController.setVisible(false);
251        }
252        super.onDestroy();
253    }
254
255    private void scheduleDismissOnScreenControls() {
256        mHandler.removeCallbacks(mDismissOnScreenControlRunner);
257        mHandler.postDelayed(mDismissOnScreenControlRunner, 2000);
258    }
259
260    private void setupOnScreenControls(View rootView, View ownerView) {
261        mNextImageView = rootView.findViewById(R.id.next_image);
262        mPrevImageView = rootView.findViewById(R.id.prev_image);
263
264        mNextImageView.setOnClickListener(this);
265        mPrevImageView.setOnClickListener(this);
266
267        setupZoomButtonController(ownerView);
268        setupOnTouchListeners(rootView);
269    }
270
271    private void setupZoomButtonController(final View ownerView) {
272        mZoomButtonsController = new ZoomButtonsController(ownerView);
273        mZoomButtonsController.setAutoDismissed(false);
274        mZoomButtonsController.setZoomSpeed(100);
275        mZoomButtonsController.setOnZoomListener(
276                new ZoomButtonsController.OnZoomListener() {
277            public void onVisibilityChanged(boolean visible) {
278                if (visible) {
279                    updateZoomButtonsEnabled();
280                }
281            }
282
283            public void onZoom(boolean zoomIn) {
284                if (zoomIn) {
285                    mImageView.zoomIn();
286                } else {
287                    mImageView.zoomOut();
288                }
289                mZoomButtonsController.setVisible(true);
290                updateZoomButtonsEnabled();
291            }
292        });
293    }
294
295    private void setupOnTouchListeners(View rootView) {
296        mGestureDetector = new GestureDetector(this, new MyGestureListener());
297
298        // If the user touches anywhere on the panel (including the
299        // next/prev button). We show the on-screen controls. In addition
300        // to that, if the touch is not on the prev/next button, we
301        // pass the event to the gesture detector to detect double tap.
302        final OnTouchListener buttonListener = new OnTouchListener() {
303            public boolean onTouch(View v, MotionEvent event) {
304                scheduleDismissOnScreenControls();
305                return false;
306            }
307        };
308
309        OnTouchListener rootListener = new OnTouchListener() {
310            public boolean onTouch(View v, MotionEvent event) {
311                buttonListener.onTouch(v, event);
312                mGestureDetector.onTouchEvent(event);
313
314                // We do not use the return value of
315                // mGestureDetector.onTouchEvent because we will not receive
316                // the "up" event if we return false for the "down" event.
317                return true;
318            }
319        };
320
321        mNextImageView.setOnTouchListener(buttonListener);
322        mPrevImageView.setOnTouchListener(buttonListener);
323        rootView.setOnTouchListener(rootListener);
324    }
325
326    private class MyGestureListener extends
327            GestureDetector.SimpleOnGestureListener {
328
329        @Override
330        public boolean onScroll(MotionEvent e1, MotionEvent e2,
331                float distanceX, float distanceY) {
332            ImageViewTouch imageView = mImageView;
333            if (imageView.getScale() > 1F) {
334                imageView.postTranslateCenter(-distanceX, -distanceY);
335            }
336            return true;
337        }
338
339        @Override
340        public boolean onSingleTapUp(MotionEvent e) {
341            setMode(MODE_NORMAL);
342            return true;
343        }
344
345        @Override
346        public boolean onSingleTapConfirmed(MotionEvent e) {
347            showOnScreenControls();
348            scheduleDismissOnScreenControls();
349            return true;
350        }
351
352        @Override
353        public boolean onDoubleTap(MotionEvent e) {
354            ImageViewTouch imageView = mImageView;
355
356            // Switch between the original scale and 3x scale.
357            if (imageView.getScale() > 2F) {
358                mImageView.zoomTo(1f);
359            } else {
360                mImageView.zoomToPoint(3f, e.getX(), e.getY());
361            }
362            return true;
363        }
364    }
365
366    boolean isPickIntent() {
367        String action = getIntent().getAction();
368        return (Intent.ACTION_PICK.equals(action)
369                || Intent.ACTION_GET_CONTENT.equals(action));
370    }
371
372    @Override
373    public boolean onCreateOptionsMenu(Menu menu) {
374        super.onCreateOptionsMenu(menu);
375
376        MenuItem item = menu.add(Menu.NONE, Menu.NONE,
377                MenuHelper.POSITION_SLIDESHOW,
378                R.string.slide_show);
379        item.setOnMenuItemClickListener(
380                new MenuItem.OnMenuItemClickListener() {
381            public boolean onMenuItemClick(MenuItem item) {
382                setMode(MODE_SLIDESHOW);
383                mLastSlideShowImage = mCurrentPosition;
384                loadNextImage(mCurrentPosition, 0, true);
385                return true;
386            }
387        });
388        item.setIcon(android.R.drawable.ic_menu_slideshow);
389
390        mImageMenuRunnable = MenuHelper.addImageMenuItems(
391                menu,
392                MenuHelper.INCLUDE_ALL,
393                ViewImage.this,
394                mHandler,
395                mDeletePhotoRunnable,
396                new MenuHelper.MenuInvoker() {
397                    public void run(final MenuHelper.MenuCallback cb) {
398                        if (mPaused) return;
399                        setMode(MODE_NORMAL);
400
401                        IImage image = mAllImages.getImageAt(mCurrentPosition);
402                        Uri uri = image.fullSizeImageUri();
403                        cb.run(uri, image);
404
405                        mImageView.clear();
406                        setImage(mCurrentPosition, false);
407                    }
408                });
409
410        item = menu.add(Menu.NONE, Menu.NONE,
411                MenuHelper.POSITION_GALLERY_SETTING, R.string.camerasettings);
412        item.setOnMenuItemClickListener(
413                new MenuItem.OnMenuItemClickListener() {
414            public boolean onMenuItemClick(MenuItem item) {
415                Intent preferences = new Intent();
416                preferences.setClass(ViewImage.this, GallerySettings.class);
417                startActivity(preferences);
418                return true;
419            }
420        });
421        item.setAlphabeticShortcut('p');
422        item.setIcon(android.R.drawable.ic_menu_preferences);
423
424        return true;
425    }
426
427    protected Runnable mDeletePhotoRunnable = new Runnable() {
428        public void run() {
429            mAllImages.removeImageAt(mCurrentPosition);
430            if (mAllImages.getCount() == 0) {
431                finish();
432                return;
433            } else {
434                if (mCurrentPosition == mAllImages.getCount()) {
435                    mCurrentPosition -= 1;
436                }
437            }
438            mImageView.clear();
439            mCache.clear();  // Because the position number is changed.
440            setImage(mCurrentPosition, true);
441        }
442    };
443
444    @Override
445    public boolean onPrepareOptionsMenu(Menu menu) {
446
447        super.onPrepareOptionsMenu(menu);
448        if (mPaused) return false;
449
450        setMode(MODE_NORMAL);
451        IImage image = mAllImages.getImageAt(mCurrentPosition);
452
453        if (mImageMenuRunnable != null) {
454            mImageMenuRunnable.gettingReadyToOpen(menu, image);
455        }
456
457        Uri uri = mAllImages.getImageAt(mCurrentPosition).fullSizeImageUri();
458        MenuHelper.enableShareMenuItem(menu, MenuHelper.isWhiteListUri(uri));
459
460        MenuHelper.enableShowOnMapMenuItem(menu, MenuHelper.hasLatLngData(image));
461
462        return true;
463    }
464
465    @Override
466    public boolean onMenuItemSelected(int featureId, MenuItem item) {
467        boolean b = super.onMenuItemSelected(featureId, item);
468        if (mImageMenuRunnable != null) {
469            mImageMenuRunnable.aboutToCall(item,
470                    mAllImages.getImageAt(mCurrentPosition));
471        }
472        return b;
473    }
474
475    void setImage(int pos, boolean showControls) {
476        mCurrentPosition = pos;
477
478        Bitmap b = mCache.getBitmap(pos);
479        if (b != null) {
480            IImage image = mAllImages.getImageAt(pos);
481            mImageView.setImageRotateBitmapResetBase(
482                    new RotateBitmap(b, image.getDegreesRotated()), true);
483            updateZoomButtonsEnabled();
484        }
485
486        ImageGetterCallback cb = new ImageGetterCallback() {
487            public void completed() {
488            }
489
490            public boolean wantsThumbnail(int pos, int offset) {
491                return !mCache.hasBitmap(pos + offset);
492            }
493
494            public boolean wantsFullImage(int pos, int offset) {
495                return offset == 0;
496            }
497
498            public int fullImageSizeToUse(int pos, int offset) {
499                // this number should be bigger so that we can zoom.  we may
500                // need to get fancier and read in the fuller size image as the
501                // user starts to zoom.
502                // Originally the value is set to 480 in order to avoid OOM.
503                // Now we set it to 2048 because of using
504                // native memory allocation for Bitmaps.
505                final int imageViewSize = 2048;
506                return imageViewSize;
507            }
508
509            public int [] loadOrder() {
510                return sOrderAdjacents;
511            }
512
513            public void imageLoaded(int pos, int offset, RotateBitmap bitmap,
514                                    boolean isThumb) {
515                // shouldn't get here after onPause()
516
517                // We may get a result from a previous request. Ignore it.
518                if (pos != mCurrentPosition) {
519                    bitmap.recycle();
520                    return;
521                }
522
523                if (isThumb) {
524                    mCache.put(pos + offset, bitmap.getBitmap());
525                }
526                if (offset == 0) {
527                    // isThumb: We always load thumb bitmap first, so we will
528                    // reset the supp matrix for then thumb bitmap, and keep
529                    // the supp matrix when the full bitmap is loaded.
530                    mImageView.setImageRotateBitmapResetBase(bitmap, isThumb);
531                    updateZoomButtonsEnabled();
532                }
533            }
534        };
535
536        // Could be null if we're stopping a slide show in the course of pausing
537        if (mGetter != null) {
538            mGetter.setPosition(pos, cb, mAllImages, mHandler);
539        }
540        updateActionIcons();
541        if (showControls) showOnScreenControls();
542        scheduleDismissOnScreenControls();
543    }
544
545    @Override
546    public void onCreate(Bundle instanceState) {
547        super.onCreate(instanceState);
548
549        Intent intent = getIntent();
550        mFullScreenInNormalMode = intent.getBooleanExtra(
551                MediaStore.EXTRA_FULL_SCREEN, true);
552        mShowActionIcons = intent.getBooleanExtra(
553                MediaStore.EXTRA_SHOW_ACTION_ICONS, true);
554
555        mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
556
557        setDefaultKeyMode(DEFAULT_KEYS_SHORTCUT);
558        requestWindowFeature(Window.FEATURE_NO_TITLE);
559        setContentView(R.layout.viewimage);
560
561        mImageView = (ImageViewTouch) findViewById(R.id.image);
562        mImageView.setEnableTrackballScroll(true);
563        mCache = new BitmapCache(3);
564        mImageView.setRecycler(mCache);
565
566        makeGetter();
567
568        mAnimationIndex = -1;
569
570        mSlideShowInAnimation = new Animation[] {
571            makeInAnimation(R.anim.transition_in),
572            makeInAnimation(R.anim.slide_in),
573            makeInAnimation(R.anim.slide_in_vertical),
574        };
575
576        mSlideShowOutAnimation = new Animation[] {
577            makeOutAnimation(R.anim.transition_out),
578            makeOutAnimation(R.anim.slide_out),
579            makeOutAnimation(R.anim.slide_out_vertical),
580        };
581
582        mSlideShowImageViews[0] =
583                (ImageViewTouchBase) findViewById(R.id.image1_slideShow);
584        mSlideShowImageViews[1] =
585                (ImageViewTouchBase) findViewById(R.id.image2_slideShow);
586        for (ImageViewTouchBase v : mSlideShowImageViews) {
587            v.setVisibility(View.INVISIBLE);
588            v.setRecycler(mCache);
589        }
590
591        mActionIconPanel = findViewById(R.id.action_icon_panel);
592
593        mParam = getIntent().getParcelableExtra(KEY_IMAGE_LIST);
594
595        boolean slideshow;
596        if (instanceState != null) {
597            mSavedUri = instanceState.getParcelable(STATE_URI);
598            slideshow = instanceState.getBoolean(STATE_SLIDESHOW, false);
599            mShowControls = instanceState.getBoolean(STATE_SHOW_CONTROLS, true);
600        } else {
601            mSavedUri = getIntent().getData();
602            slideshow = intent.getBooleanExtra(EXTRA_SLIDESHOW, false);
603        }
604
605        // We only show action icons for URIs that we know we can share and
606        // delete. Although we get read permission (for the images) from
607        // applications like MMS, we cannot pass the permission to other
608        // activities due to the current framework design.
609        if (!MenuHelper.isWhiteListUri(mSavedUri)) {
610            mShowActionIcons = false;
611        }
612
613        if (mShowActionIcons) {
614            int[] pickIds = {R.id.attach, R.id.cancel};
615            int[] normalIds = {R.id.setas, R.id.play, R.id.share, R.id.discard};
616            int[] connectIds = isPickIntent() ? pickIds : normalIds;
617            for (int id : connectIds) {
618                View view = mActionIconPanel.findViewById(id);
619                view.setVisibility(View.VISIBLE);
620                view.setOnClickListener(this);
621            }
622        }
623
624        // Don't show the "delete" icon for SingleImageList.
625        if (ImageManager.isSingleImageMode(mSavedUri.toString())) {
626            mActionIconPanel.findViewById(R.id.discard)
627                    .setVisibility(View.GONE);
628        }
629
630        if (slideshow) {
631            setMode(MODE_SLIDESHOW);
632        } else {
633            if (mFullScreenInNormalMode) {
634                getWindow().addFlags(
635                        WindowManager.LayoutParams.FLAG_FULLSCREEN);
636            }
637            if (mShowActionIcons) {
638                mActionIconPanel.setVisibility(View.VISIBLE);
639            }
640        }
641
642        setupOnScreenControls(findViewById(R.id.rootLayout), mImageView);
643    }
644
645    private void updateActionIcons() {
646        if (isPickIntent()) return;
647
648        IImage image = mAllImages.getImageAt(mCurrentPosition);
649        View panel = mActionIconPanel;
650        if (image instanceof VideoObject) {
651            panel.findViewById(R.id.setas).setVisibility(View.GONE);
652            panel.findViewById(R.id.play).setVisibility(View.VISIBLE);
653        } else {
654            panel.findViewById(R.id.setas).setVisibility(View.VISIBLE);
655            panel.findViewById(R.id.play).setVisibility(View.GONE);
656        }
657    }
658
659    private Animation makeInAnimation(int id) {
660        Animation inAnimation = AnimationUtils.loadAnimation(this, id);
661        return inAnimation;
662    }
663
664    private Animation makeOutAnimation(int id) {
665        Animation outAnimation = AnimationUtils.loadAnimation(this, id);
666        return outAnimation;
667    }
668
669    private static int getPreferencesInteger(
670            SharedPreferences prefs, String key, int defaultValue) {
671        String value = prefs.getString(key, null);
672        try {
673            return value == null ? defaultValue : Integer.parseInt(value);
674        } catch (NumberFormatException ex) {
675            Log.e(TAG, "couldn't parse preference: " + value, ex);
676            return defaultValue;
677        }
678    }
679
680    void setMode(int mode) {
681        if (mMode == mode) {
682            return;
683        }
684        View slideshowPanel = findViewById(R.id.slideShowContainer);
685        View normalPanel = findViewById(R.id.abs);
686
687        Window win = getWindow();
688        mMode = mode;
689        if (mode == MODE_SLIDESHOW) {
690            slideshowPanel.setVisibility(View.VISIBLE);
691            normalPanel.setVisibility(View.GONE);
692
693            win.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN
694                    | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
695
696            mImageView.clear();
697            mActionIconPanel.setVisibility(View.GONE);
698
699            slideshowPanel.getRootView().requestLayout();
700
701            // The preferences we want to read:
702            //   mUseShuffleOrder
703            //   mSlideShowLoop
704            //   mAnimationIndex
705            //   mSlideShowInterval
706
707            mUseShuffleOrder = mPrefs.getBoolean(PREF_SHUFFLE_SLIDESHOW, false);
708            mSlideShowLoop = mPrefs.getBoolean(PREF_SLIDESHOW_REPEAT, false);
709            mAnimationIndex = getPreferencesInteger(
710                    mPrefs, "pref_gallery_slideshow_transition_key", 0);
711            mSlideShowInterval = getPreferencesInteger(
712                    mPrefs, "pref_gallery_slideshow_interval_key", 3) * 1000;
713        } else {
714            slideshowPanel.setVisibility(View.GONE);
715            normalPanel.setVisibility(View.VISIBLE);
716
717            win.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
718            if (mFullScreenInNormalMode) {
719                win.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
720            } else {
721                win.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
722            }
723
724            if (mGetter != null) {
725                mGetter.cancelCurrent();
726            }
727
728            if (mShowActionIcons) {
729                Animation animation = new AlphaAnimation(0F, 1F);
730                animation.setDuration(500);
731                mActionIconPanel.setAnimation(animation);
732                mActionIconPanel.setVisibility(View.VISIBLE);
733            }
734
735            ImageViewTouchBase dst = mImageView;
736            dst.mLastXTouchPos = -1;
737            dst.mLastYTouchPos = -1;
738
739            for (ImageViewTouchBase ivt : mSlideShowImageViews) {
740                ivt.clear();
741            }
742
743            mShuffleOrder = null;
744
745            // mGetter null is a proxy for being paused
746            if (mGetter != null) {
747                setImage(mCurrentPosition, true);
748            }
749        }
750    }
751
752    private void generateShuffleOrder() {
753        if (mShuffleOrder == null
754                || mShuffleOrder.length != mAllImages.getCount()) {
755            mShuffleOrder = new int[mAllImages.getCount()];
756            for (int i = 0, n = mShuffleOrder.length; i < n; i++) {
757                mShuffleOrder[i] = i;
758            }
759        }
760
761        for (int i = mShuffleOrder.length - 1; i >= 0; i--) {
762            int r = mRandom.nextInt(i + 1);
763            if (r != i) {
764                int tmp = mShuffleOrder[r];
765                mShuffleOrder[r] = mShuffleOrder[i];
766                mShuffleOrder[i] = tmp;
767            }
768        }
769    }
770
771    private void loadNextImage(final int requestedPos, final long delay,
772                               final boolean firstCall) {
773        if (firstCall && mUseShuffleOrder) {
774            generateShuffleOrder();
775        }
776
777        final long targetDisplayTime = System.currentTimeMillis() + delay;
778
779        ImageGetterCallback cb = new ImageGetterCallback() {
780            public void completed() {
781            }
782
783            public boolean wantsThumbnail(int pos, int offset) {
784                return true;
785            }
786
787            public boolean wantsFullImage(int pos, int offset) {
788                return false;
789            }
790
791            public int [] loadOrder() {
792                return sOrderSlideshow;
793            }
794
795            public int fullImageSizeToUse(int pos, int offset) {
796                return 480; // TODO compute this
797            }
798
799            public void imageLoaded(final int pos, final int offset,
800                    final RotateBitmap bitmap, final boolean isThumb) {
801                long timeRemaining = Math.max(0,
802                        targetDisplayTime - System.currentTimeMillis());
803                mHandler.postDelayedGetterCallback(new Runnable() {
804                    public void run() {
805                        if (mMode == MODE_NORMAL) {
806                            return;
807                        }
808
809                        ImageViewTouchBase oldView =
810                                mSlideShowImageViews[mSlideShowImageCurrent];
811
812                        if (++mSlideShowImageCurrent
813                                == mSlideShowImageViews.length) {
814                            mSlideShowImageCurrent = 0;
815                        }
816
817                        ImageViewTouchBase newView =
818                                mSlideShowImageViews[mSlideShowImageCurrent];
819                        newView.setVisibility(View.VISIBLE);
820                        newView.setImageRotateBitmapResetBase(bitmap, true);
821                        newView.bringToFront();
822
823                        int animation = 0;
824
825                        if (mAnimationIndex == -1) {
826                            int n = mRandom.nextInt(
827                                    mSlideShowInAnimation.length);
828                            animation = n;
829                        } else {
830                            animation = mAnimationIndex;
831                        }
832
833                        Animation aIn = mSlideShowInAnimation[animation];
834                        newView.startAnimation(aIn);
835                        newView.setVisibility(View.VISIBLE);
836
837                        Animation aOut = mSlideShowOutAnimation[animation];
838                        oldView.setVisibility(View.INVISIBLE);
839                        oldView.startAnimation(aOut);
840
841                        mCurrentPosition = requestedPos;
842
843                        if (mCurrentPosition == mLastSlideShowImage
844                                && !firstCall) {
845                            if (mSlideShowLoop) {
846                                if (mUseShuffleOrder) {
847                                    generateShuffleOrder();
848                                }
849                            } else {
850                                setMode(MODE_NORMAL);
851                                return;
852                            }
853                        }
854
855                        loadNextImage(
856                                (mCurrentPosition + 1) % mAllImages.getCount(),
857                                mSlideShowInterval, false);
858                    }
859                }, timeRemaining);
860            }
861        };
862        // Could be null if we're stopping a slide show in the course of pausing
863        if (mGetter != null) {
864            int pos = requestedPos;
865            if (mShuffleOrder != null) {
866                pos = mShuffleOrder[pos];
867            }
868            mGetter.setPosition(pos, cb, mAllImages, mHandler);
869        }
870    }
871
872    private void makeGetter() {
873        mGetter = new ImageGetter(getContentResolver());
874    }
875
876    private IImageList buildImageListFromUri(Uri uri) {
877        String sortOrder = mPrefs.getString(
878                "pref_gallery_sort_key", "descending");
879        int sort = sortOrder.equals("ascending")
880                ? ImageManager.SORT_ASCENDING
881                : ImageManager.SORT_DESCENDING;
882        return ImageManager.makeImageList(getContentResolver(), uri, sort);
883    }
884
885    private boolean init(Uri uri) {
886        if (uri == null) return false;
887        mAllImages = (mParam == null)
888                ? buildImageListFromUri(uri)
889                : ImageManager.makeImageList(getContentResolver(), mParam);
890        IImage image = mAllImages.getImageForUri(uri);
891        if (image == null) return false;
892        mCurrentPosition = mAllImages.getImageIndex(image);
893        mLastSlideShowImage = mCurrentPosition;
894        return true;
895    }
896
897    private Uri getCurrentUri() {
898        if (mAllImages.getCount() == 0) return null;
899        IImage image = mAllImages.getImageAt(mCurrentPosition);
900        return image.fullSizeImageUri();
901    }
902
903    @Override
904    public void onSaveInstanceState(Bundle b) {
905        super.onSaveInstanceState(b);
906        b.putParcelable(STATE_URI,
907                mAllImages.getImageAt(mCurrentPosition).fullSizeImageUri());
908        b.putBoolean(STATE_SLIDESHOW, mMode == MODE_SLIDESHOW);
909    }
910
911    @Override
912    public void onStart() {
913        super.onStart();
914        mPaused = false;
915
916        if (!init(mSavedUri)) {
917            Log.w(TAG, "init failed: " + mSavedUri);
918            finish();
919            return;
920        }
921
922        // normally this will never be zero but if one "backs" into this
923        // activity after removing the sdcard it could be zero.  in that
924        // case just "finish" since there's nothing useful that can happen.
925        int count = mAllImages.getCount();
926        if (count == 0) {
927            finish();
928            return;
929        } else if (count <= mCurrentPosition) {
930            mCurrentPosition = count - 1;
931        }
932
933        if (mGetter == null) {
934            makeGetter();
935        }
936
937        if (mMode == MODE_SLIDESHOW) {
938            loadNextImage(mCurrentPosition, 0, true);
939        } else {  // MODE_NORMAL
940            setImage(mCurrentPosition, mShowControls);
941            mShowControls = false;
942        }
943    }
944
945    @Override
946    public void onStop() {
947        super.onStop();
948        mPaused = true;
949
950        // mGetter could be null if we call finish() and leave early in
951        // onStart().
952        if (mGetter != null) {
953            mGetter.cancelCurrent();
954            mGetter.stop();
955            mGetter = null;
956        }
957        setMode(MODE_NORMAL);
958
959        // removing all callback in the message queue
960        mHandler.removeAllGetterCallbacks();
961
962        if (mAllImages != null) {
963            mSavedUri = getCurrentUri();
964            mAllImages.close();
965            mAllImages = null;
966        }
967
968        hideOnScreenControls();
969        mImageView.clear();
970        mCache.clear();
971
972        for (ImageViewTouchBase iv : mSlideShowImageViews) {
973            iv.clear();
974        }
975    }
976
977    private void startShareMediaActivity(IImage image) {
978        boolean isVideo = image instanceof VideoObject;
979        Intent intent = new Intent();
980        intent.setAction(Intent.ACTION_SEND);
981        intent.setType(image.getMimeType());
982        intent.putExtra(Intent.EXTRA_STREAM, image.fullSizeImageUri());
983        try {
984            startActivity(Intent.createChooser(intent, getText(
985                    isVideo ? R.string.sendVideo : R.string.sendImage)));
986        } catch (android.content.ActivityNotFoundException ex) {
987            Toast.makeText(this, isVideo
988                    ? R.string.no_way_to_share_image
989                    : R.string.no_way_to_share_video,
990                    Toast.LENGTH_SHORT).show();
991        }
992    }
993
994    private void startPlayVideoActivity() {
995        IImage image = mAllImages.getImageAt(mCurrentPosition);
996        Intent intent = new Intent(
997                Intent.ACTION_VIEW, image.fullSizeImageUri());
998        try {
999            startActivity(intent);
1000        } catch (android.content.ActivityNotFoundException ex) {
1001            Log.e(TAG, "Couldn't view video " + image.fullSizeImageUri(), ex);
1002        }
1003    }
1004
1005    public void onClick(View v) {
1006        switch (v.getId()) {
1007            case R.id.discard:
1008                MenuHelper.deletePhoto(this, mDeletePhotoRunnable);
1009                break;
1010            case R.id.play:
1011                startPlayVideoActivity();
1012                break;
1013            case R.id.share: {
1014                IImage image = mAllImages.getImageAt(mCurrentPosition);
1015                if (!MenuHelper.isWhiteListUri(image.fullSizeImageUri())) {
1016                    return;
1017                }
1018                startShareMediaActivity(image);
1019                break;
1020            }
1021            case R.id.setas: {
1022                IImage image = mAllImages.getImageAt(mCurrentPosition);
1023                Intent intent = Util.createSetAsIntent(image);
1024                try {
1025                    startActivity(Intent.createChooser(
1026                            intent, getText(R.string.setImage)));
1027                } catch (android.content.ActivityNotFoundException ex) {
1028                    Toast.makeText(this, R.string.no_way_to_share_video,
1029                            Toast.LENGTH_SHORT).show();
1030                }
1031                break;
1032            }
1033            case R.id.next_image:
1034                moveNextOrPrevious(1);
1035                break;
1036            case R.id.prev_image:
1037                moveNextOrPrevious(-1);
1038                break;
1039        }
1040    }
1041
1042    private void moveNextOrPrevious(int delta) {
1043        int nextImagePos = mCurrentPosition + delta;
1044        if ((0 <= nextImagePos) && (nextImagePos < mAllImages.getCount())) {
1045            setImage(nextImagePos, true);
1046            showOnScreenControls();
1047        }
1048    }
1049
1050    @Override
1051    protected void onActivityResult(int requestCode, int resultCode,
1052            Intent data) {
1053        switch (requestCode) {
1054            case MenuHelper.RESULT_COMMON_MENU_CROP:
1055                if (resultCode == RESULT_OK) {
1056                    // The CropImage activity passes back the Uri of the
1057                    // cropped image as the Action rather than the Data.
1058                    mSavedUri = Uri.parse(data.getAction());
1059
1060                    // if onStart() runs before, then set the returned
1061                    // image as currentImage.
1062                    if (mAllImages != null) {
1063                        IImage image = mAllImages.getImageForUri(mSavedUri);
1064                        // image could be null if SD card is removed.
1065                        if (image == null) {
1066                            finish();
1067                        } else {
1068                            mCurrentPosition = mAllImages.getImageIndex(image);
1069                            setImage(mCurrentPosition, false);
1070                        }
1071                    }
1072                }
1073                break;
1074        }
1075    }
1076}
1077
1078class ImageViewTouch extends ImageViewTouchBase {
1079    private final ViewImage mViewImage;
1080    private boolean mEnableTrackballScroll;
1081
1082    public ImageViewTouch(Context context) {
1083        super(context);
1084        mViewImage = (ViewImage) context;
1085    }
1086
1087    public ImageViewTouch(Context context, AttributeSet attrs) {
1088        super(context, attrs);
1089        mViewImage = (ViewImage) context;
1090    }
1091
1092    public void setEnableTrackballScroll(boolean enable) {
1093        mEnableTrackballScroll = enable;
1094    }
1095
1096    protected void postTranslateCenter(float dx, float dy) {
1097        super.postTranslate(dx, dy);
1098        center(true, true);
1099    }
1100
1101    private static final float PAN_RATE = 20;
1102
1103    // This is the time we allow the dpad to change the image position again.
1104    private long mNextChangePositionTime;
1105
1106    @Override
1107    public boolean onKeyDown(int keyCode, KeyEvent event) {
1108        if (mViewImage.mPaused) return false;
1109
1110        // Don't respond to arrow keys if trackball scrolling is not enabled
1111        if (!mEnableTrackballScroll) {
1112            if ((keyCode >= KeyEvent.KEYCODE_DPAD_UP)
1113                    && (keyCode <= KeyEvent.KEYCODE_DPAD_RIGHT)) {
1114                return super.onKeyDown(keyCode, event);
1115            }
1116        }
1117
1118        int current = mViewImage.mCurrentPosition;
1119
1120        int nextImagePos = -2; // default no next image
1121        try {
1122            switch (keyCode) {
1123                case KeyEvent.KEYCODE_DPAD_CENTER: {
1124                    if (mViewImage.isPickIntent()) {
1125                        IImage img = mViewImage.mAllImages
1126                                .getImageAt(mViewImage.mCurrentPosition);
1127                        mViewImage.setResult(ViewImage.RESULT_OK,
1128                                 new Intent().setData(img.fullSizeImageUri()));
1129                        mViewImage.finish();
1130                    }
1131                    break;
1132                }
1133                case KeyEvent.KEYCODE_DPAD_LEFT: {
1134                    if (getScale() <= 1F && event.getEventTime()
1135                            >= mNextChangePositionTime) {
1136                        nextImagePos = current - 1;
1137                        mNextChangePositionTime = event.getEventTime() + 500;
1138                    } else {
1139                        panBy(PAN_RATE, 0);
1140                        center(true, false);
1141                    }
1142                    return true;
1143                }
1144                case KeyEvent.KEYCODE_DPAD_RIGHT: {
1145                    if (getScale() <= 1F && event.getEventTime()
1146                            >= mNextChangePositionTime) {
1147                        nextImagePos = current + 1;
1148                        mNextChangePositionTime = event.getEventTime() + 500;
1149                    } else {
1150                        panBy(-PAN_RATE, 0);
1151                        center(true, false);
1152                    }
1153                    return true;
1154                }
1155                case KeyEvent.KEYCODE_DPAD_UP: {
1156                    panBy(0, PAN_RATE);
1157                    center(false, true);
1158                    return true;
1159                }
1160                case KeyEvent.KEYCODE_DPAD_DOWN: {
1161                    panBy(0, -PAN_RATE);
1162                    center(false, true);
1163                    return true;
1164                }
1165                case KeyEvent.KEYCODE_DEL:
1166                    MenuHelper.deletePhoto(
1167                            mViewImage, mViewImage.mDeletePhotoRunnable);
1168                    break;
1169            }
1170        } finally {
1171            if (nextImagePos >= 0
1172                    && nextImagePos < mViewImage.mAllImages.getCount()) {
1173                synchronized (mViewImage) {
1174                    mViewImage.setMode(ViewImage.MODE_NORMAL);
1175                    mViewImage.setImage(nextImagePos, true);
1176                }
1177           } else if (nextImagePos != -2) {
1178               center(true, true);
1179           }
1180        }
1181
1182        return super.onKeyDown(keyCode, event);
1183    }
1184}
1185
1186// This is a cache for Bitmap displayed in ViewImage (normal mode, thumb only).
1187class BitmapCache implements ImageViewTouchBase.Recycler {
1188    public static class Entry {
1189        int mPos;
1190        Bitmap mBitmap;
1191        public Entry() {
1192            clear();
1193        }
1194        public void clear() {
1195            mPos = -1;
1196            mBitmap = null;
1197        }
1198    }
1199
1200    private final Entry[] mCache;
1201
1202    public BitmapCache(int size) {
1203        mCache = new Entry[size];
1204        for (int i = 0; i < mCache.length; i++) {
1205            mCache[i] = new Entry();
1206        }
1207    }
1208
1209    // Given the position, find the associated entry. Returns null if there is
1210    // no such entry.
1211    private Entry findEntry(int pos) {
1212        for (Entry e : mCache) {
1213            if (pos == e.mPos) {
1214                return e;
1215            }
1216        }
1217        return null;
1218    }
1219
1220    // Returns the thumb bitmap if we have it, otherwise return null.
1221    public synchronized Bitmap getBitmap(int pos) {
1222        Entry e = findEntry(pos);
1223        if (e != null) {
1224            return e.mBitmap;
1225        }
1226        return null;
1227    }
1228
1229    public synchronized void put(int pos, Bitmap bitmap) {
1230        // First see if we already have this entry.
1231        if (findEntry(pos) != null) {
1232            return;
1233        }
1234
1235        // Find the best entry we should replace.
1236        // See if there is any empty entry.
1237        // Otherwise assuming sequential access, kick out the entry with the
1238        // greatest distance.
1239        Entry best = null;
1240        int maxDist = -1;
1241        for (Entry e : mCache) {
1242            if (e.mPos == -1) {
1243                best = e;
1244                break;
1245            } else {
1246                int dist = Math.abs(pos - e.mPos);
1247                if (dist > maxDist) {
1248                    maxDist = dist;
1249                    best = e;
1250                }
1251            }
1252        }
1253
1254        // Recycle the image being kicked out.
1255        // This only works because our current usage is sequential, so we
1256        // do not happen to recycle the image being displayed.
1257        if (best.mBitmap != null) {
1258            best.mBitmap.recycle();
1259        }
1260
1261        best.mPos = pos;
1262        best.mBitmap = bitmap;
1263    }
1264
1265    // Recycle all bitmaps in the cache and clear the cache.
1266    public synchronized void clear() {
1267        for (Entry e : mCache) {
1268            if (e.mBitmap != null) {
1269                e.mBitmap.recycle();
1270            }
1271            e.clear();
1272        }
1273    }
1274
1275    // Returns whether the bitmap is in the cache.
1276    public synchronized boolean hasBitmap(int pos) {
1277        Entry e = findEntry(pos);
1278        return (e != null);
1279    }
1280
1281    // Recycle the bitmap if it's not in the cache.
1282    // The input must be non-null.
1283    public synchronized void recycle(Bitmap b) {
1284        for (Entry e : mCache) {
1285            if (e.mPos != -1) {
1286                if (e.mBitmap == b) {
1287                    return;
1288                }
1289            }
1290        }
1291        b.recycle();
1292    }
1293}
1294