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 
17 package com.android.camera;
18 
19 import com.android.gallery.R;
20 
21 import android.app.Activity;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.SharedPreferences;
25 import android.graphics.Bitmap;
26 import android.net.Uri;
27 import android.os.Bundle;
28 import android.preference.PreferenceManager;
29 import android.provider.MediaStore;
30 import android.util.AttributeSet;
31 import android.util.Log;
32 import android.view.GestureDetector;
33 import android.view.KeyEvent;
34 import android.view.Menu;
35 import android.view.MenuItem;
36 import android.view.MotionEvent;
37 import android.view.View;
38 import android.view.Window;
39 import android.view.WindowManager;
40 import android.view.View.OnTouchListener;
41 import android.view.animation.AlphaAnimation;
42 import android.view.animation.Animation;
43 import android.view.animation.AnimationUtils;
44 import android.widget.Toast;
45 import android.widget.ZoomButtonsController;
46 
47 import com.android.camera.gallery.IImage;
48 import com.android.camera.gallery.IImageList;
49 import com.android.camera.gallery.VideoObject;
50 
51 import 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.
58 public 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 
updateNextPrevControls()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
onSingleTapUp(MotionEvent e)341         public boolean onSingleTapUp(MotionEvent e) {
342             if (mPaused) return false;
343             setMode(MODE_NORMAL);
344             return true;
345         }
346 
347         @Override
onSingleTapConfirmed(MotionEvent e)348         public boolean onSingleTapConfirmed(MotionEvent e) {
349             if (mPaused) return false;
350             showOnScreenControls();
351             scheduleDismissOnScreenControls();
352             return true;
353         }
354 
355         @Override
onDoubleTap(MotionEvent e)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 
isPickIntent()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
onCreateOptionsMenu(Menu menu)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
onPrepareOptionsMenu(Menu menu)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
onMenuItemSelected(int featureId, MenuItem item)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 
setImage(int pos, boolean showControls)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
onCreate(Bundle instanceState)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 
updateActionIcons()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 
makeInAnimation(int id)667     private Animation makeInAnimation(int id) {
668         Animation inAnimation = AnimationUtils.loadAnimation(this, id);
669         return inAnimation;
670     }
671 
makeOutAnimation(int id)672     private Animation makeOutAnimation(int id) {
673         Animation outAnimation = AnimationUtils.loadAnimation(this, id);
674         return outAnimation;
675     }
676 
getPreferencesInteger( SharedPreferences prefs, String key, int defaultValue)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 
setMode(int mode)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 
generateShuffleOrder()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 
loadNextImage(final int requestedPos, final long delay, final boolean firstCall)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 
makeGetter()877     private void makeGetter() {
878         mGetter = new ImageGetter(getContentResolver());
879     }
880 
buildImageListFromUri(Uri uri)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 
init(Uri uri)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 
getCurrentUri()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
onSaveInstanceState(Bundle b)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
onStart()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
onStop()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 
startShareMediaActivity(IImage image)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 
startPlayVideoActivity()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 
onClick(View v)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 
moveNextOrPrevious(int delta)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
onActivityResult(int requestCode, int resultCode, Intent data)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 
1084 class 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).
1193 class 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