PhotoPage.java revision 00ccf35f8ec016b2cb8fdcf0c65ba643dca54a14
1/* 2 * Copyright (C) 2010 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.gallery3d.app; 18 19import android.annotation.TargetApi; 20import android.app.Activity; 21import android.content.ActivityNotFoundException; 22import android.content.ContentResolver; 23import android.content.Context; 24import android.content.Intent; 25import android.content.pm.PackageManager; 26import android.graphics.Rect; 27import android.net.Uri; 28import android.nfc.NfcAdapter; 29import android.os.Bundle; 30import android.os.Handler; 31import android.os.Message; 32import android.view.animation.AccelerateInterpolator; 33import android.widget.RelativeLayout; 34import android.widget.Toast; 35 36import com.actionbarsherlock.app.ActionBar.OnMenuVisibilityListener; 37import com.actionbarsherlock.view.Menu; 38import com.actionbarsherlock.view.MenuItem; 39import com.android.gallery3d.R; 40import com.android.gallery3d.anim.FloatAnimation; 41import com.android.gallery3d.common.ApiHelper; 42import com.android.gallery3d.common.Utils; 43import com.android.gallery3d.data.DataManager; 44import com.android.gallery3d.data.FilterDeleteSet; 45import com.android.gallery3d.data.MediaDetails; 46import com.android.gallery3d.data.MediaItem; 47import com.android.gallery3d.data.MediaObject; 48import com.android.gallery3d.data.MediaSet; 49import com.android.gallery3d.data.MtpSource; 50import com.android.gallery3d.data.Path; 51import com.android.gallery3d.data.SecureAlbum; 52import com.android.gallery3d.data.SecureSource; 53import com.android.gallery3d.data.SnailAlbum; 54import com.android.gallery3d.data.SnailItem; 55import com.android.gallery3d.data.SnailSource; 56import com.android.gallery3d.picasasource.PicasaSource; 57import com.android.gallery3d.ui.AnimationTime; 58import com.android.gallery3d.ui.BitmapScreenNail; 59import com.android.gallery3d.ui.DetailsHelper; 60import com.android.gallery3d.ui.DetailsHelper.CloseListener; 61import com.android.gallery3d.ui.DetailsHelper.DetailsSource; 62import com.android.gallery3d.ui.GLCanvas; 63import com.android.gallery3d.ui.GLRoot; 64import com.android.gallery3d.ui.GLRoot.OnGLIdleListener; 65import com.android.gallery3d.ui.GLView; 66import com.android.gallery3d.ui.ImportCompleteListener; 67import com.android.gallery3d.ui.MenuExecutor; 68import com.android.gallery3d.ui.PhotoFallbackEffect; 69import com.android.gallery3d.ui.PhotoView; 70import com.android.gallery3d.ui.PreparePageFadeoutTexture; 71import com.android.gallery3d.ui.RawTexture; 72import com.android.gallery3d.ui.SelectionManager; 73import com.android.gallery3d.ui.SynchronizedHandler; 74import com.android.gallery3d.util.GalleryUtils; 75import com.android.gallery3d.util.LightCycleHelper; 76import com.android.gallery3d.util.MediaSetUtils; 77 78public class PhotoPage extends ActivityState implements 79 PhotoView.Listener, OrientationManager.Listener, AppBridge.Server, 80 PhotoPageBottomControls.Delegate { 81 private static final String TAG = "PhotoPage"; 82 83 private static final int MSG_HIDE_BARS = 1; 84 private static final int MSG_LOCK_ORIENTATION = 2; 85 private static final int MSG_UNLOCK_ORIENTATION = 3; 86 private static final int MSG_ON_FULL_SCREEN_CHANGED = 4; 87 private static final int MSG_UPDATE_ACTION_BAR = 5; 88 private static final int MSG_UNFREEZE_GLROOT = 6; 89 private static final int MSG_WANT_BARS = 7; 90 private static final int MSG_REFRESH_GRID_BUTTON = 8; 91 private static final int MSG_REFRESH_BOTTOM_CONTROLS = 9; 92 93 private static final int HIDE_BARS_TIMEOUT = 3500; 94 private static final int UNFREEZE_GLROOT_TIMEOUT = 250; 95 96 private static final int REQUEST_SLIDESHOW = 1; 97 private static final int REQUEST_CROP = 2; 98 private static final int REQUEST_CROP_PICASA = 3; 99 private static final int REQUEST_EDIT = 4; 100 private static final int REQUEST_PLAY_VIDEO = 5; 101 private static final int REQUEST_TRIM = 6; 102 103 public static final String KEY_MEDIA_SET_PATH = "media-set-path"; 104 public static final String KEY_MEDIA_ITEM_PATH = "media-item-path"; 105 public static final String KEY_INDEX_HINT = "index-hint"; 106 public static final String KEY_OPEN_ANIMATION_RECT = "open-animation-rect"; 107 public static final String KEY_APP_BRIDGE = "app-bridge"; 108 public static final String KEY_TREAT_BACK_AS_UP = "treat-back-as-up"; 109 public static final String KEY_START_IN_FILMSTRIP = "start-in-filmstrip"; 110 public static final String KEY_RETURN_INDEX_HINT = "return-index-hint"; 111 public static final String KEY_SHOW_WHEN_LOCKED = "show_when_locked"; 112 113 public static final String KEY_ALBUMPAGE_TRANSITION = "albumpage-transition"; 114 public static final int MSG_ALBUMPAGE_NONE = 0; 115 public static final int MSG_ALBUMPAGE_STARTED = 1; 116 public static final int MSG_ALBUMPAGE_RESUMED = 2; 117 public static final int MSG_ALBUMPAGE_PICKED = 4; 118 119 public static final String ACTION_NEXTGEN_EDIT = "action_nextgen_edit"; 120 121 private GalleryApp mApplication; 122 private SelectionManager mSelectionManager; 123 124 private PhotoView mPhotoView; 125 private PhotoPage.Model mModel; 126 private DetailsHelper mDetailsHelper; 127 private boolean mShowDetails; 128 129 // mMediaSet could be null if there is no KEY_MEDIA_SET_PATH supplied. 130 // E.g., viewing a photo in gmail attachment 131 private FilterDeleteSet mMediaSet; 132 133 // The mediaset used by camera launched from secure lock screen. 134 private SecureAlbum mSecureAlbum; 135 136 private int mCurrentIndex = 0; 137 private Handler mHandler; 138 private boolean mShowBars = true; 139 private volatile boolean mActionBarAllowed = true; 140 private GalleryActionBar mActionBar; 141 private boolean mIsMenuVisible; 142 private PhotoPageBottomControls mBottomControls; 143 private MediaItem mCurrentPhoto = null; 144 private MenuExecutor mMenuExecutor; 145 private boolean mIsActive; 146 private String mSetPathString; 147 // This is the original mSetPathString before adding the camera preview item. 148 private String mOriginalSetPathString; 149 private AppBridge mAppBridge; 150 private SnailItem mScreenNailItem; 151 private SnailAlbum mScreenNailSet; 152 private OrientationManager mOrientationManager; 153 private boolean mHasActivityResult; 154 private boolean mTreatBackAsUp; 155 private boolean mStartInFilmstrip; 156 private boolean mStartedFromAlbumPage; 157 158 private RawTexture mFadeOutTexture; 159 private Rect mOpenAnimationRect; 160 public static final int ANIM_TIME_OPENING = 300; 161 162 // The item that is deleted (but it can still be undeleted before commiting) 163 private Path mDeletePath; 164 private boolean mDeleteIsFocus; // whether the deleted item was in focus 165 166 private NfcAdapter mNfcAdapter; 167 168 private final MyMenuVisibilityListener mMenuVisibilityListener = 169 new MyMenuVisibilityListener(); 170 171 public static interface Model extends PhotoView.Model { 172 public void resume(); 173 public void pause(); 174 public boolean isEmpty(); 175 public void setCurrentPhoto(Path path, int indexHint); 176 } 177 178 private class MyMenuVisibilityListener implements OnMenuVisibilityListener { 179 @Override 180 public void onMenuVisibilityChanged(boolean isVisible) { 181 mIsMenuVisible = isVisible; 182 refreshHidingMessage(); 183 } 184 } 185 186 private static class BackgroundFadeOut extends FloatAnimation { 187 public BackgroundFadeOut() { 188 super(1f, 0f, ANIM_TIME_OPENING); 189 setInterpolator(new AccelerateInterpolator(2f)); 190 } 191 } 192 193 private final FloatAnimation mBackgroundFade = new BackgroundFadeOut(); 194 195 @Override 196 protected int getBackgroundColorId() { 197 return R.color.photo_background; 198 } 199 200 private final GLView mRootPane = new GLView() { 201 @Override 202 protected void renderBackground(GLCanvas view) { 203 if (mFadeOutTexture != null) { 204 if (mBackgroundFade.calculate(AnimationTime.get())) invalidate(); 205 if (!mBackgroundFade.isActive()) { 206 mFadeOutTexture = null; 207 mOpenAnimationRect = null; 208 BitmapScreenNail.enableDrawPlaceholder(); 209 } else { 210 float fadeAlpha = mBackgroundFade.get(); 211 if (fadeAlpha < 1f) { 212 view.clearBuffer(getBackgroundColor()); 213 view.setAlpha(fadeAlpha); 214 } 215 mFadeOutTexture.draw(view, 0, 0); 216 view.setAlpha(1f - fadeAlpha); 217 return; 218 } 219 } 220 view.clearBuffer(getBackgroundColor()); 221 } 222 223 @Override 224 protected void onLayout( 225 boolean changed, int left, int top, int right, int bottom) { 226 mPhotoView.layout(0, 0, right - left, bottom - top); 227 if (mShowDetails) { 228 mDetailsHelper.layout(left, mActionBar.getHeight(), right, bottom); 229 } 230 } 231 }; 232 233 @Override 234 public void onCreate(Bundle data, Bundle restoreState) { 235 super.onCreate(data, restoreState); 236 mActionBar = mActivity.getGalleryActionBar(); 237 mSelectionManager = new SelectionManager(mActivity, false); 238 mMenuExecutor = new MenuExecutor(mActivity, mSelectionManager); 239 240 mPhotoView = new PhotoView(mActivity); 241 mPhotoView.setListener(this); 242 mRootPane.addComponent(mPhotoView); 243 mApplication = (GalleryApp) ((Activity) mActivity).getApplication(); 244 mOrientationManager = mActivity.getOrientationManager(); 245 mOrientationManager.addListener(this); 246 mActivity.getGLRoot().setOrientationSource(mOrientationManager); 247 248 mHandler = new SynchronizedHandler(mActivity.getGLRoot()) { 249 @Override 250 public void handleMessage(Message message) { 251 switch (message.what) { 252 case MSG_HIDE_BARS: { 253 hideBars(); 254 break; 255 } 256 case MSG_REFRESH_GRID_BUTTON: { 257 setGridButtonVisibility(mPhotoView.getFilmMode()); 258 break; 259 } 260 case MSG_REFRESH_BOTTOM_CONTROLS: { 261 if (mBottomControls != null) mBottomControls.refresh(); 262 break; 263 } 264 case MSG_LOCK_ORIENTATION: { 265 mOrientationManager.lockOrientation(); 266 break; 267 } 268 case MSG_UNLOCK_ORIENTATION: { 269 mOrientationManager.unlockOrientation(); 270 break; 271 } 272 case MSG_ON_FULL_SCREEN_CHANGED: { 273 mAppBridge.onFullScreenChanged(message.arg1 == 1); 274 break; 275 } 276 case MSG_UPDATE_ACTION_BAR: { 277 updateBars(); 278 break; 279 } 280 case MSG_WANT_BARS: { 281 wantBars(); 282 break; 283 } 284 case MSG_UNFREEZE_GLROOT: { 285 mActivity.getGLRoot().unfreeze(); 286 break; 287 } 288 default: throw new AssertionError(message.what); 289 } 290 } 291 }; 292 293 mSetPathString = data.getString(KEY_MEDIA_SET_PATH); 294 mOriginalSetPathString = mSetPathString; 295 mNfcAdapter = NfcAdapter.getDefaultAdapter(mActivity.getAndroidContext()); 296 String itemPathString = data.getString(KEY_MEDIA_ITEM_PATH); 297 Path itemPath = itemPathString != null ? 298 Path.fromString(data.getString(KEY_MEDIA_ITEM_PATH)) : 299 null; 300 mTreatBackAsUp = data.getBoolean(KEY_TREAT_BACK_AS_UP, false); 301 mStartInFilmstrip = 302 data.getBoolean(KEY_START_IN_FILMSTRIP, false); 303 mStartedFromAlbumPage = 304 data.getInt(KEY_ALBUMPAGE_TRANSITION, 305 MSG_ALBUMPAGE_NONE) == MSG_ALBUMPAGE_STARTED; 306 setGridButtonVisibility(!mStartedFromAlbumPage); 307 if (mSetPathString != null) { 308 mAppBridge = (AppBridge) data.getParcelable(KEY_APP_BRIDGE); 309 if (mAppBridge != null) { 310 mShowBars = false; 311 312 mAppBridge.setServer(this); 313 mOrientationManager.lockOrientation(); 314 315 // Get the ScreenNail from AppBridge and register it. 316 int id = SnailSource.newId(); 317 Path screenNailSetPath = SnailSource.getSetPath(id); 318 Path screenNailItemPath = SnailSource.getItemPath(id); 319 mScreenNailSet = (SnailAlbum) mActivity.getDataManager() 320 .getMediaObject(screenNailSetPath); 321 mScreenNailItem = (SnailItem) mActivity.getDataManager() 322 .getMediaObject(screenNailItemPath); 323 mScreenNailItem.setScreenNail(mAppBridge.attachScreenNail()); 324 325 // Check if the path is a secure album. 326 if (SecureSource.isSecurePath(mSetPathString)) { 327 mSecureAlbum = (SecureAlbum) mActivity.getDataManager() 328 .getMediaSet(mSetPathString); 329 } 330 if (data.getBoolean(KEY_SHOW_WHEN_LOCKED, false)) { 331 // Set the flag to be on top of the lock screen. 332 mFlags |= FLAG_SHOW_WHEN_LOCKED; 333 } 334 335 // Combine the original MediaSet with the one for ScreenNail 336 // from AppBridge. 337 mSetPathString = "/combo/item/{" + screenNailSetPath + 338 "," + mSetPathString + "}"; 339 340 // Start from the screen nail. 341 itemPath = screenNailItemPath; 342 } 343 344 MediaSet originalSet = mActivity.getDataManager() 345 .getMediaSet(mSetPathString); 346 mSelectionManager.setSourceMediaSet(originalSet); 347 mSetPathString = "/filter/delete/{" + mSetPathString + "}"; 348 mMediaSet = (FilterDeleteSet) mActivity.getDataManager() 349 .getMediaSet(mSetPathString); 350 mCurrentIndex = data.getInt(KEY_INDEX_HINT, 0); 351 if (mMediaSet == null) { 352 Log.w(TAG, "failed to restore " + mSetPathString); 353 } 354 if (itemPath == null) { 355 int mediaItemCount = mMediaSet.getMediaItemCount(); 356 if (mediaItemCount > 0) { 357 if (mCurrentIndex >= mediaItemCount) mCurrentIndex = 0; 358 itemPath = mMediaSet.getMediaItem(mCurrentIndex, 1) 359 .get(0).getPath(); 360 } else { 361 // Bail out, PhotoPage can't load on an empty album 362 return; 363 } 364 } 365 PhotoDataAdapter pda = new PhotoDataAdapter( 366 mActivity, mPhotoView, mMediaSet, itemPath, mCurrentIndex, 367 mAppBridge == null ? -1 : 0, 368 mAppBridge == null ? false : mAppBridge.isPanorama(), 369 mAppBridge == null ? false : mAppBridge.isStaticCamera()); 370 mModel = pda; 371 mPhotoView.setModel(mModel); 372 373 pda.setDataListener(new PhotoDataAdapter.DataListener() { 374 375 @Override 376 public void onPhotoChanged(int index, Path item) { 377 int oldIndex = mCurrentIndex; 378 mCurrentIndex = index; 379 if (item != null) { 380 MediaItem photo = mModel.getMediaItem(0); 381 if (photo != null) updateCurrentPhoto(photo); 382 } 383 if (mAppBridge != null) { 384 if (oldIndex == 0 && mCurrentIndex > 0 385 && !mPhotoView.getFilmMode()) { 386 mPhotoView.setFilmMode(true); 387 } 388 } 389 updateBars(); 390 391 // Reset the timeout for the bars after a swipe 392 refreshHidingMessage(); 393 } 394 395 @Override 396 public void onLoadingFinished() { 397 if (!mModel.isEmpty()) { 398 MediaItem photo = mModel.getMediaItem(0); 399 if (photo != null) updateCurrentPhoto(photo); 400 } else if (mIsActive) { 401 // We only want to finish the PhotoPage if there is no 402 // deletion that the user can undo. 403 if (mMediaSet.getNumberOfDeletions() == 0) { 404 mActivity.getStateManager().finishState( 405 PhotoPage.this); 406 } 407 } 408 } 409 410 @Override 411 public void onLoadingStarted() { 412 } 413 }); 414 } else { 415 // Get default media set by the URI 416 MediaItem mediaItem = (MediaItem) 417 mActivity.getDataManager().getMediaObject(itemPath); 418 mModel = new SinglePhotoDataAdapter(mActivity, mPhotoView, mediaItem); 419 mPhotoView.setModel(mModel); 420 updateCurrentPhoto(mediaItem); 421 } 422 423 mPhotoView.setFilmMode(mStartInFilmstrip && mMediaSet.getMediaItemCount() > 1); 424 if (mSecureAlbum == null) { 425 RelativeLayout galleryRoot = (RelativeLayout) ((Activity) mActivity) 426 .findViewById(mAppBridge != null ? R.id.content : R.id.gallery_root); 427 if (galleryRoot != null) { 428 mBottomControls = new PhotoPageBottomControls(this, mActivity, galleryRoot); 429 } 430 } 431 } 432 433 public boolean canDisplayBottomControls() { 434 return mShowBars && !mPhotoView.getFilmMode(); 435 } 436 437 public boolean canDisplayBottomControl(int control) { 438 if (mCurrentPhoto == null) return false; 439 switch(control) { 440 case R.id.photopage_bottom_control_edit: 441 return mCurrentPhoto.getMediaType() == MediaObject.MEDIA_TYPE_IMAGE; 442 case R.id.photopage_bottom_control_panorama: 443 return (mCurrentPhoto.getSupportedOperations() 444 & MediaItem.SUPPORT_PANORAMA) != 0; 445 default: 446 return false; 447 } 448 } 449 450 public void onBottomControlClicked(int control) { 451 switch(control) { 452 case R.id.photopage_bottom_control_edit: 453 launchPhotoEditor(); 454 return; 455 case R.id.photopage_bottom_control_panorama: 456 LightCycleHelper.viewPanorama(mActivity, mCurrentPhoto.getContentUri()); 457 return; 458 default: 459 return; 460 } 461 } 462 463 @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN) 464 private void setNfcBeamPushUris(Uri[] uris) { 465 if (mNfcAdapter != null && ApiHelper.HAS_SET_BEAM_PUSH_URIS) { 466 mNfcAdapter.setBeamPushUris(uris, mActivity); 467 } 468 } 469 470 private Intent createShareIntent(Path path) { 471 DataManager manager = mActivity.getDataManager(); 472 int type = manager.getMediaType(path); 473 int support = manager.getSupportedOperations(path); 474 boolean isPanorama = (support & MediaObject.SUPPORT_PANORAMA) != 0; 475 Intent intent = new Intent(Intent.ACTION_SEND); 476 intent.setType(MenuExecutor.getMimeType(type, isPanorama)); 477 Uri uri = manager.getContentUri(path); 478 intent.putExtra(Intent.EXTRA_STREAM, uri); 479 return intent; 480 481 } 482 483 private void launchPhotoEditor() { 484 MediaItem current = mModel.getMediaItem(0); 485 if (current == null) return; 486 487 Intent intent = new Intent(ACTION_NEXTGEN_EDIT); 488 intent.setData(mActivity.getDataManager().getContentUri(current.getPath())).setFlags( 489 Intent.FLAG_GRANT_READ_URI_PERMISSION); 490 if (mActivity.getPackageManager() 491 .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY).size() == 0) { 492 intent.setAction(Intent.ACTION_EDIT); 493 } 494 ((Activity) mActivity).startActivityForResult(Intent.createChooser(intent, null), 495 REQUEST_EDIT); 496 } 497 498 private void updateShareURI(Path path) { 499 DataManager manager = mActivity.getDataManager(); 500 Uri uri = manager.getContentUri(path); 501 mActionBar.setShareIntent(createShareIntent(path)); 502 setNfcBeamPushUris(new Uri[]{uri}); 503 } 504 505 private void updateCurrentPhoto(MediaItem photo) { 506 if (mCurrentPhoto == photo) return; 507 mCurrentPhoto = photo; 508 if (mCurrentPhoto == null) return; 509 updateMenuOperations(); 510 updateTitle(); 511 if (mBottomControls != null) mBottomControls.refresh(); 512 if (mShowDetails) { 513 mDetailsHelper.reloadDetails(); 514 } 515 if ((mSecureAlbum == null) 516 && (photo.getSupportedOperations() & MediaItem.SUPPORT_SHARE) != 0) { 517 updateShareURI(photo.getPath()); 518 } 519 } 520 521 private void updateTitle() { 522 if (mCurrentPhoto == null) return; 523 boolean showTitle = mActivity.getAndroidContext().getResources().getBoolean( 524 R.bool.show_action_bar_title); 525 if (showTitle && mCurrentPhoto.getName() != null) { 526 mActionBar.setTitle(mCurrentPhoto.getName()); 527 } else { 528 mActionBar.setTitle(""); 529 } 530 } 531 532 private void updateMenuOperations() { 533 Menu menu = mActionBar.getMenu(); 534 535 // it could be null if onCreateActionBar has not been called yet 536 if (menu == null) return; 537 538 setGridButtonVisibility(mPhotoView.getFilmMode()); 539 540 MenuItem item = menu.findItem(R.id.action_slideshow); 541 item.setVisible((mSecureAlbum == null) && canDoSlideShow()); 542 if (mCurrentPhoto == null) return; 543 544 int supportedOperations = mCurrentPhoto.getSupportedOperations(); 545 if (mSecureAlbum != null) { 546 supportedOperations &= MediaObject.SUPPORT_DELETE; 547 } else if (!GalleryUtils.isEditorAvailable(mActivity, "image/*")) { 548 supportedOperations &= ~MediaObject.SUPPORT_EDIT; 549 } 550 MenuExecutor.updateMenuOperation(menu, supportedOperations); 551 } 552 553 private boolean canDoSlideShow() { 554 if (mMediaSet == null || mCurrentPhoto == null) { 555 return false; 556 } 557 if (mCurrentPhoto.getMediaType() != MediaObject.MEDIA_TYPE_IMAGE) { 558 return false; 559 } 560 if (MtpSource.isMtpPath(mOriginalSetPathString)) { 561 return false; 562 } 563 return true; 564 } 565 566 ////////////////////////////////////////////////////////////////////////// 567 // Action Bar show/hide management 568 ////////////////////////////////////////////////////////////////////////// 569 570 private void showBars() { 571 if (mShowBars) return; 572 mShowBars = true; 573 mOrientationManager.unlockOrientation(); 574 mActionBar.show(); 575 mActivity.getGLRoot().setLightsOutMode(false); 576 refreshHidingMessage(); 577 if (mBottomControls != null) mBottomControls.refresh(); 578 } 579 580 private void hideBars() { 581 if (!mShowBars) return; 582 mShowBars = false; 583 mActionBar.hide(); 584 mActivity.getGLRoot().setLightsOutMode(true); 585 mHandler.removeMessages(MSG_HIDE_BARS); 586 if (mBottomControls != null) mBottomControls.refresh(); 587 } 588 589 private void refreshHidingMessage() { 590 mHandler.removeMessages(MSG_HIDE_BARS); 591 if (!mIsMenuVisible && !mPhotoView.getFilmMode()) { 592 mHandler.sendEmptyMessageDelayed(MSG_HIDE_BARS, HIDE_BARS_TIMEOUT); 593 } 594 } 595 596 private boolean canShowBars() { 597 // No bars if we are showing camera preview. 598 if (mAppBridge != null && mCurrentIndex == 0 599 && !mPhotoView.getFilmMode()) return false; 600 601 // No bars if it's not allowed. 602 if (!mActionBarAllowed) return false; 603 604 return true; 605 } 606 607 private void wantBars() { 608 if (canShowBars()) showBars(); 609 } 610 611 private void toggleBars() { 612 if (mShowBars) { 613 hideBars(); 614 } else { 615 if (canShowBars()) showBars(); 616 } 617 } 618 619 private void updateBars() { 620 if (!canShowBars()) { 621 hideBars(); 622 } 623 } 624 625 @Override 626 public void onOrientationCompensationChanged() { 627 mActivity.getGLRoot().requestLayoutContentPane(); 628 } 629 630 @Override 631 protected void onBackPressed() { 632 if (mShowDetails) { 633 hideDetails(); 634 } else if (mAppBridge == null || !switchWithCaptureAnimation(-1)) { 635 // We are leaving this page. Set the result now. 636 setResult(); 637 if (mStartInFilmstrip && !mPhotoView.getFilmMode()) { 638 mPhotoView.setFilmMode(true); 639 } else if (mTreatBackAsUp) { 640 onUpPressed(); 641 } else { 642 super.onBackPressed(); 643 } 644 } 645 } 646 647 private void onUpPressed() { 648 if (mStartInFilmstrip && !mPhotoView.getFilmMode()) { 649 mPhotoView.setFilmMode(true); 650 return; 651 } 652 653 if (mActivity.getStateManager().getStateCount() > 1) { 654 setResult(); 655 super.onBackPressed(); 656 return; 657 } 658 659 if (mOriginalSetPathString == null) return; 660 661 if (mAppBridge == null) { 662 // We're in view mode so set up the stacks on our own. 663 Bundle data = new Bundle(getData()); 664 data.putString(AlbumPage.KEY_MEDIA_PATH, mOriginalSetPathString); 665 data.putString(AlbumPage.KEY_PARENT_MEDIA_PATH, 666 mActivity.getDataManager().getTopSetPath( 667 DataManager.INCLUDE_ALL)); 668 mActivity.getStateManager().switchState(this, AlbumPage.class, data); 669 } else { 670 // Start the real gallery activity to view the camera roll. 671 Uri uri = Uri.parse("content://media/external/file?bucketId=" 672 + MediaSetUtils.CAMERA_BUCKET_ID); 673 Intent intent = new Intent(Intent.ACTION_VIEW); 674 intent.setDataAndType(uri, ContentResolver.CURSOR_DIR_BASE_TYPE + "/image"); 675 ((Activity) mActivity).startActivity(intent); 676 } 677 } 678 679 private void setResult() { 680 Intent result = null; 681 result = new Intent(); 682 result.putExtra(KEY_RETURN_INDEX_HINT, mCurrentIndex); 683 setStateResult(Activity.RESULT_OK, result); 684 } 685 686 ////////////////////////////////////////////////////////////////////////// 687 // AppBridge.Server interface 688 ////////////////////////////////////////////////////////////////////////// 689 690 @Override 691 public void setCameraRelativeFrame(Rect frame) { 692 mPhotoView.setCameraRelativeFrame(frame); 693 } 694 695 @Override 696 public boolean switchWithCaptureAnimation(int offset) { 697 return mPhotoView.switchWithCaptureAnimation(offset); 698 } 699 700 @Override 701 public void setSwipingEnabled(boolean enabled) { 702 mPhotoView.setSwipingEnabled(enabled); 703 } 704 705 @Override 706 public void notifyScreenNailChanged() { 707 mScreenNailItem.setScreenNail(mAppBridge.attachScreenNail()); 708 mScreenNailSet.notifyChange(); 709 } 710 711 @Override 712 public void addSecureAlbumItem(boolean isVideo, int id) { 713 mSecureAlbum.addMediaItem(isVideo, id); 714 } 715 716 @Override 717 protected boolean onCreateActionBar(Menu menu) { 718 mActionBar.createActionBarMenu(R.menu.photo, menu); 719 updateMenuOperations(); 720 updateTitle(); 721 return true; 722 } 723 724 private MenuExecutor.ProgressListener mConfirmDialogListener = 725 new MenuExecutor.ProgressListener() { 726 @Override 727 public void onProgressUpdate(int index) {} 728 729 @Override 730 public void onProgressComplete(int result) {} 731 732 @Override 733 public void onConfirmDialogShown() { 734 mHandler.removeMessages(MSG_HIDE_BARS); 735 } 736 737 @Override 738 public void onConfirmDialogDismissed(boolean confirmed) { 739 refreshHidingMessage(); 740 } 741 742 @Override 743 public void onProgressStart() {} 744 }; 745 746 @Override 747 protected boolean onItemSelected(MenuItem item) { 748 if (mModel == null) return true; 749 refreshHidingMessage(); 750 MediaItem current = mModel.getMediaItem(0); 751 752 if (current == null) { 753 // item is not ready, ignore 754 return true; 755 } 756 757 int currentIndex = mModel.getCurrentIndex(); 758 Path path = current.getPath(); 759 760 DataManager manager = mActivity.getDataManager(); 761 int action = item.getItemId(); 762 String confirmMsg = null; 763 switch (action) { 764 case android.R.id.home: { 765 onUpPressed(); 766 return true; 767 } 768 case R.id.action_grid: { 769 if (mStartedFromAlbumPage) { 770 onUpPressed(); 771 } else { 772 preparePhotoFallbackView(); 773 Bundle data = new Bundle(getData()); 774 data.putString(AlbumPage.KEY_MEDIA_PATH, mOriginalSetPathString); 775 data.putString(AlbumPage.KEY_PARENT_MEDIA_PATH, 776 mActivity.getDataManager().getTopSetPath( 777 DataManager.INCLUDE_ALL)); 778 779 // We only show cluster menu in the first AlbumPage in stack 780 // TODO: Enable this when running from the camera app 781 boolean inAlbum = mActivity.getStateManager().hasStateClass(AlbumPage.class); 782 data.putBoolean(AlbumPage.KEY_SHOW_CLUSTER_MENU, !inAlbum 783 && mAppBridge == null); 784 785 data.putBoolean(PhotoPage.KEY_APP_BRIDGE, mAppBridge != null); 786 787 // Account for live preview being first item 788 mActivity.getTransitionStore().put(KEY_RETURN_INDEX_HINT, 789 mAppBridge != null ? mCurrentIndex - 1 : mCurrentIndex); 790 791 mActivity.getStateManager().startState(AlbumPage.class, data); 792 } 793 return true; 794 } 795 case R.id.action_slideshow: { 796 Bundle data = new Bundle(); 797 data.putString(SlideshowPage.KEY_SET_PATH, mMediaSet.getPath().toString()); 798 data.putString(SlideshowPage.KEY_ITEM_PATH, path.toString()); 799 data.putInt(SlideshowPage.KEY_PHOTO_INDEX, currentIndex); 800 data.putBoolean(SlideshowPage.KEY_REPEAT, true); 801 mActivity.getStateManager().startStateForResult( 802 SlideshowPage.class, REQUEST_SLIDESHOW, data); 803 return true; 804 } 805 case R.id.action_crop: { 806 Activity activity = mActivity; 807 Intent intent = new Intent(CropImage.CROP_ACTION); 808 intent.setClass(activity, CropImage.class); 809 intent.setData(manager.getContentUri(path)); 810 activity.startActivityForResult(intent, PicasaSource.isPicasaImage(current) 811 ? REQUEST_CROP_PICASA 812 : REQUEST_CROP); 813 return true; 814 } 815 case R.id.action_trim: { 816 Intent intent = new Intent(mActivity, TrimVideo.class); 817 intent.setData(manager.getContentUri(path)); 818 // We need the file path to wrap this into a RandomAccessFile. 819 intent.putExtra(KEY_MEDIA_ITEM_PATH, current.getFilePath()); 820 mActivity.startActivityForResult(intent, REQUEST_TRIM); 821 return true; 822 } 823 case R.id.action_edit: { 824 launchPhotoEditor(); 825 return true; 826 } 827 case R.id.action_details: { 828 if (mShowDetails) { 829 hideDetails(); 830 } else { 831 showDetails(); 832 } 833 return true; 834 } 835 case R.id.action_delete: 836 confirmMsg = mActivity.getResources().getQuantityString( 837 R.plurals.delete_selection, 1); 838 case R.id.action_setas: 839 case R.id.action_rotate_ccw: 840 case R.id.action_rotate_cw: 841 case R.id.action_show_on_map: 842 mSelectionManager.deSelectAll(); 843 mSelectionManager.toggle(path); 844 mMenuExecutor.onMenuClicked(item, confirmMsg, mConfirmDialogListener); 845 return true; 846 case R.id.action_import: 847 mSelectionManager.deSelectAll(); 848 mSelectionManager.toggle(path); 849 mMenuExecutor.onMenuClicked(item, confirmMsg, 850 new ImportCompleteListener(mActivity)); 851 return true; 852 case R.id.action_share: 853 Activity activity = mActivity; 854 Intent intent = createShareIntent(mCurrentPhoto.getPath()); 855 activity.startActivity(Intent.createChooser(intent, 856 activity.getString(R.string.share))); 857 return true; 858 default : 859 return false; 860 } 861 } 862 863 private void hideDetails() { 864 mShowDetails = false; 865 mDetailsHelper.hide(); 866 } 867 868 private void showDetails() { 869 mShowDetails = true; 870 if (mDetailsHelper == null) { 871 mDetailsHelper = new DetailsHelper(mActivity, mRootPane, new MyDetailsSource()); 872 mDetailsHelper.setCloseListener(new CloseListener() { 873 @Override 874 public void onClose() { 875 hideDetails(); 876 } 877 }); 878 } 879 mDetailsHelper.show(); 880 } 881 882 //////////////////////////////////////////////////////////////////////////// 883 // Callbacks from PhotoView 884 //////////////////////////////////////////////////////////////////////////// 885 @Override 886 public void onSingleTapUp(int x, int y) { 887 if (mAppBridge != null) { 888 if (mAppBridge.onSingleTapUp(x, y)) return; 889 } 890 891 MediaItem item = mModel.getMediaItem(0); 892 if (item == null || item == mScreenNailItem) { 893 // item is not ready or it is camera preview, ignore 894 return; 895 } 896 897 int supported = item.getSupportedOperations(); 898 boolean playVideo = (mSecureAlbum == null) && 899 ((supported & MediaItem.SUPPORT_PLAY) != 0); 900 boolean viewPanorama = (mSecureAlbum == null) && 901 ((supported & MediaItem.SUPPORT_PANORAMA) != 0); 902 boolean unlock = ((supported & MediaItem.SUPPORT_UNLOCK) != 0); 903 904 if (playVideo) { 905 // determine if the point is at center (1/6) of the photo view. 906 // (The position of the "play" icon is at center (1/6) of the photo) 907 int w = mPhotoView.getWidth(); 908 int h = mPhotoView.getHeight(); 909 playVideo = (Math.abs(x - w / 2) * 12 <= w) 910 && (Math.abs(y - h / 2) * 12 <= h); 911 } 912 913 if (playVideo) { 914 playVideo(mActivity, item.getPlayUri(), item.getName()); 915 } else if (viewPanorama) { 916 LightCycleHelper.viewPanorama(mActivity, item.getContentUri()); 917 } else if (unlock) { 918 mActivity.getStateManager().finishState(this); 919 } else { 920 toggleBars(); 921 } 922 } 923 924 @Override 925 public void lockOrientation() { 926 mHandler.sendEmptyMessage(MSG_LOCK_ORIENTATION); 927 } 928 929 @Override 930 public void unlockOrientation() { 931 mHandler.sendEmptyMessage(MSG_UNLOCK_ORIENTATION); 932 } 933 934 @Override 935 public void onActionBarAllowed(boolean allowed) { 936 mActionBarAllowed = allowed; 937 mHandler.sendEmptyMessage(MSG_UPDATE_ACTION_BAR); 938 } 939 940 @Override 941 public void onActionBarWanted() { 942 mHandler.sendEmptyMessage(MSG_WANT_BARS); 943 } 944 945 @Override 946 public void onFullScreenChanged(boolean full) { 947 Message m = mHandler.obtainMessage( 948 MSG_ON_FULL_SCREEN_CHANGED, full ? 1 : 0, 0); 949 m.sendToTarget(); 950 } 951 952 // How we do delete/undo: 953 // 954 // When the user choose to delete a media item, we just tell the 955 // FilterDeleteSet to hide that item. If the user choose to undo it, we 956 // again tell FilterDeleteSet not to hide it. If the user choose to commit 957 // the deletion, we then actually delete the media item. 958 @Override 959 public void onDeleteImage(Path path, int offset) { 960 onCommitDeleteImage(); // commit the previous deletion 961 mDeletePath = path; 962 mDeleteIsFocus = (offset == 0); 963 mMediaSet.addDeletion(path, mCurrentIndex + offset); 964 } 965 966 @Override 967 public void onUndoDeleteImage() { 968 if (mDeletePath == null) return; 969 // If the deletion was done on the focused item, we want the model to 970 // focus on it when it is undeleted. 971 if (mDeleteIsFocus) mModel.setFocusHintPath(mDeletePath); 972 mMediaSet.removeDeletion(mDeletePath); 973 mDeletePath = null; 974 } 975 976 @Override 977 public void onCommitDeleteImage() { 978 if (mDeletePath == null) return; 979 mSelectionManager.deSelectAll(); 980 mSelectionManager.toggle(mDeletePath); 981 mMenuExecutor.onMenuClicked(R.id.action_delete, null, true, false); 982 mDeletePath = null; 983 } 984 985 public static void playVideo(Activity activity, Uri uri, String title) { 986 try { 987 Intent intent = new Intent(Intent.ACTION_VIEW) 988 .setDataAndType(uri, "video/*") 989 .putExtra(Intent.EXTRA_TITLE, title) 990 .putExtra(MovieActivity.KEY_TREAT_UP_AS_BACK, true); 991 activity.startActivityForResult(intent, REQUEST_PLAY_VIDEO); 992 } catch (ActivityNotFoundException e) { 993 Toast.makeText(activity, activity.getString(R.string.video_err), 994 Toast.LENGTH_SHORT).show(); 995 } 996 } 997 998 private void setCurrentPhotoByIntent(Intent intent) { 999 if (intent == null) return; 1000 Path path = mApplication.getDataManager() 1001 .findPathByUri(intent.getData(), intent.getType()); 1002 if (path != null) { 1003 mModel.setCurrentPhoto(path, mCurrentIndex); 1004 } 1005 } 1006 1007 @Override 1008 protected void onStateResult(int requestCode, int resultCode, Intent data) { 1009 mHasActivityResult = true; 1010 switch (requestCode) { 1011 case REQUEST_EDIT: 1012 setCurrentPhotoByIntent(data); 1013 break; 1014 case REQUEST_CROP: 1015 if (resultCode == Activity.RESULT_OK) { 1016 setCurrentPhotoByIntent(data); 1017 } 1018 break; 1019 case REQUEST_CROP_PICASA: { 1020 if (resultCode == Activity.RESULT_OK) { 1021 Context context = mActivity.getAndroidContext(); 1022 String message = context.getString(R.string.crop_saved, 1023 context.getString(R.string.folder_download)); 1024 Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); 1025 } 1026 break; 1027 } 1028 case REQUEST_SLIDESHOW: { 1029 if (data == null) break; 1030 String path = data.getStringExtra(SlideshowPage.KEY_ITEM_PATH); 1031 int index = data.getIntExtra(SlideshowPage.KEY_PHOTO_INDEX, 0); 1032 if (path != null) { 1033 mModel.setCurrentPhoto(Path.fromString(path), index); 1034 } 1035 } 1036 } 1037 } 1038 1039 @Override 1040 protected void clearStateResult() { 1041 mHasActivityResult = false; 1042 } 1043 1044 private class PreparePhotoFallback implements OnGLIdleListener { 1045 private PhotoFallbackEffect mPhotoFallback = new PhotoFallbackEffect(); 1046 private boolean mResultReady = false; 1047 1048 public synchronized PhotoFallbackEffect get() { 1049 while (!mResultReady) { 1050 Utils.waitWithoutInterrupt(this); 1051 } 1052 return mPhotoFallback; 1053 } 1054 1055 @Override 1056 public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) { 1057 mPhotoFallback = mPhotoView.buildFallbackEffect(mRootPane, canvas); 1058 synchronized (this) { 1059 mResultReady = true; 1060 notifyAll(); 1061 } 1062 return false; 1063 } 1064 } 1065 1066 private void preparePhotoFallbackView() { 1067 GLRoot root = mActivity.getGLRoot(); 1068 PreparePhotoFallback task = new PreparePhotoFallback(); 1069 root.unlockRenderThread(); 1070 PhotoFallbackEffect anim; 1071 try { 1072 root.addOnGLIdleListener(task); 1073 anim = task.get(); 1074 } finally { 1075 root.lockRenderThread(); 1076 } 1077 mActivity.getTransitionStore().put( 1078 AlbumPage.KEY_RESUME_ANIMATION, anim); 1079 } 1080 1081 @Override 1082 public void onPause() { 1083 super.onPause(); 1084 mIsActive = false; 1085 1086 mActivity.getGLRoot().unfreeze(); 1087 mHandler.removeMessages(MSG_UNFREEZE_GLROOT); 1088 1089 DetailsHelper.pause(); 1090 if (mModel != null) { 1091 if (isFinishing()) preparePhotoFallbackView(); 1092 mModel.pause(); 1093 } 1094 mPhotoView.pause(); 1095 mHandler.removeMessages(MSG_HIDE_BARS); 1096 mActionBar.removeOnMenuVisibilityListener(mMenuVisibilityListener); 1097 1098 onCommitDeleteImage(); 1099 mMenuExecutor.pause(); 1100 if (mMediaSet != null) mMediaSet.clearDeletion(); 1101 } 1102 1103 @Override 1104 public void onCurrentImageUpdated() { 1105 mActivity.getGLRoot().unfreeze(); 1106 } 1107 1108 private void setGridButtonVisibility(boolean enabled) { 1109 Menu menu = mActionBar.getMenu(); 1110 if (menu == null) return; 1111 MenuItem item = menu.findItem(R.id.action_grid); 1112 if (item != null) item.setVisible((mSecureAlbum == null) && enabled); 1113 } 1114 1115 public void onFilmModeChanged(boolean enabled) { 1116 mHandler.sendEmptyMessage(MSG_REFRESH_GRID_BUTTON); 1117 mHandler.sendEmptyMessage(MSG_REFRESH_BOTTOM_CONTROLS); 1118 if (enabled) { 1119 mHandler.removeMessages(MSG_HIDE_BARS); 1120 } else { 1121 refreshHidingMessage(); 1122 } 1123 } 1124 1125 private void transitionFromAlbumPageIfNeeded() { 1126 TransitionStore transitions = mActivity.getTransitionStore(); 1127 1128 int albumPageTransition = transitions.get( 1129 KEY_ALBUMPAGE_TRANSITION, MSG_ALBUMPAGE_NONE); 1130 1131 if (albumPageTransition == MSG_ALBUMPAGE_NONE && mAppBridge != null) { 1132 // Generally, resuming the PhotoPage when in Camera should 1133 // reset to the capture mode to allow quick photo taking 1134 mCurrentIndex = 0; 1135 mPhotoView.resetToFirstPicture(); 1136 } else { 1137 int resumeIndex = transitions.get(KEY_INDEX_HINT, -1); 1138 if (resumeIndex >= 0) { 1139 if (mAppBridge != null) { 1140 // Account for live preview being the first item 1141 resumeIndex++; 1142 } 1143 if (resumeIndex < mMediaSet.getMediaItemCount()) { 1144 mCurrentIndex = resumeIndex; 1145 mModel.moveTo(mCurrentIndex); 1146 } 1147 } 1148 } 1149 1150 if (albumPageTransition == MSG_ALBUMPAGE_RESUMED) { 1151 mPhotoView.setFilmMode(mStartInFilmstrip || mAppBridge != null); 1152 } else if (albumPageTransition == MSG_ALBUMPAGE_PICKED) { 1153 mPhotoView.setFilmMode(false); 1154 } 1155 1156 mFadeOutTexture = transitions.get(PreparePageFadeoutTexture.KEY_FADE_TEXTURE); 1157 if (mFadeOutTexture != null) { 1158 mBackgroundFade.start(); 1159 BitmapScreenNail.disableDrawPlaceholder(); 1160 mOpenAnimationRect = 1161 albumPageTransition == MSG_ALBUMPAGE_NONE ? 1162 (Rect) mData.getParcelable(KEY_OPEN_ANIMATION_RECT) : 1163 (Rect) transitions.get(KEY_OPEN_ANIMATION_RECT); 1164 mPhotoView.setOpenAnimationRect(mOpenAnimationRect); 1165 mBackgroundFade.start(); 1166 } 1167 } 1168 1169 @Override 1170 protected void onResume() { 1171 super.onResume(); 1172 1173 if (mModel == null) { 1174 mActivity.getStateManager().finishState(this); 1175 return; 1176 } 1177 transitionFromAlbumPageIfNeeded(); 1178 1179 mActivity.getGLRoot().freeze(); 1180 mIsActive = true; 1181 setContentPane(mRootPane); 1182 1183 mModel.resume(); 1184 mPhotoView.resume(); 1185 mActionBar.setDisplayOptions( 1186 ((mSecureAlbum == null) && (mSetPathString != null)), true); 1187 mActionBar.addOnMenuVisibilityListener(mMenuVisibilityListener); 1188 if (!mShowBars) { 1189 mActionBar.hide(); 1190 mActivity.getGLRoot().setLightsOutMode(true); 1191 } 1192 1193 mHasActivityResult = false; 1194 mHandler.sendEmptyMessageDelayed(MSG_UNFREEZE_GLROOT, UNFREEZE_GLROOT_TIMEOUT); 1195 } 1196 1197 @Override 1198 protected void onDestroy() { 1199 if (mAppBridge != null) { 1200 mAppBridge.setServer(null); 1201 mScreenNailItem.setScreenNail(null); 1202 mAppBridge.detachScreenNail(); 1203 mAppBridge = null; 1204 mScreenNailSet = null; 1205 mScreenNailItem = null; 1206 } 1207 mOrientationManager.removeListener(this); 1208 mActivity.getGLRoot().setOrientationSource(null); 1209 if (mBottomControls != null) mBottomControls.cleanup(); 1210 1211 // Remove all pending messages. 1212 mHandler.removeCallbacksAndMessages(null); 1213 super.onDestroy(); 1214 } 1215 1216 private class MyDetailsSource implements DetailsSource { 1217 1218 @Override 1219 public MediaDetails getDetails() { 1220 return mModel.getMediaItem(0).getDetails(); 1221 } 1222 1223 @Override 1224 public int size() { 1225 return mMediaSet != null ? mMediaSet.getMediaItemCount() : 1; 1226 } 1227 1228 @Override 1229 public int setIndex() { 1230 return mModel.getCurrentIndex(); 1231 } 1232 } 1233} 1234