PhotoPage.java revision 736cf74b170d32374c8b35dbe6a46d975258be87
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.app.ActionBar.OnMenuVisibilityListener; 20import android.app.Activity; 21import android.content.ActivityNotFoundException; 22import android.content.Context; 23import android.content.Intent; 24import android.graphics.Rect; 25import android.net.Uri; 26import android.nfc.NfcAdapter; 27import android.os.Bundle; 28import android.os.Handler; 29import android.os.Message; 30import android.view.Menu; 31import android.view.MenuInflater; 32import android.view.MenuItem; 33import android.view.View; 34import android.view.WindowManager; 35import android.widget.ShareActionProvider; 36import android.widget.Toast; 37 38import com.android.gallery3d.R; 39import com.android.gallery3d.data.DataManager; 40import com.android.gallery3d.data.MediaDetails; 41import com.android.gallery3d.data.MediaItem; 42import com.android.gallery3d.data.MediaObject; 43import com.android.gallery3d.data.MediaSet; 44import com.android.gallery3d.data.MtpDevice; 45import com.android.gallery3d.data.Path; 46import com.android.gallery3d.data.SnailSource; 47import com.android.gallery3d.picasasource.PicasaSource; 48import com.android.gallery3d.ui.DetailsHelper; 49import com.android.gallery3d.ui.DetailsHelper.CloseListener; 50import com.android.gallery3d.ui.DetailsHelper.DetailsSource; 51import com.android.gallery3d.ui.GLCanvas; 52import com.android.gallery3d.ui.GLView; 53import com.android.gallery3d.ui.ImportCompleteListener; 54import com.android.gallery3d.ui.MenuExecutor; 55import com.android.gallery3d.ui.PhotoView; 56import com.android.gallery3d.ui.ScreenNail; 57import com.android.gallery3d.ui.ScreenNailHolder; 58import com.android.gallery3d.ui.SelectionManager; 59import com.android.gallery3d.ui.SynchronizedHandler; 60import com.android.gallery3d.ui.UserInteractionListener; 61import com.android.gallery3d.util.GalleryUtils; 62 63public class PhotoPage extends ActivityState 64 implements PhotoView.PhotoTapListener, UserInteractionListener { 65 private static final String TAG = "PhotoPage"; 66 67 private static final int MSG_HIDE_BARS = 1; 68 69 private static final int HIDE_BARS_TIMEOUT = 3500; 70 71 private static final int REQUEST_SLIDESHOW = 1; 72 private static final int REQUEST_CROP = 2; 73 private static final int REQUEST_CROP_PICASA = 3; 74 75 public static final String KEY_MEDIA_SET_PATH = "media-set-path"; 76 public static final String KEY_MEDIA_ITEM_PATH = "media-item-path"; 77 public static final String KEY_INDEX_HINT = "index-hint"; 78 public static final String KEY_OPEN_ANIMATION_RECT = "open-animation-rect"; 79 public static final String KEY_SCREENNAIL_HOLDER = "screennail-holder"; 80 81 private GalleryApp mApplication; 82 private SelectionManager mSelectionManager; 83 84 private PhotoView mPhotoView; 85 private PhotoPage.Model mModel; 86 private DetailsHelper mDetailsHelper; 87 private boolean mShowDetails; 88 private Path mPendingSharePath; 89 private Path mScreenNailItemPath; 90 91 // mMediaSet could be null if there is no KEY_MEDIA_SET_PATH supplied. 92 // E.g., viewing a photo in gmail attachment 93 private MediaSet mMediaSet; 94 private Menu mMenu; 95 96 private final Intent mResultIntent = new Intent(); 97 private int mCurrentIndex = 0; 98 private Handler mHandler; 99 private boolean mShowBars = true; 100 private GalleryActionBar mActionBar; 101 private MyMenuVisibilityListener mMenuVisibilityListener; 102 private PageTapListener mPageTapListener; 103 private boolean mIsMenuVisible; 104 private boolean mIsInteracting; 105 private MediaItem mCurrentPhoto = null; 106 private MenuExecutor mMenuExecutor; 107 private boolean mIsActive; 108 private ShareActionProvider mShareActionProvider; 109 private String mSetPathString; 110 private ScreenNailHolder mScreenNailHolder; 111 private ScreenNail mScreenNail; 112 113 private NfcAdapter mNfcAdapter; 114 115 public static interface Model extends PhotoView.Model { 116 public void resume(); 117 public void pause(); 118 public boolean isEmpty(); 119 public MediaItem getCurrentMediaItem(); 120 public int getCurrentIndex(); 121 public void setCurrentPhoto(Path path, int indexHint); 122 } 123 124 private class MyMenuVisibilityListener implements OnMenuVisibilityListener { 125 public void onMenuVisibilityChanged(boolean isVisible) { 126 mIsMenuVisible = isVisible; 127 refreshHidingMessage(); 128 } 129 } 130 131 public interface PageTapListener { 132 // Return true if the tap is consumed. 133 public boolean onSingleTapUp(int x, int y); 134 } 135 136 public void setPageTapListener(PageTapListener listener) { 137 mPageTapListener = listener; 138 } 139 140 private final GLView mRootPane = new GLView() { 141 142 @Override 143 protected void renderBackground(GLCanvas view) { 144 view.clearBuffer(); 145 } 146 147 @Override 148 protected void onLayout( 149 boolean changed, int left, int top, int right, int bottom) { 150 mPhotoView.layout(0, 0, right - left, bottom - top); 151 if (mShowDetails) { 152 mDetailsHelper.layout(left, mActionBar.getHeight(), right, bottom); 153 } 154 } 155 }; 156 157 @Override 158 public void onCreate(Bundle data, Bundle restoreState) { 159 mActionBar = mActivity.getGalleryActionBar(); 160 mSelectionManager = new SelectionManager(mActivity, false); 161 mMenuExecutor = new MenuExecutor(mActivity, mSelectionManager); 162 163 mPhotoView = new PhotoView(mActivity); 164 mPhotoView.setPhotoTapListener(this); 165 mRootPane.addComponent(mPhotoView); 166 mApplication = (GalleryApp)((Activity) mActivity).getApplication(); 167 168 mSetPathString = data.getString(KEY_MEDIA_SET_PATH); 169 mNfcAdapter = NfcAdapter.getDefaultAdapter(mActivity.getAndroidContext()); 170 Path itemPath = Path.fromString(data.getString(KEY_MEDIA_ITEM_PATH)); 171 172 if (mSetPathString != null) { 173 mScreenNailHolder = 174 (ScreenNailHolder) data.getParcelable(KEY_SCREENNAIL_HOLDER); 175 if (mScreenNailHolder != null) { 176 mScreenNail = mScreenNailHolder.attach(); 177 178 // Get the ScreenNail from ScreenNailHolder and register it. 179 int id = SnailSource.registerScreenNail(mScreenNail); 180 Path screenNailSetPath = SnailSource.getSetPath(id); 181 mScreenNailItemPath = SnailSource.getItemPath(id); 182 183 // Combine the original MediaSet with the one for CameraScreenNail. 184 mSetPathString = "/combo/item/{" + screenNailSetPath + 185 "," + mSetPathString + "}"; 186 187 // Start from the screen nail. 188 itemPath = mScreenNailItemPath; 189 } 190 191 mMediaSet = mActivity.getDataManager().getMediaSet(mSetPathString); 192 mCurrentIndex = data.getInt(KEY_INDEX_HINT, 0); 193 mMediaSet = (MediaSet) 194 mActivity.getDataManager().getMediaObject(mSetPathString); 195 if (mMediaSet == null) { 196 Log.w(TAG, "failed to restore " + mSetPathString); 197 } 198 PhotoDataAdapter pda = new PhotoDataAdapter( 199 mActivity, mPhotoView, mMediaSet, itemPath, mCurrentIndex); 200 mModel = pda; 201 mPhotoView.setModel(mModel); 202 203 mResultIntent.putExtra(KEY_INDEX_HINT, mCurrentIndex); 204 setStateResult(Activity.RESULT_OK, mResultIntent); 205 206 pda.setDataListener(new PhotoDataAdapter.DataListener() { 207 208 @Override 209 public void onPhotoChanged(int index, Path item) { 210 mCurrentIndex = index; 211 mResultIntent.putExtra(KEY_INDEX_HINT, index); 212 if (item != null) { 213 mResultIntent.putExtra(KEY_MEDIA_ITEM_PATH, item.toString()); 214 MediaItem photo = mModel.getCurrentMediaItem(); 215 if (photo != null) updateCurrentPhoto(photo); 216 } else { 217 mResultIntent.removeExtra(KEY_MEDIA_ITEM_PATH); 218 } 219 setStateResult(Activity.RESULT_OK, mResultIntent); 220 } 221 222 @Override 223 public void onLoadingFinished() { 224 GalleryUtils.setSpinnerVisibility((Activity) mActivity, false); 225 if (!mModel.isEmpty()) { 226 MediaItem photo = mModel.getCurrentMediaItem(); 227 if (photo != null) updateCurrentPhoto(photo); 228 } else if (mIsActive) { 229 mActivity.getStateManager().finishState(PhotoPage.this); 230 } 231 } 232 233 @Override 234 public void onLoadingStarted() { 235 GalleryUtils.setSpinnerVisibility((Activity) mActivity, true); 236 } 237 }); 238 } else { 239 // Get default media set by the URI 240 MediaItem mediaItem = (MediaItem) 241 mActivity.getDataManager().getMediaObject(itemPath); 242 mModel = new SinglePhotoDataAdapter(mActivity, mPhotoView, mediaItem); 243 mPhotoView.setModel(mModel); 244 updateCurrentPhoto(mediaItem); 245 } 246 247 mHandler = new SynchronizedHandler(mActivity.getGLRoot()) { 248 @Override 249 public void handleMessage(Message message) { 250 switch (message.what) { 251 case MSG_HIDE_BARS: { 252 hideBars(); 253 break; 254 } 255 default: throw new AssertionError(message.what); 256 } 257 } 258 }; 259 260 // start the opening animation only if it's not restored. 261 if (restoreState == null) { 262 mPhotoView.setOpenAnimationRect((Rect) data.getParcelable(KEY_OPEN_ANIMATION_RECT)); 263 } 264 } 265 266 private void updateShareURI(Path path) { 267 if (mShareActionProvider != null) { 268 DataManager manager = mActivity.getDataManager(); 269 int type = manager.getMediaType(path); 270 Intent intent = new Intent(Intent.ACTION_SEND); 271 intent.setType(MenuExecutor.getMimeType(type)); 272 intent.putExtra(Intent.EXTRA_STREAM, manager.getContentUri(path)); 273 mShareActionProvider.setShareIntent(intent); 274 if (mNfcAdapter != null) { 275 mNfcAdapter.setBeamPushUris(new Uri[]{manager.getContentUri(path)}, 276 (Activity)mActivity); 277 } 278 mPendingSharePath = null; 279 } else { 280 // This happens when ActionBar is not created yet. 281 mPendingSharePath = path; 282 } 283 } 284 285 private void updateCurrentPhoto(MediaItem photo) { 286 if (mCurrentPhoto == photo) return; 287 mCurrentPhoto = photo; 288 if (mCurrentPhoto == null) return; 289 updateMenuOperations(); 290 // Hide the action bar when going back to camera preview. 291 if (photo.getPath() == mScreenNailItemPath) hideBars(); 292 updateTitle(); 293 if (mShowDetails) { 294 mDetailsHelper.reloadDetails(mModel.getCurrentIndex()); 295 } 296 mPhotoView.showVideoPlayIcon( 297 photo.getMediaType() == MediaObject.MEDIA_TYPE_VIDEO); 298 299 if ((photo.getSupportedOperations() & MediaItem.SUPPORT_SHARE) != 0) { 300 updateShareURI(photo.getPath()); 301 } 302 } 303 304 private void updateTitle() { 305 if (mCurrentPhoto == null) return; 306 boolean showTitle = mActivity.getAndroidContext().getResources().getBoolean( 307 R.bool.show_action_bar_title); 308 if (showTitle && mCurrentPhoto.getName() != null) 309 mActionBar.setTitle(mCurrentPhoto.getName()); 310 else 311 mActionBar.setTitle(""); 312 } 313 314 private void updateMenuOperations() { 315 if (mMenu == null) return; 316 MenuItem item = mMenu.findItem(R.id.action_slideshow); 317 if (item != null) { 318 item.setVisible(canDoSlideShow()); 319 } 320 if (mCurrentPhoto == null) return; 321 int supportedOperations = mCurrentPhoto.getSupportedOperations(); 322 if (!GalleryUtils.isEditorAvailable((Context) mActivity, "image/*")) { 323 supportedOperations &= ~MediaObject.SUPPORT_EDIT; 324 } 325 326 MenuExecutor.updateMenuOperation(mMenu, supportedOperations); 327 } 328 329 private boolean canDoSlideShow() { 330 if (mMediaSet == null || mCurrentPhoto == null) { 331 return false; 332 } 333 if (mCurrentPhoto.getMediaType() != MediaObject.MEDIA_TYPE_IMAGE) { 334 return false; 335 } 336 if (mMediaSet instanceof MtpDevice) { 337 return false; 338 } 339 return true; 340 } 341 342 private void showBars() { 343 if (mShowBars) return; 344 mShowBars = true; 345 mActionBar.show(); 346 WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes(); 347 params.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE; 348 ((Activity) mActivity).getWindow().setAttributes(params); 349 } 350 351 private void hideBars() { 352 if (!mShowBars) return; 353 mShowBars = false; 354 mActionBar.hide(); 355 WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes(); 356 params.systemUiVisibility = View. SYSTEM_UI_FLAG_LOW_PROFILE; 357 ((Activity) mActivity).getWindow().setAttributes(params); 358 } 359 360 private void refreshHidingMessage() { 361 mHandler.removeMessages(MSG_HIDE_BARS); 362 if (!mIsMenuVisible && !mIsInteracting) { 363 mHandler.sendEmptyMessageDelayed(MSG_HIDE_BARS, HIDE_BARS_TIMEOUT); 364 } 365 } 366 367 @Override 368 public void onUserInteraction() { 369 showBars(); 370 refreshHidingMessage(); 371 } 372 373 public void onUserInteractionTap() { 374 if (mShowBars) { 375 hideBars(); 376 mHandler.removeMessages(MSG_HIDE_BARS); 377 } else { 378 showBars(); 379 refreshHidingMessage(); 380 } 381 } 382 383 @Override 384 public void onUserInteractionBegin() { 385 showBars(); 386 mIsInteracting = true; 387 refreshHidingMessage(); 388 } 389 390 @Override 391 public void onUserInteractionEnd() { 392 mIsInteracting = false; 393 394 // This function could be called from GL thread (in SlotView.render) 395 // and post to the main thread. So, it could be executed while the 396 // activity is paused. 397 if (mIsActive) refreshHidingMessage(); 398 } 399 400 @Override 401 protected void onBackPressed() { 402 if (mShowDetails) { 403 hideDetails(); 404 } else if (mScreenNail == null 405 || !switchWithCaptureAnimation(-1)) { 406 super.onBackPressed(); 407 } 408 } 409 410 // Switch to the previous or next picture using the capture animation. 411 // The offset is -1 to switch to the previous picture, 1 to switch to 412 // the next picture. 413 public boolean switchWithCaptureAnimation(int offset) { 414 return mPhotoView.switchWithCaptureAnimation(offset); 415 } 416 417 @Override 418 protected boolean onCreateActionBar(Menu menu) { 419 MenuInflater inflater = ((Activity) mActivity).getMenuInflater(); 420 inflater.inflate(R.menu.photo, menu); 421 mShareActionProvider = GalleryActionBar.initializeShareActionProvider(menu); 422 if (mPendingSharePath != null) updateShareURI(mPendingSharePath); 423 mMenu = menu; 424 mShowBars = true; 425 updateMenuOperations(); 426 updateTitle(); 427 return true; 428 } 429 430 @Override 431 protected boolean onItemSelected(MenuItem item) { 432 MediaItem current = mModel.getCurrentMediaItem(); 433 434 if (current == null) { 435 // item is not ready, ignore 436 return true; 437 } 438 439 int currentIndex = mModel.getCurrentIndex(); 440 Path path = current.getPath(); 441 442 DataManager manager = mActivity.getDataManager(); 443 int action = item.getItemId(); 444 boolean needsConfirm = false; 445 switch (action) { 446 case android.R.id.home: { 447 if (mSetPathString != null) { 448 if (mActivity.getStateManager().getStateCount() > 1) { 449 onBackPressed(); 450 } else { 451 // We're in view mode so set up the stacks on our own. 452 Bundle data = new Bundle(getData()); 453 data.putString(AlbumPage.KEY_MEDIA_PATH, mSetPathString); 454 data.putString(AlbumPage.KEY_PARENT_MEDIA_PATH, 455 mActivity.getDataManager().getTopSetPath( 456 DataManager.INCLUDE_ALL)); 457 mActivity.getStateManager().switchState(this, AlbumPage.class, data); 458 } 459 } 460 return true; 461 } 462 case R.id.action_slideshow: { 463 Bundle data = new Bundle(); 464 data.putString(SlideshowPage.KEY_SET_PATH, mMediaSet.getPath().toString()); 465 data.putString(SlideshowPage.KEY_ITEM_PATH, path.toString()); 466 data.putInt(SlideshowPage.KEY_PHOTO_INDEX, currentIndex); 467 data.putBoolean(SlideshowPage.KEY_REPEAT, true); 468 mActivity.getStateManager().startStateForResult( 469 SlideshowPage.class, REQUEST_SLIDESHOW, data); 470 return true; 471 } 472 case R.id.action_crop: { 473 Activity activity = (Activity) mActivity; 474 Intent intent = new Intent(CropImage.CROP_ACTION); 475 intent.setClass(activity, CropImage.class); 476 intent.setData(manager.getContentUri(path)); 477 activity.startActivityForResult(intent, PicasaSource.isPicasaImage(current) 478 ? REQUEST_CROP_PICASA 479 : REQUEST_CROP); 480 return true; 481 } 482 case R.id.action_details: { 483 if (mShowDetails) { 484 hideDetails(); 485 } else { 486 showDetails(currentIndex); 487 } 488 return true; 489 } 490 case R.id.action_delete: 491 needsConfirm = true; 492 case R.id.action_setas: 493 case R.id.action_rotate_ccw: 494 case R.id.action_rotate_cw: 495 case R.id.action_show_on_map: 496 case R.id.action_edit: 497 mSelectionManager.deSelectAll(); 498 mSelectionManager.toggle(path); 499 mMenuExecutor.onMenuClicked(item, needsConfirm, null); 500 return true; 501 case R.id.action_import: 502 mSelectionManager.deSelectAll(); 503 mSelectionManager.toggle(path); 504 mMenuExecutor.onMenuClicked(item, needsConfirm, 505 new ImportCompleteListener(mActivity)); 506 return true; 507 default : 508 return false; 509 } 510 } 511 512 private void hideDetails() { 513 mShowDetails = false; 514 mDetailsHelper.hide(); 515 } 516 517 private void showDetails(int index) { 518 mShowDetails = true; 519 if (mDetailsHelper == null) { 520 mDetailsHelper = new DetailsHelper(mActivity, mRootPane, new MyDetailsSource()); 521 mDetailsHelper.setCloseListener(new CloseListener() { 522 public void onClose() { 523 hideDetails(); 524 } 525 }); 526 } 527 mDetailsHelper.reloadDetails(index); 528 mDetailsHelper.show(); 529 } 530 531 public void onSingleTapUp(int x, int y) { 532 if (mPageTapListener != null) { 533 if (mPageTapListener.onSingleTapUp(x, y)) return; 534 } 535 536 MediaItem item = mModel.getCurrentMediaItem(); 537 if (item == null) { 538 // item is not ready, ignore 539 return; 540 } 541 542 boolean playVideo = 543 (item.getSupportedOperations() & MediaItem.SUPPORT_PLAY) != 0; 544 545 if (playVideo) { 546 // determine if the point is at center (1/6) of the photo view. 547 // (The position of the "play" icon is at center (1/6) of the photo) 548 int w = mPhotoView.getWidth(); 549 int h = mPhotoView.getHeight(); 550 playVideo = (Math.abs(x - w / 2) * 12 <= w) 551 && (Math.abs(y - h / 2) * 12 <= h); 552 } 553 554 if (playVideo) { 555 playVideo((Activity) mActivity, item.getPlayUri(), item.getName()); 556 } else { 557 onUserInteractionTap(); 558 } 559 } 560 561 public static void playVideo(Activity activity, Uri uri, String title) { 562 try { 563 Intent intent = new Intent(Intent.ACTION_VIEW) 564 .setDataAndType(uri, "video/*"); 565 intent.putExtra(Intent.EXTRA_TITLE, title); 566 activity.startActivity(intent); 567 } catch (ActivityNotFoundException e) { 568 Toast.makeText(activity, activity.getString(R.string.video_err), 569 Toast.LENGTH_SHORT).show(); 570 } 571 } 572 573 @Override 574 protected void onStateResult(int requestCode, int resultCode, Intent data) { 575 switch (requestCode) { 576 case REQUEST_CROP: 577 if (resultCode == Activity.RESULT_OK) { 578 if (data == null) break; 579 Path path = mApplication.getDataManager() 580 .findPathByUri(data.getData(), data.getType()); 581 if (path != null) { 582 mModel.setCurrentPhoto(path, mCurrentIndex); 583 } 584 } 585 break; 586 case REQUEST_CROP_PICASA: { 587 int message = resultCode == Activity.RESULT_OK 588 ? R.string.crop_saved 589 : R.string.crop_not_saved; 590 Toast.makeText(mActivity.getAndroidContext(), 591 message, Toast.LENGTH_SHORT).show(); 592 break; 593 } 594 case REQUEST_SLIDESHOW: { 595 if (data == null) break; 596 String path = data.getStringExtra(SlideshowPage.KEY_ITEM_PATH); 597 int index = data.getIntExtra(SlideshowPage.KEY_PHOTO_INDEX, 0); 598 if (path != null) { 599 mModel.setCurrentPhoto(Path.fromString(path), index); 600 } 601 } 602 } 603 } 604 605 @Override 606 public void onPause() { 607 super.onPause(); 608 mIsActive = false; 609 DetailsHelper.pause(); 610 mPhotoView.pause(); 611 mModel.pause(); 612 mHandler.removeMessages(MSG_HIDE_BARS); 613 mActionBar.removeOnMenuVisibilityListener(mMenuVisibilityListener); 614 615 mMenuExecutor.pause(); 616 } 617 618 @Override 619 protected void onResume() { 620 super.onResume(); 621 mIsActive = true; 622 setContentPane(mRootPane); 623 624 mModel.resume(); 625 mPhotoView.resume(); 626 if (mMenuVisibilityListener == null) { 627 mMenuVisibilityListener = new MyMenuVisibilityListener(); 628 } 629 mActionBar.setDisplayOptions(mSetPathString != null, true); 630 mActionBar.addOnMenuVisibilityListener(mMenuVisibilityListener); 631 632 onUserInteraction(); 633 } 634 635 @Override 636 protected void onDestroy() { 637 if (mScreenNailHolder != null) { 638 // Unregister the ScreenNail and notify mScreenNailHolder. 639 SnailSource.unregisterScreenNail(mScreenNail); 640 mScreenNailHolder.detach(); 641 mScreenNailHolder = null; 642 mScreenNail = null; 643 } 644 super.onDestroy(); 645 } 646 647 private class MyDetailsSource implements DetailsSource { 648 private int mIndex; 649 650 @Override 651 public MediaDetails getDetails() { 652 return mModel.getCurrentMediaItem().getDetails(); 653 } 654 655 @Override 656 public int size() { 657 return mMediaSet != null ? mMediaSet.getMediaItemCount() : 1; 658 } 659 660 @Override 661 public int findIndex(int indexHint) { 662 mIndex = indexHint; 663 return indexHint; 664 } 665 666 @Override 667 public int getIndex() { 668 return mIndex; 669 } 670 } 671 672} 673