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 NoSearchActivity 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            if (mPaused) return false;
333            ImageViewTouch imageView = mImageView;
334            if (imageView.getScale() > 1F) {
335                imageView.postTranslateCenter(-distanceX, -distanceY);
336            }
337            return true;
338        }
339
340        @Override
341        public boolean onSingleTapUp(MotionEvent e) {
342            if (mPaused) return false;
343            setMode(MODE_NORMAL);
344            return true;
345        }
346
347        @Override
348        public boolean onSingleTapConfirmed(MotionEvent e) {
349            if (mPaused) return false;
350            showOnScreenControls();
351            scheduleDismissOnScreenControls();
352            return true;
353        }
354
355        @Override
356        public boolean onDoubleTap(MotionEvent e) {
357            if (mPaused) return false;
358            ImageViewTouch imageView = mImageView;
359
360            // Switch between the original scale and 3x scale.
361            if (imageView.getScale() > 2F) {
362                mImageView.zoomTo(1f);
363            } else {
364                mImageView.zoomToPoint(3f, e.getX(), e.getY());
365            }
366            return true;
367        }
368    }
369
370    boolean isPickIntent() {
371        String action = getIntent().getAction();
372        return (Intent.ACTION_PICK.equals(action)
373                || Intent.ACTION_GET_CONTENT.equals(action));
374    }
375
376    @Override
377    public boolean onCreateOptionsMenu(Menu menu) {
378        super.onCreateOptionsMenu(menu);
379
380        MenuItem item = menu.add(Menu.NONE, Menu.NONE,
381                MenuHelper.POSITION_SLIDESHOW,
382                R.string.slide_show);
383        item.setOnMenuItemClickListener(
384                new MenuItem.OnMenuItemClickListener() {
385            public boolean onMenuItemClick(MenuItem item) {
386                setMode(MODE_SLIDESHOW);
387                mLastSlideShowImage = mCurrentPosition;
388                loadNextImage(mCurrentPosition, 0, true);
389                return true;
390            }
391        });
392        item.setIcon(android.R.drawable.ic_menu_slideshow);
393
394        mImageMenuRunnable = MenuHelper.addImageMenuItems(
395                menu,
396                MenuHelper.INCLUDE_ALL,
397                ViewImage.this,
398                mHandler,
399                mDeletePhotoRunnable,
400                new MenuHelper.MenuInvoker() {
401                    public void run(final MenuHelper.MenuCallback cb) {
402                        if (mPaused) return;
403                        setMode(MODE_NORMAL);
404
405                        IImage image = mAllImages.getImageAt(mCurrentPosition);
406                        Uri uri = image.fullSizeImageUri();
407                        cb.run(uri, image);
408
409                        // We might have deleted all images in the callback, so
410                        // call setImage() only if we still have some images.
411                        if (mAllImages.getCount() > 0) {
412                            mImageView.clear();
413                            setImage(mCurrentPosition, false);
414                        }
415                    }
416                });
417
418        item = menu.add(Menu.NONE, Menu.NONE,
419                MenuHelper.POSITION_GALLERY_SETTING, R.string.camerasettings);
420        item.setOnMenuItemClickListener(
421                new MenuItem.OnMenuItemClickListener() {
422            public boolean onMenuItemClick(MenuItem item) {
423                Intent preferences = new Intent();
424                preferences.setClass(ViewImage.this, GallerySettings.class);
425                startActivity(preferences);
426                return true;
427            }
428        });
429        item.setAlphabeticShortcut('p');
430        item.setIcon(android.R.drawable.ic_menu_preferences);
431
432        return true;
433    }
434
435    protected Runnable mDeletePhotoRunnable = new Runnable() {
436        public void run() {
437            mAllImages.removeImageAt(mCurrentPosition);
438            if (mAllImages.getCount() == 0) {
439                finish();
440                return;
441            } else {
442                if (mCurrentPosition == mAllImages.getCount()) {
443                    mCurrentPosition -= 1;
444                }
445            }
446            mImageView.clear();
447            mCache.clear();  // Because the position number is changed.
448            setImage(mCurrentPosition, true);
449        }
450    };
451
452    @Override
453    public boolean onPrepareOptionsMenu(Menu menu) {
454
455        super.onPrepareOptionsMenu(menu);
456        if (mPaused) return false;
457
458        setMode(MODE_NORMAL);
459        IImage image = mAllImages.getImageAt(mCurrentPosition);
460
461        if (mImageMenuRunnable != null) {
462            mImageMenuRunnable.gettingReadyToOpen(menu, image);
463        }
464
465        Uri uri = mAllImages.getImageAt(mCurrentPosition).fullSizeImageUri();
466        MenuHelper.enableShareMenuItem(menu, MenuHelper.isWhiteListUri(uri));
467
468        MenuHelper.enableShowOnMapMenuItem(menu, MenuHelper.hasLatLngData(image));
469
470        return true;
471    }
472
473    @Override
474    public boolean onMenuItemSelected(int featureId, MenuItem item) {
475        boolean b = super.onMenuItemSelected(featureId, item);
476        if (mImageMenuRunnable != null) {
477            mImageMenuRunnable.aboutToCall(item,
478                    mAllImages.getImageAt(mCurrentPosition));
479        }
480        return b;
481    }
482
483    void setImage(int pos, boolean showControls) {
484        mCurrentPosition = pos;
485
486        Bitmap b = mCache.getBitmap(pos);
487        if (b != null) {
488            IImage image = mAllImages.getImageAt(pos);
489            mImageView.setImageRotateBitmapResetBase(
490                    new RotateBitmap(b, image.getDegreesRotated()), true);
491            updateZoomButtonsEnabled();
492        }
493
494        ImageGetterCallback cb = new ImageGetterCallback() {
495            public void completed() {
496            }
497
498            public boolean wantsThumbnail(int pos, int offset) {
499                return !mCache.hasBitmap(pos + offset);
500            }
501
502            public boolean wantsFullImage(int pos, int offset) {
503                return offset == 0;
504            }
505
506            public int fullImageSizeToUse(int pos, int offset) {
507                // this number should be bigger so that we can zoom.  we may
508                // need to get fancier and read in the fuller size image as the
509                // user starts to zoom.
510                // Originally the value is set to 480 in order to avoid OOM.
511                // Now we set it to 2048 because of using
512                // native memory allocation for Bitmaps.
513                final int imageViewSize = 2048;
514                return imageViewSize;
515            }
516
517            public int [] loadOrder() {
518                return sOrderAdjacents;
519            }
520
521            public void imageLoaded(int pos, int offset, RotateBitmap bitmap,
522                                    boolean isThumb) {
523                // shouldn't get here after onPause()
524
525                // We may get a result from a previous request. Ignore it.
526                if (pos != mCurrentPosition) {
527                    bitmap.recycle();
528                    return;
529                }
530
531                if (isThumb) {
532                    mCache.put(pos + offset, bitmap.getBitmap());
533                }
534                if (offset == 0) {
535                    // isThumb: We always load thumb bitmap first, so we will
536                    // reset the supp matrix for then thumb bitmap, and keep
537                    // the supp matrix when the full bitmap is loaded.
538                    mImageView.setImageRotateBitmapResetBase(bitmap, isThumb);
539                    updateZoomButtonsEnabled();
540                }
541            }
542        };
543
544        // Could be null if we're stopping a slide show in the course of pausing
545        if (mGetter != null) {
546            mGetter.setPosition(pos, cb, mAllImages, mHandler);
547        }
548        updateActionIcons();
549        if (showControls) showOnScreenControls();
550        scheduleDismissOnScreenControls();
551    }
552
553    @Override
554    public void onCreate(Bundle instanceState) {
555        super.onCreate(instanceState);
556
557        Intent intent = getIntent();
558        mFullScreenInNormalMode = intent.getBooleanExtra(
559                MediaStore.EXTRA_FULL_SCREEN, true);
560        mShowActionIcons = intent.getBooleanExtra(
561                MediaStore.EXTRA_SHOW_ACTION_ICONS, true);
562
563        mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
564
565        setDefaultKeyMode(DEFAULT_KEYS_SHORTCUT);
566        requestWindowFeature(Window.FEATURE_NO_TITLE);
567        setContentView(R.layout.viewimage);
568
569        mImageView = (ImageViewTouch) findViewById(R.id.image);
570        mImageView.setEnableTrackballScroll(true);
571        mCache = new BitmapCache(3);
572        mImageView.setRecycler(mCache);
573
574        makeGetter();
575
576        mAnimationIndex = -1;
577
578        mSlideShowInAnimation = new Animation[] {
579            makeInAnimation(R.anim.transition_in),
580            makeInAnimation(R.anim.slide_in),
581            makeInAnimation(R.anim.slide_in_vertical),
582        };
583
584        mSlideShowOutAnimation = new Animation[] {
585            makeOutAnimation(R.anim.transition_out),
586            makeOutAnimation(R.anim.slide_out),
587            makeOutAnimation(R.anim.slide_out_vertical),
588        };
589
590        mSlideShowImageViews[0] =
591                (ImageViewTouchBase) findViewById(R.id.image1_slideShow);
592        mSlideShowImageViews[1] =
593                (ImageViewTouchBase) findViewById(R.id.image2_slideShow);
594        for (ImageViewTouchBase v : mSlideShowImageViews) {
595            v.setVisibility(View.INVISIBLE);
596            v.setRecycler(mCache);
597        }
598
599        mActionIconPanel = findViewById(R.id.action_icon_panel);
600
601        mParam = getIntent().getParcelableExtra(KEY_IMAGE_LIST);
602
603        boolean slideshow;
604        if (instanceState != null) {
605            mSavedUri = instanceState.getParcelable(STATE_URI);
606            slideshow = instanceState.getBoolean(STATE_SLIDESHOW, false);
607            mShowControls = instanceState.getBoolean(STATE_SHOW_CONTROLS, true);
608        } else {
609            mSavedUri = getIntent().getData();
610            slideshow = intent.getBooleanExtra(EXTRA_SLIDESHOW, false);
611        }
612
613        // We only show action icons for URIs that we know we can share and
614        // delete. Although we get read permission (for the images) from
615        // applications like MMS, we cannot pass the permission to other
616        // activities due to the current framework design.
617        if (!MenuHelper.isWhiteListUri(mSavedUri)) {
618            mShowActionIcons = false;
619        }
620
621        if (mShowActionIcons) {
622            int[] pickIds = {R.id.attach, R.id.cancel};
623            int[] normalIds = {R.id.setas, R.id.play, R.id.share, R.id.discard};
624            int[] connectIds = isPickIntent() ? pickIds : normalIds;
625            for (int id : connectIds) {
626                View view = mActionIconPanel.findViewById(id);
627                view.setVisibility(View.VISIBLE);
628                view.setOnClickListener(this);
629            }
630        }
631
632        // Don't show the "delete" icon for SingleImageList.
633        if (ImageManager.isSingleImageMode(mSavedUri.toString())) {
634            mActionIconPanel.findViewById(R.id.discard)
635                    .setVisibility(View.GONE);
636        }
637
638        if (slideshow) {
639            setMode(MODE_SLIDESHOW);
640        } else {
641            if (mFullScreenInNormalMode) {
642                getWindow().addFlags(
643                        WindowManager.LayoutParams.FLAG_FULLSCREEN);
644            }
645            if (mShowActionIcons) {
646                mActionIconPanel.setVisibility(View.VISIBLE);
647            }
648        }
649
650        setupOnScreenControls(findViewById(R.id.rootLayout), mImageView);
651    }
652
653    private void updateActionIcons() {
654        if (isPickIntent()) return;
655
656        IImage image = mAllImages.getImageAt(mCurrentPosition);
657        View panel = mActionIconPanel;
658        if (image instanceof VideoObject) {
659            panel.findViewById(R.id.setas).setVisibility(View.GONE);
660            panel.findViewById(R.id.play).setVisibility(View.VISIBLE);
661        } else {
662            panel.findViewById(R.id.setas).setVisibility(View.VISIBLE);
663            panel.findViewById(R.id.play).setVisibility(View.GONE);
664        }
665    }
666
667    private Animation makeInAnimation(int id) {
668        Animation inAnimation = AnimationUtils.loadAnimation(this, id);
669        return inAnimation;
670    }
671
672    private Animation makeOutAnimation(int id) {
673        Animation outAnimation = AnimationUtils.loadAnimation(this, id);
674        return outAnimation;
675    }
676
677    private static int getPreferencesInteger(
678            SharedPreferences prefs, String key, int defaultValue) {
679        String value = prefs.getString(key, null);
680        try {
681            return value == null ? defaultValue : Integer.parseInt(value);
682        } catch (NumberFormatException ex) {
683            Log.e(TAG, "couldn't parse preference: " + value, ex);
684            return defaultValue;
685        }
686    }
687
688    void setMode(int mode) {
689        if (mMode == mode) {
690            return;
691        }
692        View slideshowPanel = findViewById(R.id.slideShowContainer);
693        View normalPanel = findViewById(R.id.abs);
694
695        Window win = getWindow();
696        mMode = mode;
697        if (mode == MODE_SLIDESHOW) {
698            slideshowPanel.setVisibility(View.VISIBLE);
699            normalPanel.setVisibility(View.GONE);
700
701            win.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN
702                    | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
703
704            mImageView.clear();
705            mActionIconPanel.setVisibility(View.GONE);
706
707            slideshowPanel.getRootView().requestLayout();
708
709            // The preferences we want to read:
710            //   mUseShuffleOrder
711            //   mSlideShowLoop
712            //   mAnimationIndex
713            //   mSlideShowInterval
714
715            mUseShuffleOrder = mPrefs.getBoolean(PREF_SHUFFLE_SLIDESHOW, false);
716            mSlideShowLoop = mPrefs.getBoolean(PREF_SLIDESHOW_REPEAT, false);
717            mAnimationIndex = getPreferencesInteger(
718                    mPrefs, "pref_gallery_slideshow_transition_key", 0);
719            mSlideShowInterval = getPreferencesInteger(
720                    mPrefs, "pref_gallery_slideshow_interval_key", 3) * 1000;
721        } else {
722            slideshowPanel.setVisibility(View.GONE);
723            normalPanel.setVisibility(View.VISIBLE);
724
725            win.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
726            if (mFullScreenInNormalMode) {
727                win.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
728            } else {
729                win.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
730            }
731
732            if (mGetter != null) {
733                mGetter.cancelCurrent();
734            }
735
736            if (mShowActionIcons) {
737                Animation animation = new AlphaAnimation(0F, 1F);
738                animation.setDuration(500);
739                mActionIconPanel.setAnimation(animation);
740                mActionIconPanel.setVisibility(View.VISIBLE);
741            }
742
743            ImageViewTouchBase dst = mImageView;
744            for (ImageViewTouchBase ivt : mSlideShowImageViews) {
745                ivt.clear();
746            }
747
748            mShuffleOrder = null;
749
750            // mGetter null is a proxy for being paused
751            if (mGetter != null) {
752                setImage(mCurrentPosition, true);
753            }
754        }
755    }
756
757    private void generateShuffleOrder() {
758        if (mShuffleOrder == null
759                || mShuffleOrder.length != mAllImages.getCount()) {
760            mShuffleOrder = new int[mAllImages.getCount()];
761            for (int i = 0, n = mShuffleOrder.length; i < n; i++) {
762                mShuffleOrder[i] = i;
763            }
764        }
765
766        for (int i = mShuffleOrder.length - 1; i >= 0; i--) {
767            int r = mRandom.nextInt(i + 1);
768            if (r != i) {
769                int tmp = mShuffleOrder[r];
770                mShuffleOrder[r] = mShuffleOrder[i];
771                mShuffleOrder[i] = tmp;
772            }
773        }
774    }
775
776    private void loadNextImage(final int requestedPos, final long delay,
777                               final boolean firstCall) {
778        if (firstCall && mUseShuffleOrder) {
779            generateShuffleOrder();
780        }
781
782        final long targetDisplayTime = System.currentTimeMillis() + delay;
783
784        ImageGetterCallback cb = new ImageGetterCallback() {
785            public void completed() {
786            }
787
788            public boolean wantsThumbnail(int pos, int offset) {
789                return true;
790            }
791
792            public boolean wantsFullImage(int pos, int offset) {
793                return false;
794            }
795
796            public int [] loadOrder() {
797                return sOrderSlideshow;
798            }
799
800            public int fullImageSizeToUse(int pos, int offset) {
801                return 480; // TODO compute this
802            }
803
804            public void imageLoaded(final int pos, final int offset,
805                    final RotateBitmap bitmap, final boolean isThumb) {
806                long timeRemaining = Math.max(0,
807                        targetDisplayTime - System.currentTimeMillis());
808                mHandler.postDelayedGetterCallback(new Runnable() {
809                    public void run() {
810                        if (mMode == MODE_NORMAL) {
811                            return;
812                        }
813
814                        ImageViewTouchBase oldView =
815                                mSlideShowImageViews[mSlideShowImageCurrent];
816
817                        if (++mSlideShowImageCurrent
818                                == mSlideShowImageViews.length) {
819                            mSlideShowImageCurrent = 0;
820                        }
821
822                        ImageViewTouchBase newView =
823                                mSlideShowImageViews[mSlideShowImageCurrent];
824                        newView.setVisibility(View.VISIBLE);
825                        newView.setImageRotateBitmapResetBase(bitmap, true);
826                        newView.bringToFront();
827
828                        int animation = 0;
829
830                        if (mAnimationIndex == -1) {
831                            int n = mRandom.nextInt(
832                                    mSlideShowInAnimation.length);
833                            animation = n;
834                        } else {
835                            animation = mAnimationIndex;
836                        }
837
838                        Animation aIn = mSlideShowInAnimation[animation];
839                        newView.startAnimation(aIn);
840                        newView.setVisibility(View.VISIBLE);
841
842                        Animation aOut = mSlideShowOutAnimation[animation];
843                        oldView.setVisibility(View.INVISIBLE);
844                        oldView.startAnimation(aOut);
845
846                        mCurrentPosition = requestedPos;
847
848                        if (mCurrentPosition == mLastSlideShowImage
849                                && !firstCall) {
850                            if (mSlideShowLoop) {
851                                if (mUseShuffleOrder) {
852                                    generateShuffleOrder();
853                                }
854                            } else {
855                                setMode(MODE_NORMAL);
856                                return;
857                            }
858                        }
859
860                        loadNextImage(
861                                (mCurrentPosition + 1) % mAllImages.getCount(),
862                                mSlideShowInterval, false);
863                    }
864                }, timeRemaining);
865            }
866        };
867        // Could be null if we're stopping a slide show in the course of pausing
868        if (mGetter != null) {
869            int pos = requestedPos;
870            if (mShuffleOrder != null) {
871                pos = mShuffleOrder[pos];
872            }
873            mGetter.setPosition(pos, cb, mAllImages, mHandler);
874        }
875    }
876
877    private void makeGetter() {
878        mGetter = new ImageGetter(getContentResolver());
879    }
880
881    private IImageList buildImageListFromUri(Uri uri) {
882        String sortOrder = mPrefs.getString(
883                "pref_gallery_sort_key", "descending");
884        int sort = sortOrder.equals("ascending")
885                ? ImageManager.SORT_ASCENDING
886                : ImageManager.SORT_DESCENDING;
887        return ImageManager.makeImageList(getContentResolver(), uri, sort);
888    }
889
890    private boolean init(Uri uri) {
891        if (uri == null) return false;
892        mAllImages = (mParam == null)
893                ? buildImageListFromUri(uri)
894                : ImageManager.makeImageList(getContentResolver(), mParam);
895        IImage image = mAllImages.getImageForUri(uri);
896        if (image == null) return false;
897        mCurrentPosition = mAllImages.getImageIndex(image);
898        mLastSlideShowImage = mCurrentPosition;
899        return true;
900    }
901
902    private Uri getCurrentUri() {
903        if (mAllImages.getCount() == 0) return null;
904        IImage image = mAllImages.getImageAt(mCurrentPosition);
905        if (image == null) return null;
906        return image.fullSizeImageUri();
907    }
908
909    @Override
910    public void onSaveInstanceState(Bundle b) {
911        super.onSaveInstanceState(b);
912        b.putParcelable(STATE_URI,
913                mAllImages.getImageAt(mCurrentPosition).fullSizeImageUri());
914        b.putBoolean(STATE_SLIDESHOW, mMode == MODE_SLIDESHOW);
915    }
916
917    @Override
918    public void onStart() {
919        super.onStart();
920        mPaused = false;
921
922        if (!init(mSavedUri)) {
923            Log.w(TAG, "init failed: " + mSavedUri);
924            finish();
925            return;
926        }
927
928        // normally this will never be zero but if one "backs" into this
929        // activity after removing the sdcard it could be zero.  in that
930        // case just "finish" since there's nothing useful that can happen.
931        int count = mAllImages.getCount();
932        if (count == 0) {
933            finish();
934            return;
935        } else if (count <= mCurrentPosition) {
936            mCurrentPosition = count - 1;
937        }
938
939        if (mGetter == null) {
940            makeGetter();
941        }
942
943        if (mMode == MODE_SLIDESHOW) {
944            loadNextImage(mCurrentPosition, 0, true);
945        } else {  // MODE_NORMAL
946            setImage(mCurrentPosition, mShowControls);
947            mShowControls = false;
948        }
949    }
950
951    @Override
952    public void onStop() {
953        super.onStop();
954        mPaused = true;
955
956        // mGetter could be null if we call finish() and leave early in
957        // onStart().
958        if (mGetter != null) {
959            mGetter.cancelCurrent();
960            mGetter.stop();
961            mGetter = null;
962        }
963        setMode(MODE_NORMAL);
964
965        // removing all callback in the message queue
966        mHandler.removeAllGetterCallbacks();
967
968        if (mAllImages != null) {
969            mSavedUri = getCurrentUri();
970            mAllImages.close();
971            mAllImages = null;
972        }
973
974        hideOnScreenControls();
975        mImageView.clear();
976        mCache.clear();
977
978        for (ImageViewTouchBase iv : mSlideShowImageViews) {
979            iv.clear();
980        }
981    }
982
983    private void startShareMediaActivity(IImage image) {
984        boolean isVideo = image instanceof VideoObject;
985        Intent intent = new Intent();
986        intent.setAction(Intent.ACTION_SEND);
987        intent.setType(image.getMimeType());
988        intent.putExtra(Intent.EXTRA_STREAM, image.fullSizeImageUri());
989        try {
990            startActivity(Intent.createChooser(intent, getText(
991                    isVideo ? R.string.sendVideo : R.string.sendImage)));
992        } catch (android.content.ActivityNotFoundException ex) {
993            Toast.makeText(this, isVideo
994                    ? R.string.no_way_to_share_image
995                    : R.string.no_way_to_share_video,
996                    Toast.LENGTH_SHORT).show();
997        }
998    }
999
1000    private void startPlayVideoActivity() {
1001        IImage image = mAllImages.getImageAt(mCurrentPosition);
1002        Intent intent = new Intent(
1003                Intent.ACTION_VIEW, image.fullSizeImageUri());
1004        try {
1005            startActivity(intent);
1006        } catch (android.content.ActivityNotFoundException ex) {
1007            Log.e(TAG, "Couldn't view video " + image.fullSizeImageUri(), ex);
1008        }
1009    }
1010
1011    public void onClick(View v) {
1012        switch (v.getId()) {
1013            case R.id.discard:
1014                MenuHelper.deletePhoto(this, mDeletePhotoRunnable);
1015                break;
1016            case R.id.play:
1017                startPlayVideoActivity();
1018                break;
1019            case R.id.share: {
1020                IImage image = mAllImages.getImageAt(mCurrentPosition);
1021                if (!MenuHelper.isWhiteListUri(image.fullSizeImageUri())) {
1022                    return;
1023                }
1024                startShareMediaActivity(image);
1025                break;
1026            }
1027            case R.id.setas: {
1028                IImage image = mAllImages.getImageAt(mCurrentPosition);
1029                Intent intent = Util.createSetAsIntent(image);
1030                try {
1031                    startActivity(Intent.createChooser(
1032                            intent, getText(R.string.setImage)));
1033                } catch (android.content.ActivityNotFoundException ex) {
1034                    Toast.makeText(this, R.string.no_way_to_share_video,
1035                            Toast.LENGTH_SHORT).show();
1036                }
1037                break;
1038            }
1039            case R.id.next_image:
1040                moveNextOrPrevious(1);
1041                break;
1042            case R.id.prev_image:
1043                moveNextOrPrevious(-1);
1044                break;
1045        }
1046    }
1047
1048    private void moveNextOrPrevious(int delta) {
1049        int nextImagePos = mCurrentPosition + delta;
1050        if ((0 <= nextImagePos) && (nextImagePos < mAllImages.getCount())) {
1051            setImage(nextImagePos, true);
1052            showOnScreenControls();
1053        }
1054    }
1055
1056    @Override
1057    protected void onActivityResult(int requestCode, int resultCode,
1058            Intent data) {
1059        switch (requestCode) {
1060            case MenuHelper.RESULT_COMMON_MENU_CROP:
1061                if (resultCode == RESULT_OK) {
1062                    // The CropImage activity passes back the Uri of the
1063                    // cropped image as the Action rather than the Data.
1064                    mSavedUri = Uri.parse(data.getAction());
1065
1066                    // if onStart() runs before, then set the returned
1067                    // image as currentImage.
1068                    if (mAllImages != null) {
1069                        IImage image = mAllImages.getImageForUri(mSavedUri);
1070                        // image could be null if SD card is removed.
1071                        if (image == null) {
1072                            finish();
1073                        } else {
1074                            mCurrentPosition = mAllImages.getImageIndex(image);
1075                            setImage(mCurrentPosition, false);
1076                        }
1077                    }
1078                }
1079                break;
1080        }
1081    }
1082}
1083
1084class ImageViewTouch extends ImageViewTouchBase {
1085    private final ViewImage mViewImage;
1086    private boolean mEnableTrackballScroll;
1087
1088    public ImageViewTouch(Context context) {
1089        super(context);
1090        mViewImage = (ViewImage) context;
1091    }
1092
1093    public ImageViewTouch(Context context, AttributeSet attrs) {
1094        super(context, attrs);
1095        mViewImage = (ViewImage) context;
1096    }
1097
1098    public void setEnableTrackballScroll(boolean enable) {
1099        mEnableTrackballScroll = enable;
1100    }
1101
1102    protected void postTranslateCenter(float dx, float dy) {
1103        super.postTranslate(dx, dy);
1104        center(true, true);
1105    }
1106
1107    private static final float PAN_RATE = 20;
1108
1109    // This is the time we allow the dpad to change the image position again.
1110    private long mNextChangePositionTime;
1111
1112    @Override
1113    public boolean onKeyDown(int keyCode, KeyEvent event) {
1114        if (mViewImage.mPaused) return false;
1115
1116        // Don't respond to arrow keys if trackball scrolling is not enabled
1117        if (!mEnableTrackballScroll) {
1118            if ((keyCode >= KeyEvent.KEYCODE_DPAD_UP)
1119                    && (keyCode <= KeyEvent.KEYCODE_DPAD_RIGHT)) {
1120                return super.onKeyDown(keyCode, event);
1121            }
1122        }
1123
1124        int current = mViewImage.mCurrentPosition;
1125
1126        int nextImagePos = -2; // default no next image
1127        try {
1128            switch (keyCode) {
1129                case KeyEvent.KEYCODE_DPAD_CENTER: {
1130                    if (mViewImage.isPickIntent()) {
1131                        IImage img = mViewImage.mAllImages
1132                                .getImageAt(mViewImage.mCurrentPosition);
1133                        mViewImage.setResult(ViewImage.RESULT_OK,
1134                                 new Intent().setData(img.fullSizeImageUri()));
1135                        mViewImage.finish();
1136                    }
1137                    break;
1138                }
1139                case KeyEvent.KEYCODE_DPAD_LEFT: {
1140                    if (getScale() <= 1F && event.getEventTime()
1141                            >= mNextChangePositionTime) {
1142                        nextImagePos = current - 1;
1143                        mNextChangePositionTime = event.getEventTime() + 500;
1144                    } else {
1145                        panBy(PAN_RATE, 0);
1146                        center(true, false);
1147                    }
1148                    return true;
1149                }
1150                case KeyEvent.KEYCODE_DPAD_RIGHT: {
1151                    if (getScale() <= 1F && event.getEventTime()
1152                            >= mNextChangePositionTime) {
1153                        nextImagePos = current + 1;
1154                        mNextChangePositionTime = event.getEventTime() + 500;
1155                    } else {
1156                        panBy(-PAN_RATE, 0);
1157                        center(true, false);
1158                    }
1159                    return true;
1160                }
1161                case KeyEvent.KEYCODE_DPAD_UP: {
1162                    panBy(0, PAN_RATE);
1163                    center(false, true);
1164                    return true;
1165                }
1166                case KeyEvent.KEYCODE_DPAD_DOWN: {
1167                    panBy(0, -PAN_RATE);
1168                    center(false, true);
1169                    return true;
1170                }
1171                case KeyEvent.KEYCODE_DEL:
1172                    MenuHelper.deletePhoto(
1173                            mViewImage, mViewImage.mDeletePhotoRunnable);
1174                    break;
1175            }
1176        } finally {
1177            if (nextImagePos >= 0
1178                    && nextImagePos < mViewImage.mAllImages.getCount()) {
1179                synchronized (mViewImage) {
1180                    mViewImage.setMode(ViewImage.MODE_NORMAL);
1181                    mViewImage.setImage(nextImagePos, true);
1182                }
1183           } else if (nextImagePos != -2) {
1184               center(true, true);
1185           }
1186        }
1187
1188        return super.onKeyDown(keyCode, event);
1189    }
1190}
1191
1192// This is a cache for Bitmap displayed in ViewImage (normal mode, thumb only).
1193class BitmapCache implements ImageViewTouchBase.Recycler {
1194    public static class Entry {
1195        int mPos;
1196        Bitmap mBitmap;
1197        public Entry() {
1198            clear();
1199        }
1200        public void clear() {
1201            mPos = -1;
1202            mBitmap = null;
1203        }
1204    }
1205
1206    private final Entry[] mCache;
1207
1208    public BitmapCache(int size) {
1209        mCache = new Entry[size];
1210        for (int i = 0; i < mCache.length; i++) {
1211            mCache[i] = new Entry();
1212        }
1213    }
1214
1215    // Given the position, find the associated entry. Returns null if there is
1216    // no such entry.
1217    private Entry findEntry(int pos) {
1218        for (Entry e : mCache) {
1219            if (pos == e.mPos) {
1220                return e;
1221            }
1222        }
1223        return null;
1224    }
1225
1226    // Returns the thumb bitmap if we have it, otherwise return null.
1227    public synchronized Bitmap getBitmap(int pos) {
1228        Entry e = findEntry(pos);
1229        if (e != null) {
1230            return e.mBitmap;
1231        }
1232        return null;
1233    }
1234
1235    public synchronized void put(int pos, Bitmap bitmap) {
1236        // First see if we already have this entry.
1237        if (findEntry(pos) != null) {
1238            return;
1239        }
1240
1241        // Find the best entry we should replace.
1242        // See if there is any empty entry.
1243        // Otherwise assuming sequential access, kick out the entry with the
1244        // greatest distance.
1245        Entry best = null;
1246        int maxDist = -1;
1247        for (Entry e : mCache) {
1248            if (e.mPos == -1) {
1249                best = e;
1250                break;
1251            } else {
1252                int dist = Math.abs(pos - e.mPos);
1253                if (dist > maxDist) {
1254                    maxDist = dist;
1255                    best = e;
1256                }
1257            }
1258        }
1259
1260        // Recycle the image being kicked out.
1261        // This only works because our current usage is sequential, so we
1262        // do not happen to recycle the image being displayed.
1263        if (best.mBitmap != null) {
1264            best.mBitmap.recycle();
1265        }
1266
1267        best.mPos = pos;
1268        best.mBitmap = bitmap;
1269    }
1270
1271    // Recycle all bitmaps in the cache and clear the cache.
1272    public synchronized void clear() {
1273        for (Entry e : mCache) {
1274            if (e.mBitmap != null) {
1275                e.mBitmap.recycle();
1276            }
1277            e.clear();
1278        }
1279    }
1280
1281    // Returns whether the bitmap is in the cache.
1282    public synchronized boolean hasBitmap(int pos) {
1283        Entry e = findEntry(pos);
1284        return (e != null);
1285    }
1286
1287    // Recycle the bitmap if it's not in the cache.
1288    // The input must be non-null.
1289    public synchronized void recycle(Bitmap b) {
1290        for (Entry e : mCache) {
1291            if (e.mPos != -1) {
1292                if (e.mBitmap == b) {
1293                    return;
1294                }
1295            }
1296        }
1297        b.recycle();
1298    }
1299}
1300