blob: 60619068b87b0c8704327cd7727636cc4214d5b4 [file] [log] [blame]
Wu-cheng Li46fc7ae2009-06-19 19:28:47 +08001/*
2 * Copyright (C) 2007 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.camera;
18
19import com.android.camera.gallery.Cancelable;
20import com.android.camera.gallery.IImage;
21import com.android.camera.gallery.IImageList;
22import com.android.camera.gallery.VideoObject;
23
24import android.app.Activity;
25import android.content.Context;
26import android.content.Intent;
27import android.content.SharedPreferences;
28import android.graphics.Bitmap;
29import android.graphics.Matrix;
30import android.net.Uri;
31import android.os.Bundle;
32import android.os.Handler;
33import android.os.Message;
34import android.preference.PreferenceManager;
35import android.provider.MediaStore;
36import android.util.AttributeSet;
37import android.util.Log;
38import android.view.GestureDetector;
39import android.view.KeyEvent;
40import android.view.Menu;
41import android.view.MenuItem;
42import android.view.MotionEvent;
43import android.view.View;
44import android.view.Window;
45import android.view.WindowManager;
46import android.view.animation.AlphaAnimation;
47import android.view.animation.Animation;
48import android.view.animation.AnimationUtils;
49import android.widget.Toast;
50import android.widget.ZoomButtonsController;
51
52import java.util.Random;
53import java.util.concurrent.CancellationException;
54import java.util.concurrent.ExecutionException;
55
56// This activity can display a whole picture and navigate them in a specific
57// gallery. It has two modes: normal mode and slide show mode. In normal mode
58// the user view one image at a time, and can click "previous" and "next"
59// button to see the previous or next image. In slide show mode it shows one
60// image after another, with some transition effect.
61public class ReviewImage extends Activity implements View.OnClickListener {
62 private static final String PREF_SLIDESHOW_REPEAT =
63 "pref_gallery_slideshow_repeat_key";
64 private static final String PREF_SHUFFLE_SLIDESHOW =
65 "pref_gallery_slideshow_shuffle_key";
66 private static final String STATE_URI = "uri";
67 private static final String STATE_SLIDESHOW = "slideshow";
68 private static final String EXTRA_SLIDESHOW = "slideshow";
69 private static final String TAG = "ReviewImage";
70
71 private static final boolean AUTO_DISMISS = true;
72 private static final boolean NO_AUTO_DISMISS = false;
73
74 private ReviewImageGetter mGetter;
75 private Uri mSavedUri;
76
77 // Choices for what adjacents to load.
78 private static final int[] sOrderAdjacents = new int[] {0, 1, -1};
79 private static final int[] sOrderSlideshow = new int[] {0};
80
81 final LocalHandler mHandler = new LocalHandler();
82
83 private final Random mRandom = new Random(System.currentTimeMillis());
84 private int [] mShuffleOrder = null;
85 private boolean mUseShuffleOrder = false;
86 private boolean mSlideShowLoop = false;
87
88 static final int MODE_NORMAL = 1;
89 static final int MODE_SLIDESHOW = 2;
90 private int mMode = MODE_NORMAL;
91
92 private boolean mFullScreenInNormalMode;
93
94 private int mSlideShowInterval;
95 private int mLastSlideShowImage;
96 int mCurrentPosition = 0;
97
98 // represents which style animation to use
99 private int mAnimationIndex;
100 private Animation [] mSlideShowInAnimation;
101 private Animation [] mSlideShowOutAnimation;
102
103 private SharedPreferences mPrefs;
104
105 private View mRootView;
106 private View mControlBar;
107 private View mNextImageView;
108 private View mPrevImageView;
109 private final Animation mHideNextImageViewAnimation = new AlphaAnimation(1F, 0F);
110 private final Animation mHidePrevImageViewAnimation = new AlphaAnimation(1F, 0F);
111 private final Animation mShowNextImageViewAnimation = new AlphaAnimation(0F, 1F);
112 private final Animation mShowPrevImageViewAnimation = new AlphaAnimation(0F, 1F);
113
114 static final int PADDING = 20;
115 static final int HYSTERESIS = PADDING * 2;
116 static final int BASE_SCROLL_DURATION = 1000; // ms
117
118 public static final String KEY_IMAGE_LIST = "image_list";
119
120 IImageList mAllImages;
121
122 private int mSlideShowImageCurrent = 0;
123 private final ImageViewTouchBase [] mSlideShowImageViews =
124 new ImageViewTouchBase[2];
125
126 GestureDetector mGestureDetector;
127 private ZoomButtonsController mZoomButtonsController;
128
129 // The image view displayed for normal mode.
130 private ImageViewTouch2 mImageView;
131 // This is the cache for thumbnail bitmaps.
132 private BitmapCache mCache;
133 private MenuHelper.MenuItemsResult mImageMenuRunnable;
134
135 private Runnable mDismissOnScreenControlsRunnable;
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 showOnScreenControls(final boolean autoDismiss) {
170 // If the view has not been attached to the window yet, the
171 // zoomButtonControls will not able to show up. So delay it until the
172 // view has attached to window.
173 if (mRootView.getWindowToken() == null) {
174 mHandler.postGetterCallback(new Runnable() {
175 public void run() {
176 showOnScreenControls(autoDismiss);
177 }
178 });
179 return;
180 }
181 mHandler.removeCallbacks(mDismissOnScreenControlsRunnable);
182 updateNextPrevControls();
183
184 IImage image = mAllImages.getImageAt(mCurrentPosition);
185 if (image instanceof VideoObject) {
186 mZoomButtonsController.setVisible(false);
187 } else {
188 updateZoomButtonsEnabled();
189 mZoomButtonsController.setVisible(true);
190 }
191 if (autoDismiss) scheduleDismissOnScreenControls();
192 }
193
194 @Override
195 public boolean dispatchTouchEvent(MotionEvent m) {
196 boolean sup = super.dispatchTouchEvent(m);
197
198 // This is a hack to show the on screen controls. We should make sure
199 // this event is not handled by others(ie. sup == false), and listen for
200 // the events on zoom/prev/next buttons.
201 // However, since we have no other pressable views, it is OK now.
202 // TODO: Fix the above issue.
203 if (mMode == MODE_NORMAL) {
204 switch (m.getAction()) {
205 case MotionEvent.ACTION_DOWN:
206 showOnScreenControls(NO_AUTO_DISMISS);
207 break;
208 case MotionEvent.ACTION_UP:
209 scheduleDismissOnScreenControls();
210 break;
211 }
212 }
213
214 if (sup == false) {
215 mGestureDetector.onTouchEvent(m);
216 return true;
217 }
218 return true;
219 }
220
221 private void scheduleDismissOnScreenControls() {
222 mHandler.removeCallbacks(mDismissOnScreenControlsRunnable);
223 mHandler.postDelayed(mDismissOnScreenControlsRunnable, 1500);
224 }
225
226 private void updateZoomButtonsEnabled() {
227 ImageViewTouch2 imageView = mImageView;
228 float scale = imageView.getScale();
229 mZoomButtonsController.setZoomInEnabled(scale < imageView.mMaxZoom);
230 mZoomButtonsController.setZoomOutEnabled(scale > 1);
231 }
232
233 @Override
234 protected void onDestroy() {
235
236 // This is necessary to make the ZoomButtonsController unregister
237 // its configuration change receiver.
238 if (mZoomButtonsController != null) {
239 mZoomButtonsController.setVisible(false);
240 }
241
242 super.onDestroy();
243 }
244
245 private void setupZoomButtonController(View rootView) {
246 mGestureDetector = new GestureDetector(this, new MyGestureListener());
247 mZoomButtonsController = new ZoomButtonsController(rootView);
248 mZoomButtonsController.setAutoDismissed(false);
Wu-cheng Li559899a2009-06-30 15:26:48 +0800249 mZoomButtonsController.setZoomSpeed(100);
Wu-cheng Li46fc7ae2009-06-19 19:28:47 +0800250 mZoomButtonsController.setOnZoomListener(
251 new ZoomButtonsController.OnZoomListener() {
252 public void onVisibilityChanged(boolean visible) {
253 if (visible) {
254 updateZoomButtonsEnabled();
255 }
256 }
257
258 public void onZoom(boolean zoomIn) {
259 if (zoomIn) {
260 mImageView.zoomIn();
261 } else {
262 mImageView.zoomOut();
263 }
264 updateZoomButtonsEnabled();
265 }
266 });
267 }
268
269 private class MyGestureListener extends
270 GestureDetector.SimpleOnGestureListener {
271
272 @Override
273 public boolean onScroll(MotionEvent e1, MotionEvent e2,
274 float distanceX, float distanceY) {
275 ImageViewTouch2 imageView = mImageView;
276 if (imageView.getScale() > 1F) {
277 imageView.postTranslateCenter(-distanceX, -distanceY);
278 }
279 return true;
280 }
281
282 @Override
283 public boolean onSingleTapUp(MotionEvent e) {
284 setMode(MODE_NORMAL);
285 return true;
286 }
287 }
288
289 private void setupDismissOnScreenControlRunnable() {
290 mDismissOnScreenControlsRunnable = new Runnable() {
291 public void run() {
292 if (mNextImageView.getVisibility() == View.VISIBLE) {
293 Animation a = mHideNextImageViewAnimation;
294 a.setDuration(500);
295 mNextImageView.startAnimation(a);
296 mNextImageView.setVisibility(View.INVISIBLE);
297 }
298
299 if (mPrevImageView.getVisibility() == View.VISIBLE) {
300 Animation a = mHidePrevImageViewAnimation;
301 a.setDuration(500);
302 mPrevImageView.startAnimation(a);
303 mPrevImageView.setVisibility(View.INVISIBLE);
304 }
305 mZoomButtonsController.setVisible(false);
306 }
307 };
308 }
309
310 boolean isPickIntent() {
311 String action = getIntent().getAction();
312 return (Intent.ACTION_PICK.equals(action)
313 || Intent.ACTION_GET_CONTENT.equals(action));
314 }
315
316 @Override
317 public boolean onCreateOptionsMenu(Menu menu) {
318 super.onCreateOptionsMenu(menu);
319
Wu-cheng Li46fc7ae2009-06-19 19:28:47 +0800320 mImageMenuRunnable = MenuHelper.addImageMenuItems(
321 menu,
322 MenuHelper.INCLUDE_ALL,
Wu-cheng Li46fc7ae2009-06-19 19:28:47 +0800323 ReviewImage.this,
324 mHandler,
325 mDeletePhotoRunnable,
326 new MenuHelper.MenuInvoker() {
327 public void run(final MenuHelper.MenuCallback cb) {
Chih-Chung Chang35f8af02009-07-01 14:52:24 +0800328 setMode(MODE_NORMAL);
329
330 IImage image = mAllImages.getImageAt(mCurrentPosition);
331 Uri uri = image.fullSizeImageUri();
332 cb.run(uri, image);
333
334 mImageView.clear();
335 setImage(mCurrentPosition);
Wu-cheng Li46fc7ae2009-06-19 19:28:47 +0800336 }
337 });
338
339 if (true) {
340 MenuItem item = menu.add(Menu.CATEGORY_SECONDARY, 203, 1000,
341 R.string.camerasettings);
342 item.setOnMenuItemClickListener(
343 new MenuItem.OnMenuItemClickListener() {
344 public boolean onMenuItemClick(MenuItem item) {
345 Intent preferences = new Intent();
346 preferences.setClass(ReviewImage.this, GallerySettings.class);
347 startActivity(preferences);
348 return true;
349 }
350 });
351 item.setAlphabeticShortcut('p');
352 item.setIcon(android.R.drawable.ic_menu_preferences);
353 }
354
355 // Hidden menu just so the shortcut will bring up the zoom controls
356 // the string resource is a placeholder
357 menu.add(Menu.CATEGORY_SECONDARY, 203, 0, R.string.camerasettings)
358 .setOnMenuItemClickListener(
359 new MenuItem.OnMenuItemClickListener() {
360 public boolean onMenuItemClick(MenuItem item) {
361 showOnScreenControls(AUTO_DISMISS);
362 return true;
363 }
364 })
365 .setAlphabeticShortcut('z')
366 .setVisible(false);
367
368 return true;
369 }
370
371 protected Runnable mDeletePhotoRunnable = new Runnable() {
372 public void run() {
373 mAllImages.removeImageAt(mCurrentPosition);
374 if (mAllImages.getCount() == 0) {
375 finish();
376 } else {
377 if (mCurrentPosition == mAllImages.getCount()) {
378 mCurrentPosition -= 1;
379 }
380 }
381 mImageView.clear();
382 mCache.clear(); // Because the position number is changed.
383 setImage(mCurrentPosition);
384 }
385 };
386
387 @Override
388 public boolean onPrepareOptionsMenu(Menu menu) {
389 super.onPrepareOptionsMenu(menu);
390 setMode(MODE_NORMAL);
391
392 if (mImageMenuRunnable != null) {
393 mImageMenuRunnable.gettingReadyToOpen(menu,
394 mAllImages.getImageAt(mCurrentPosition));
395 }
396
397 Uri uri = mAllImages.getImageAt(mCurrentPosition).fullSizeImageUri();
398 MenuHelper.enableShareMenuItem(menu, !MenuHelper.isMMSUri(uri));
399
400 return true;
401 }
402
403 @Override
404 public boolean onMenuItemSelected(int featureId, MenuItem item) {
405 boolean b = super.onMenuItemSelected(featureId, item);
406 if (mImageMenuRunnable != null) {
407 mImageMenuRunnable.aboutToCall(item,
408 mAllImages.getImageAt(mCurrentPosition));
409 }
410 return b;
411 }
412
413 void setImage(int pos) {
414 mCurrentPosition = pos;
415
416 Bitmap b = mCache.getBitmap(pos);
417 if (b != null) {
418 mImageView.setImageBitmapResetBase(b, true);
419 updateZoomButtonsEnabled();
420 }
421
422 ImageGetterCallback cb = new ImageGetterCallback() {
423 public void completed(boolean wasCanceled) {
424 mImageView.setFocusableInTouchMode(true);
425 mImageView.requestFocus();
426 }
427
428 public boolean wantsThumbnail(int pos, int offset) {
429 return !mCache.hasBitmap(pos + offset);
430 }
431
432 public boolean wantsFullImage(int pos, int offset) {
433 return offset == 0;
434 }
435
436 public int fullImageSizeToUse(int pos, int offset) {
437 // TODO
438 // this number should be bigger so that we can zoom. we may
439 // need to get fancier and read in the fuller size image as the
440 // user starts to zoom. use -1 to get the full full size image.
441 // for now use 480 so we don't run out of memory
442 final int imageViewSize = 480;
443 return imageViewSize;
444 }
445
446 public int [] loadOrder() {
447 return sOrderAdjacents;
448 }
449
450 public void imageLoaded(int pos, int offset, Bitmap bitmap,
451 boolean isThumb) {
452 // shouldn't get here after onPause()
453
454 // We may get a result from a previous request. Ignore it.
455 if (pos != mCurrentPosition) {
456 bitmap.recycle();
457 return;
458 }
459
460 if (isThumb) {
461 mCache.put(pos + offset, bitmap);
462 }
463 if (offset == 0) {
464 // isThumb: We always load thumb bitmap first, so we will
465 // reset the supp matrix for then thumb bitmap, and keep
466 // the supp matrix when the full bitmap is loaded.
467 mImageView.setImageBitmapResetBase(bitmap, isThumb);
468 updateZoomButtonsEnabled();
469 }
470 }
471 };
472
473 // Could be null if we're stopping a slide show in the course of pausing
474 if (mGetter != null) {
475 mGetter.setPosition(pos, cb);
476 }
477 updateActionIcons();
478 showOnScreenControls(AUTO_DISMISS);
479 }
480
481 @Override
482 public void onCreate(Bundle instanceState) {
483 super.onCreate(instanceState);
484
485 Intent intent = getIntent();
486 mFullScreenInNormalMode = intent.getBooleanExtra(
487 MediaStore.EXTRA_FULL_SCREEN, true);
488
489 mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
490
491 setDefaultKeyMode(DEFAULT_KEYS_SHORTCUT);
492 requestWindowFeature(Window.FEATURE_NO_TITLE);
493 setContentView(R.layout.review_image);
494
495 mRootView = findViewById(R.id.root);
496 mControlBar = findViewById(R.id.control_bar);
497 mImageView = (ImageViewTouch2) findViewById(R.id.image);
498 mImageView.setEnableTrackballScroll(true);
499 mCache = new BitmapCache(3);
500 mImageView.setRecycler(mCache);
501
502 makeGetter();
503
504 mAnimationIndex = -1;
505
506 mSlideShowInAnimation = new Animation[] {
507 makeInAnimation(R.anim.transition_in),
508 makeInAnimation(R.anim.slide_in),
509 makeInAnimation(R.anim.slide_in_vertical),
510 };
511
512 mSlideShowOutAnimation = new Animation[] {
513 makeOutAnimation(R.anim.transition_out),
514 makeOutAnimation(R.anim.slide_out),
515 makeOutAnimation(R.anim.slide_out_vertical),
516 };
517
518 mSlideShowImageViews[0] =
519 (ImageViewTouchBase) findViewById(R.id.image1_slideShow);
520 mSlideShowImageViews[1] =
521 (ImageViewTouchBase) findViewById(R.id.image2_slideShow);
522 for (ImageViewTouchBase v : mSlideShowImageViews) {
523 v.setVisibility(View.INVISIBLE);
524 v.setRecycler(mCache);
525 }
526
527 Uri uri = getIntent().getData();
528 IImageList imageList = getIntent().getParcelableExtra(KEY_IMAGE_LIST);
529 boolean slideshow = intent.getBooleanExtra(EXTRA_SLIDESHOW, false);
530
531 if (instanceState != null) {
532 uri = instanceState.getParcelable(STATE_URI);
533 slideshow = instanceState.getBoolean(STATE_SLIDESHOW, false);
534 }
535
536 if (!init(uri, imageList)) {
537 finish();
538 return;
539 }
540
541 int[] pickIds = {R.id.attach, R.id.cancel};
542 int[] reviewIds = {R.id.btn_delete, R.id.btn_share, R.id.btn_set_as, R.id.btn_play,
543 R.id.btn_done};
544 int[] connectIds = isPickIntent() ? pickIds : reviewIds;
545 for (int id : connectIds) {
546 View view = mControlBar.findViewById(id);
547 view.setOnClickListener(this);
548 // Set the LinearLayout of the given button to visible
549 ((View) view.getParent()).setVisibility(View.VISIBLE);
550 }
551
552 if (slideshow) {
553 setMode(MODE_SLIDESHOW);
554 } else {
555 if (mFullScreenInNormalMode) {
556 getWindow().addFlags(
557 WindowManager.LayoutParams.FLAG_FULLSCREEN);
558 }
559 }
560
561 setupZoomButtonController(findViewById(R.id.mainPanel));
562 setupDismissOnScreenControlRunnable();
563
564 mNextImageView = findViewById(R.id.next_image);
565 mPrevImageView = findViewById(R.id.prev_image);
566 mNextImageView.setOnClickListener(this);
567 mPrevImageView.setOnClickListener(this);
568
569 mNextImageView.setFocusable(true);
570 mPrevImageView.setFocusable(true);
571
572 }
573
574 private void setButtonPanelVisibility(int id, int visibility) {
575 View button = mControlBar.findViewById(id);
576 ((View) button.getParent()).setVisibility(visibility);
577 }
578
579 private void updateActionIcons() {
580 if (isPickIntent()) return;
581
582 IImage image = mAllImages.getImageAt(mCurrentPosition);
583 if (image instanceof VideoObject) {
584 setButtonPanelVisibility(R.id.btn_set_as, View.GONE);
585 setButtonPanelVisibility(R.id.btn_play, View.VISIBLE);
586 } else {
587 setButtonPanelVisibility(R.id.btn_set_as, View.VISIBLE);
588 setButtonPanelVisibility(R.id.btn_play, View.GONE);
589 }
590 }
591
592 private Animation makeInAnimation(int id) {
593 Animation inAnimation = AnimationUtils.loadAnimation(this, id);
594 return inAnimation;
595 }
596
597 private Animation makeOutAnimation(int id) {
598 Animation outAnimation = AnimationUtils.loadAnimation(this, id);
599 return outAnimation;
600 }
601
602 private static int getPreferencesInteger(
603 SharedPreferences prefs, String key, int defaultValue) {
604 String value = prefs.getString(key, null);
605 try {
606 return value == null ? defaultValue : Integer.parseInt(value);
607 } catch (NumberFormatException ex) {
608 Log.e(TAG, "couldn't parse preference: " + value, ex);
609 return defaultValue;
610 }
611 }
612
613 void setMode(int mode) {
614 if (mMode == mode) {
615 return;
616 }
617 View slideshowPanel = findViewById(R.id.slideShowContainer);
618 View normalPanel = findViewById(R.id.abs);
619
620 Window win = getWindow();
621 mMode = mode;
622 if (mode == MODE_SLIDESHOW) {
623 slideshowPanel.setVisibility(View.VISIBLE);
624 normalPanel.setVisibility(View.GONE);
625
626 win.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN
627 | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
628
629 mImageView.clear();
630
631 slideshowPanel.getRootView().requestLayout();
632
633 // The preferences we want to read:
634 // mUseShuffleOrder
635 // mSlideShowLoop
636 // mAnimationIndex
637 // mSlideShowInterval
638
639 mUseShuffleOrder = mPrefs.getBoolean(PREF_SHUFFLE_SLIDESHOW, false);
640 mSlideShowLoop = mPrefs.getBoolean(PREF_SLIDESHOW_REPEAT, false);
641 mAnimationIndex = getPreferencesInteger(
642 mPrefs, "pref_gallery_slideshow_transition_key", 0);
643 mSlideShowInterval = getPreferencesInteger(
644 mPrefs, "pref_gallery_slideshow_interval_key", 3) * 1000;
645 if (mUseShuffleOrder) {
646 generateShuffleOrder();
647 }
648 } else {
649 slideshowPanel.setVisibility(View.GONE);
650 normalPanel.setVisibility(View.VISIBLE);
651
652 win.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
653 if (mFullScreenInNormalMode) {
654 win.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
655 } else {
656 win.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
657 }
658
659 if (mGetter != null) {
660 mGetter.cancelCurrent();
661 }
662
663 ImageViewTouchBase dst = mImageView;
664 dst.mLastXTouchPos = -1;
665 dst.mLastYTouchPos = -1;
666
667 for (ImageViewTouchBase ivt : mSlideShowImageViews) {
668 ivt.clear();
669 }
670
671 mShuffleOrder = null;
672
673 // mGetter null is a proxy for being paused
674 if (mGetter != null) {
675 setImage(mCurrentPosition);
676 }
677 }
678 }
679
680 private void generateShuffleOrder() {
681 if (mShuffleOrder == null
682 || mShuffleOrder.length != mAllImages.getCount()) {
683 mShuffleOrder = new int[mAllImages.getCount()];
684 for (int i = 0, n = mShuffleOrder.length; i < n; i++) {
685 mShuffleOrder[i] = i;
686 }
687 }
688
689 for (int i = mShuffleOrder.length - 1; i >= 0; i--) {
690 int r = mRandom.nextInt(i + 1);
691 if (r != i) {
692 int tmp = mShuffleOrder[r];
693 mShuffleOrder[r] = mShuffleOrder[i];
694 mShuffleOrder[i] = tmp;
695 }
696 }
697 }
698
699 private void loadNextImage(final int requestedPos, final long delay,
700 final boolean firstCall) {
701 if (firstCall && mUseShuffleOrder) {
702 generateShuffleOrder();
703 }
704
705 final long targetDisplayTime = System.currentTimeMillis() + delay;
706
707 ImageGetterCallback cb = new ImageGetterCallback() {
708 public void completed(boolean wasCanceled) {
709 }
710
711 public boolean wantsThumbnail(int pos, int offset) {
712 return true;
713 }
714
715 public boolean wantsFullImage(int pos, int offset) {
716 return false;
717 }
718
719 public int [] loadOrder() {
720 return sOrderSlideshow;
721 }
722
723 public int fullImageSizeToUse(int pos, int offset) {
724 return 480; // TODO compute this
725 }
726
727 public void imageLoaded(final int pos, final int offset,
728 final Bitmap bitmap, final boolean isThumb) {
729 long timeRemaining = Math.max(0,
730 targetDisplayTime - System.currentTimeMillis());
731 mHandler.postDelayedGetterCallback(new Runnable() {
732 public void run() {
733 if (mMode == MODE_NORMAL) {
734 return;
735 }
736
737 ImageViewTouchBase oldView =
738 mSlideShowImageViews[mSlideShowImageCurrent];
739
740 if (++mSlideShowImageCurrent
741 == mSlideShowImageViews.length) {
742 mSlideShowImageCurrent = 0;
743 }
744
745 ImageViewTouchBase newView =
746 mSlideShowImageViews[mSlideShowImageCurrent];
747 newView.setVisibility(View.VISIBLE);
748 newView.setImageBitmapResetBase(bitmap, true);
749 newView.bringToFront();
750
751 int animation = 0;
752
753 if (mAnimationIndex == -1) {
754 int n = mRandom.nextInt(
755 mSlideShowInAnimation.length);
756 animation = n;
757 } else {
758 animation = mAnimationIndex;
759 }
760
761 Animation aIn = mSlideShowInAnimation[animation];
762 newView.startAnimation(aIn);
763 newView.setVisibility(View.VISIBLE);
764
765 Animation aOut = mSlideShowOutAnimation[animation];
766 oldView.setVisibility(View.INVISIBLE);
767 oldView.startAnimation(aOut);
768
769 mCurrentPosition = requestedPos;
770
771 if (mCurrentPosition == mLastSlideShowImage
772 && !firstCall) {
773 if (mSlideShowLoop) {
774 if (mUseShuffleOrder) {
775 generateShuffleOrder();
776 }
777 } else {
778 setMode(MODE_NORMAL);
779 return;
780 }
781 }
782
783 loadNextImage(
784 (mCurrentPosition + 1) % mAllImages.getCount(),
785 mSlideShowInterval, false);
786 }
787 }, timeRemaining);
788 }
789 };
790 // Could be null if we're stopping a slide show in the course of pausing
791 if (mGetter != null) {
792 int pos = requestedPos;
793 if (mShuffleOrder != null) {
794 pos = mShuffleOrder[pos];
795 }
796 mGetter.setPosition(pos, cb);
797 }
798 }
799
800 private void makeGetter() {
801 mGetter = new ReviewImageGetter(this);
802 }
803
804 private IImageList buildImageListFromUri(Uri uri) {
805 String sortOrder = mPrefs.getString(
806 "pref_gallery_sort_key", "descending");
807 int sort = ImageManager.SORT_ASCENDING;
808 return ImageManager.makeImageList(uri, getContentResolver(), sort);
809 }
810
811 private boolean init(Uri uri, IImageList imageList) {
812 if (uri == null) return false;
813 mAllImages = (imageList == null)
814 ? buildImageListFromUri(uri)
815 : imageList;
816 mAllImages.open(getContentResolver());
817 IImage image = mAllImages.getImageForUri(uri);
818 if (image == null) return false;
819 mCurrentPosition = mAllImages.getImageIndex(image);
820 mLastSlideShowImage = mCurrentPosition;
821 return true;
822 }
823
824 private Uri getCurrentUri() {
Owen Lin387833a2009-06-29 17:30:24 -0700825 if (mAllImages.getCount() == 0) return null;
Wu-cheng Li46fc7ae2009-06-19 19:28:47 +0800826 IImage image = mAllImages.getImageAt(mCurrentPosition);
827 return image.fullSizeImageUri();
828 }
829
830 @Override
831 public void onSaveInstanceState(Bundle b) {
832 super.onSaveInstanceState(b);
833 b.putParcelable(STATE_URI,
834 mAllImages.getImageAt(mCurrentPosition).fullSizeImageUri());
835 b.putBoolean(STATE_SLIDESHOW, mMode == MODE_SLIDESHOW);
836 }
837
838 @Override
839 public void onStart() {
840 super.onStart();
841
842 init(mSavedUri, mAllImages);
843
844 // normally this will never be zero but if one "backs" into this
845 // activity after removing the sdcard it could be zero. in that
846 // case just "finish" since there's nothing useful that can happen.
847 int count = mAllImages.getCount();
848 if (count == 0) {
849 finish();
850 } else if (count <= mCurrentPosition) {
851 mCurrentPosition = count - 1;
852 }
853
854 if (mGetter == null) {
855 makeGetter();
856 }
857
858 if (mMode == MODE_SLIDESHOW) {
859 loadNextImage(mCurrentPosition, 0, true);
860 } else { // MODE_NORMAL
861 setImage(mCurrentPosition);
862 }
863 }
864
865 @Override
866 public void onStop() {
867 super.onStop();
868
869 mGetter.cancelCurrent();
870 mGetter.stop();
871 mGetter = null;
872 setMode(MODE_NORMAL);
873
874 // removing all callback in the message queue
875 mHandler.removeAllGetterCallbacks();
876
877 mSavedUri = getCurrentUri();
878
879 mAllImages.deactivate();
880 mDismissOnScreenControlsRunnable.run();
881 if (mDismissOnScreenControlsRunnable != null) {
882 mHandler.removeCallbacks(mDismissOnScreenControlsRunnable);
883 }
884
885 mImageView.clear();
886 mCache.clear();
887
888 for (ImageViewTouchBase iv : mSlideShowImageViews) {
889 iv.clear();
890 }
891 }
892
893 private void startShareMediaActivity(IImage image) {
894 boolean isVideo = image instanceof VideoObject;
895 Intent intent = new Intent();
896 intent.setAction(Intent.ACTION_SEND);
897 intent.setType(image.getMimeType());
898 intent.putExtra(Intent.EXTRA_STREAM, image.fullSizeImageUri());
899 try {
900 startActivity(Intent.createChooser(intent, getText(
901 isVideo ? R.string.sendVideo : R.string.sendImage)));
902 } catch (android.content.ActivityNotFoundException ex) {
903 Toast.makeText(this, isVideo
904 ? R.string.no_way_to_share_image
905 : R.string.no_way_to_share_video,
906 Toast.LENGTH_SHORT).show();
907 }
908 }
909
910 private void startPlayVideoActivity() {
911 IImage image = mAllImages.getImageAt(mCurrentPosition);
912 Intent intent = new Intent(
913 Intent.ACTION_VIEW, image.fullSizeImageUri());
914 try {
915 startActivity(intent);
916 } catch (android.content.ActivityNotFoundException ex) {
917 Log.e(TAG, "Couldn't view video " + image.fullSizeImageUri(), ex);
918 }
919 }
920
921 public void onClick(View v) {
922 switch (v.getId()) {
923 case R.id.btn_delete:
repo sync4adbd032009-06-25 10:56:45 +0800924 MenuHelper.deleteImage(this, mDeletePhotoRunnable,
925 mAllImages.getImageAt(mCurrentPosition));
Wu-cheng Li46fc7ae2009-06-19 19:28:47 +0800926 break;
927 case R.id.btn_play:
928 startPlayVideoActivity();
929 break;
930 case R.id.btn_share: {
931 IImage image = mAllImages.getImageAt(mCurrentPosition);
932 if (MenuHelper.isMMSUri(image.fullSizeImageUri())) {
933 return;
934 }
935 startShareMediaActivity(image);
936 break;
937 }
938 case R.id.btn_set_as: {
Chih-Chung Changbb187782009-07-02 16:46:30 +0800939 IImage image = mAllImages.getImageAt(mCurrentPosition);
940 Intent intent = Util.createSetAsIntent(image);
Wu-cheng Li46fc7ae2009-06-19 19:28:47 +0800941 try {
942 startActivity(Intent.createChooser(
943 intent, getText(R.string.setImage)));
944 } catch (android.content.ActivityNotFoundException ex) {
945 Toast.makeText(this, R.string.no_way_to_share_video,
946 Toast.LENGTH_SHORT).show();
947 }
948 break;
949 }
950 case R.id.btn_done:
951 finish();
952 break;
953 case R.id.next_image:
954 moveNextOrPrevious(1);
955 break;
956 case R.id.prev_image:
957 moveNextOrPrevious(-1);
958 break;
959 }
960 }
961
962 private void moveNextOrPrevious(int delta) {
963 int nextImagePos = mCurrentPosition + delta;
964 if ((0 <= nextImagePos) && (nextImagePos < mAllImages.getCount())) {
965 setImage(nextImagePos);
966 }
967 }
968
969 @Override
970 protected void onActivityResult(int requestCode, int resultCode,
971 Intent data) {
972 switch (requestCode) {
973 case MenuHelper.RESULT_COMMON_MENU_CROP:
974 if (resultCode == RESULT_OK) {
975 // The CropImage activity passes back the Uri of the
976 // cropped image as the Action rather than the Data.
977 mSavedUri = Uri.parse(data.getAction());
978 }
979 break;
980 }
981 }
982
983 static class LocalHandler extends Handler {
984 private static final int IMAGE_GETTER_CALLBACK = 1;
985
986 @Override
987 public void handleMessage(Message message) {
988 switch(message.what) {
989 case IMAGE_GETTER_CALLBACK:
990 ((Runnable) message.obj).run();
991 break;
992 }
993 }
994
995 public void postGetterCallback(Runnable callback) {
996 postDelayedGetterCallback(callback, 0);
997 }
998
999 public void postDelayedGetterCallback(Runnable callback, long delay) {
1000 if (callback == null) {
1001 throw new NullPointerException();
1002 }
1003 Message message = Message.obtain();
1004 message.what = IMAGE_GETTER_CALLBACK;
1005 message.obj = callback;
1006 sendMessageDelayed(message, delay);
1007 }
1008
1009 public void removeAllGetterCallbacks() {
1010 removeMessages(IMAGE_GETTER_CALLBACK);
1011 }
1012 }
1013}
1014
1015class ReviewImageGetter {
1016
1017 @SuppressWarnings("unused")
1018 private static final String TAG = "ImageGetter";
1019
1020 // The thread which does the work.
1021 private final Thread mGetterThread;
1022
1023 // The base position that's being retrieved. The actual images retrieved
1024 // are this base plus each of the offets.
1025 private int mCurrentPosition = -1;
1026
1027 // The callback to invoke for each image.
1028 private ImageGetterCallback mCB;
1029
1030 // This is the loader cancelable that gets set while we're loading an image.
1031 // If we change position we can cancel the current load using this.
1032 private Cancelable<Bitmap> mLoad;
1033
1034 // True if we're canceling the current load.
1035 private boolean mCancelCurrent = false;
1036
1037 // True when the therad should exit.
1038 private boolean mDone = false;
1039
1040 // True when the loader thread is waiting for work.
1041 private boolean mReady = false;
1042
1043 // The ViewImage this ImageGetter belongs to
1044 ReviewImage mViewImage;
1045
1046 void cancelCurrent() {
1047 synchronized (this) {
1048 if (!mReady) {
1049 mCancelCurrent = true;
1050 Cancelable<Bitmap> load = mLoad;
1051 if (load != null) {
1052 load.requestCancel();
1053 }
1054 mCancelCurrent = false;
1055 }
1056 }
1057 }
1058
1059 private class ImageGetterRunnable implements Runnable {
1060 private Runnable callback(final int position, final int offset,
1061 final boolean isThumb, final Bitmap bitmap) {
1062 return new Runnable() {
1063 public void run() {
1064 // check for inflight callbacks that aren't applicable
1065 // any longer before delivering them
1066 if (!isCanceled() && position == mCurrentPosition) {
1067 mCB.imageLoaded(position, offset, bitmap, isThumb);
1068 } else if (bitmap != null) {
1069 bitmap.recycle();
1070 }
1071 }
1072 };
1073 }
1074
1075 private Runnable completedCallback(final boolean wasCanceled) {
1076 return new Runnable() {
1077 public void run() {
1078 mCB.completed(wasCanceled);
1079 }
1080 };
1081 }
1082
1083 public void run() {
1084 int lastPosition = -1;
1085 while (!mDone) {
1086 synchronized (ReviewImageGetter.this) {
1087 mReady = true;
1088 ReviewImageGetter.this.notify();
1089
1090 if (mCurrentPosition == -1
1091 || lastPosition == mCurrentPosition) {
1092 try {
1093 ReviewImageGetter.this.wait();
1094 } catch (InterruptedException ex) {
1095 continue;
1096 }
1097 }
1098
1099 lastPosition = mCurrentPosition;
1100 mReady = false;
1101 }
1102
1103 if (lastPosition != -1) {
1104 int imageCount = mViewImage.mAllImages.getCount();
1105
1106 int [] order = mCB.loadOrder();
1107 for (int i = 0; i < order.length; i++) {
1108 int offset = order[i];
1109 int imageNumber = lastPosition + offset;
1110 if (imageNumber >= 0 && imageNumber < imageCount) {
1111 IImage image = mViewImage.mAllImages
1112 .getImageAt(lastPosition + offset);
1113 if (image == null || isCanceled()) {
1114 break;
1115 }
1116 if (mCB.wantsThumbnail(lastPosition, offset)) {
1117 Bitmap b = image.thumbBitmap();
1118 mViewImage.mHandler.postGetterCallback(
1119 callback(lastPosition, offset,
1120 true, b));
1121 }
1122 }
1123 }
1124
1125 for (int i = 0; i < order.length; i++) {
1126 int offset = order[i];
1127 int imageNumber = lastPosition + offset;
1128 if (imageNumber >= 0 && imageNumber < imageCount) {
1129 IImage image = mViewImage.mAllImages
1130 .getImageAt(lastPosition + offset);
1131 if (mCB.wantsFullImage(lastPosition, offset)
1132 && !(image instanceof VideoObject)) {
1133 int sizeToUse = mCB.fullImageSizeToUse(
1134 lastPosition, offset);
1135 if (image != null && !isCanceled()) {
1136 mLoad = image.fullSizeBitmapCancelable(
1137 sizeToUse);
1138 }
1139 if (mLoad != null) {
1140 // The return value could be null if the
1141 // bitmap is too big, or we cancelled it.
1142 Bitmap b;
1143 try {
1144 b = mLoad.get();
1145 } catch (InterruptedException e) {
1146 b = null;
1147 } catch (ExecutionException e) {
1148 throw new RuntimeException(e);
1149 } catch (CancellationException e) {
1150 b = null;
1151 }
1152 mLoad = null;
1153 if (b != null) {
1154 if (isCanceled()) {
1155 b.recycle();
1156 } else {
1157 Runnable cb = callback(
1158 lastPosition, offset,
1159 false, b);
1160 mViewImage.mHandler
1161 .postGetterCallback(cb);
1162 }
1163 }
1164 }
1165 }
1166 }
1167 }
1168 mViewImage.mHandler.postGetterCallback(
1169 completedCallback(isCanceled()));
1170 }
1171 }
1172 }
1173 }
1174
1175 public ReviewImageGetter(ReviewImage viewImage) {
1176 mViewImage = viewImage;
1177 mGetterThread = new Thread(new ImageGetterRunnable());
1178 mGetterThread.setName("ImageGettter");
1179 mGetterThread.start();
1180 }
1181
1182 private boolean isCanceled() {
1183 synchronized (this) {
1184 return mCancelCurrent;
1185 }
1186 }
1187
1188 public void setPosition(int position, ImageGetterCallback cb) {
1189 synchronized (this) {
1190 if (!mReady) {
1191 try {
1192 mCancelCurrent = true;
1193 // if the thread is waiting before loading the full size
1194 // image then this will free it up
1195 BitmapManager.instance()
1196 .cancelThreadDecoding(mGetterThread);
1197 ReviewImageGetter.this.notify();
1198 ReviewImageGetter.this.wait();
1199 BitmapManager.instance()
1200 .allowThreadDecoding(mGetterThread);
1201 mCancelCurrent = false;
1202 } catch (InterruptedException ex) {
1203 // not sure what to do here
1204 }
1205 }
1206 }
1207
1208 mCurrentPosition = position;
1209 mCB = cb;
1210
1211 synchronized (this) {
1212 ReviewImageGetter.this.notify();
1213 }
1214 }
1215
1216 public void stop() {
1217 synchronized (this) {
1218 mDone = true;
1219 ReviewImageGetter.this.notify();
1220 }
1221 try {
1222 BitmapManager.instance().cancelThreadDecoding(mGetterThread);
1223 mGetterThread.join();
1224 } catch (InterruptedException ex) {
1225 // Ignore the exception
1226 }
1227 }
1228}
1229
1230class ImageViewTouch2 extends ImageViewTouchBase {
1231 private final ReviewImage mViewImage;
1232 private boolean mEnableTrackballScroll;
1233
1234 public ImageViewTouch2(Context context) {
1235 super(context);
1236 mViewImage = (ReviewImage) context;
1237 }
1238
1239 public ImageViewTouch2(Context context, AttributeSet attrs) {
1240 super(context, attrs);
1241 mViewImage = (ReviewImage) context;
1242 }
1243
1244 public void setEnableTrackballScroll(boolean enable) {
1245 mEnableTrackballScroll = enable;
1246 }
1247
1248 protected void postTranslateCenter(float dx, float dy) {
1249 super.postTranslate(dx, dy);
1250 center(true, true);
1251 }
1252
1253 static final float PAN_RATE = 20;
1254
1255 @Override
1256 public boolean onKeyDown(int keyCode, KeyEvent event) {
1257 // Don't respond to arrow keys if trackball scrolling is not enabled
1258 if (!mEnableTrackballScroll) {
1259 if ((keyCode >= KeyEvent.KEYCODE_DPAD_UP)
1260 && (keyCode <= KeyEvent.KEYCODE_DPAD_RIGHT)) {
1261 return super.onKeyDown(keyCode, event);
1262 }
1263 }
1264
1265 int current = mViewImage.mCurrentPosition;
1266
1267 int nextImagePos = -2; // default no next image
1268 try {
1269 switch (keyCode) {
1270 case KeyEvent.KEYCODE_DPAD_CENTER: {
1271 if (mViewImage.isPickIntent()) {
1272 IImage img = mViewImage.mAllImages
1273 .getImageAt(mViewImage.mCurrentPosition);
1274 mViewImage.setResult(ReviewImage.RESULT_OK,
1275 new Intent().setData(img.fullSizeImageUri()));
1276 mViewImage.finish();
1277 }
1278 break;
1279 }
1280 case KeyEvent.KEYCODE_DPAD_LEFT: {
1281 int maxOffset = (current == 0) ? 0 : ReviewImage.HYSTERESIS;
1282 if (getScale() <= 1F
1283 || isShiftedToNextImage(true, maxOffset)) {
1284 nextImagePos = current - 1;
1285 } else {
1286 panBy(PAN_RATE, 0);
1287 center(true, false);
1288 }
1289 return true;
1290 }
1291 case KeyEvent.KEYCODE_DPAD_RIGHT: {
1292 int maxOffset =
1293 (current == mViewImage.mAllImages.getCount() - 1)
1294 ? 0
1295 : ReviewImage.HYSTERESIS;
1296 if (getScale() <= 1F
1297 || isShiftedToNextImage(false, maxOffset)) {
1298 nextImagePos = current + 1;
1299 } else {
1300 panBy(-PAN_RATE, 0);
1301 center(true, false);
1302 }
1303 return true;
1304 }
1305 case KeyEvent.KEYCODE_DPAD_UP: {
1306 panBy(0, PAN_RATE);
1307 center(false, true);
1308 return true;
1309 }
1310 case KeyEvent.KEYCODE_DPAD_DOWN: {
1311 panBy(0, -PAN_RATE);
1312 center(false, true);
1313 return true;
1314 }
1315 case KeyEvent.KEYCODE_DEL:
1316 MenuHelper.deletePhoto(
1317 mViewImage, mViewImage.mDeletePhotoRunnable);
1318 break;
1319 }
1320 } finally {
1321 if (nextImagePos >= 0
1322 && nextImagePos < mViewImage.mAllImages.getCount()) {
1323 synchronized (mViewImage) {
1324 mViewImage.setMode(ReviewImage.MODE_NORMAL);
1325 mViewImage.setImage(nextImagePos);
1326 }
1327 } else if (nextImagePos != -2) {
1328 center(true, true);
1329 }
1330 }
1331
1332 return super.onKeyDown(keyCode, event);
1333 }
1334
1335 protected boolean isShiftedToNextImage(boolean left, int maxOffset) {
1336 boolean retval;
1337 Bitmap bitmap = mBitmapDisplayed;
1338 Matrix m = getImageViewMatrix();
1339 if (left) {
1340 float [] t1 = new float[] { 0, 0 };
1341 m.mapPoints(t1);
1342 retval = t1[0] > maxOffset;
1343 } else {
1344 int width = bitmap != null ? bitmap.getWidth() : getWidth();
1345 float [] t1 = new float[] { width, 0 };
1346 m.mapPoints(t1);
1347 retval = t1[0] + maxOffset < getWidth();
1348 }
1349 return retval;
1350 }
1351}