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