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