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