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