PhotoPage.java revision 014d9586d8145a33f4e99eaad570587f6d650026
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 @Override 243 public void onPhotoAvailable(long version, boolean fullImage) { 244 } 245 }); 246 } else { 247 // Get default media set by the URI 248 MediaItem mediaItem = (MediaItem) 249 mActivity.getDataManager().getMediaObject(itemPath); 250 mModel = new SinglePhotoDataAdapter(mActivity, mPhotoView, mediaItem); 251 mPhotoView.setModel(mModel); 252 updateCurrentPhoto(mediaItem); 253 } 254 255 mHandler = new SynchronizedHandler(mActivity.getGLRoot()) { 256 @Override 257 public void handleMessage(Message message) { 258 switch (message.what) { 259 case MSG_HIDE_BARS: { 260 hideBars(); 261 break; 262 } 263 default: throw new AssertionError(message.what); 264 } 265 } 266 }; 267 268 // start the opening animation only if it's not restored. 269 if (restoreState == null) { 270 mPhotoView.setOpenAnimationRect((Rect) data.getParcelable(KEY_OPEN_ANIMATION_RECT)); 271 } 272 } 273 274 private void updateShareURI(Path path) { 275 if (mShareActionProvider != null) { 276 DataManager manager = mActivity.getDataManager(); 277 int type = manager.getMediaType(path); 278 Intent intent = new Intent(Intent.ACTION_SEND); 279 intent.setType(MenuExecutor.getMimeType(type)); 280 intent.putExtra(Intent.EXTRA_STREAM, manager.getContentUri(path)); 281 mShareActionProvider.setShareIntent(intent); 282 if (mNfcAdapter != null) { 283 mNfcAdapter.setBeamPushUris(new Uri[]{manager.getContentUri(path)}, 284 (Activity)mActivity); 285 } 286 mPendingSharePath = null; 287 } else { 288 // This happens when ActionBar is not created yet. 289 mPendingSharePath = path; 290 } 291 } 292 293 private void updateCurrentPhoto(MediaItem photo) { 294 if (mCurrentPhoto == photo) return; 295 mCurrentPhoto = photo; 296 if (mCurrentPhoto == null) return; 297 updateMenuOperations(); 298 updateTitle(); 299 if (mShowDetails) { 300 mDetailsHelper.reloadDetails(mModel.getCurrentIndex()); 301 } 302 mPhotoView.showVideoPlayIcon( 303 photo.getMediaType() == MediaObject.MEDIA_TYPE_VIDEO); 304 305 if ((photo.getSupportedOperations() & MediaItem.SUPPORT_SHARE) != 0) { 306 updateShareURI(photo.getPath()); 307 } 308 } 309 310 private void updateTitle() { 311 if (mCurrentPhoto == null) return; 312 boolean showTitle = mActivity.getAndroidContext().getResources().getBoolean( 313 R.bool.show_action_bar_title); 314 if (showTitle && mCurrentPhoto.getName() != null) 315 mActionBar.setTitle(mCurrentPhoto.getName()); 316 else 317 mActionBar.setTitle(""); 318 } 319 320 private void updateMenuOperations() { 321 if (mMenu == null) return; 322 MenuItem item = mMenu.findItem(R.id.action_slideshow); 323 if (item != null) { 324 item.setVisible(canDoSlideShow()); 325 } 326 if (mCurrentPhoto == null) return; 327 int supportedOperations = mCurrentPhoto.getSupportedOperations(); 328 if (!GalleryUtils.isEditorAvailable((Context) mActivity, "image/*")) { 329 supportedOperations &= ~MediaObject.SUPPORT_EDIT; 330 } 331 332 MenuExecutor.updateMenuOperation(mMenu, supportedOperations); 333 } 334 335 private boolean canDoSlideShow() { 336 if (mMediaSet == null || mCurrentPhoto == null) { 337 return false; 338 } 339 if (mCurrentPhoto.getMediaType() != MediaObject.MEDIA_TYPE_IMAGE) { 340 return false; 341 } 342 if (mMediaSet instanceof MtpDevice) { 343 return false; 344 } 345 return true; 346 } 347 348 private void showBars() { 349 if (mShowBars) return; 350 mShowBars = true; 351 mActionBar.show(); 352 WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes(); 353 params.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE; 354 ((Activity) mActivity).getWindow().setAttributes(params); 355 } 356 357 private void hideBars() { 358 if (!mShowBars) return; 359 mShowBars = false; 360 mActionBar.hide(); 361 WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes(); 362 params.systemUiVisibility = View. SYSTEM_UI_FLAG_LOW_PROFILE; 363 ((Activity) mActivity).getWindow().setAttributes(params); 364 } 365 366 private void refreshHidingMessage() { 367 mHandler.removeMessages(MSG_HIDE_BARS); 368 if (!mIsMenuVisible && !mIsInteracting) { 369 mHandler.sendEmptyMessageDelayed(MSG_HIDE_BARS, HIDE_BARS_TIMEOUT); 370 } 371 } 372 373 @Override 374 public void onUserInteraction() { 375 showBars(); 376 refreshHidingMessage(); 377 } 378 379 public void onUserInteractionTap() { 380 if (mShowBars) { 381 hideBars(); 382 mHandler.removeMessages(MSG_HIDE_BARS); 383 } else { 384 showBars(); 385 refreshHidingMessage(); 386 } 387 } 388 389 @Override 390 public void onUserInteractionBegin() { 391 showBars(); 392 mIsInteracting = true; 393 refreshHidingMessage(); 394 } 395 396 @Override 397 public void onUserInteractionEnd() { 398 mIsInteracting = false; 399 400 // This function could be called from GL thread (in SlotView.render) 401 // and post to the main thread. So, it could be executed while the 402 // activity is paused. 403 if (mIsActive) refreshHidingMessage(); 404 } 405 406 @Override 407 protected void onBackPressed() { 408 if (mShowDetails) { 409 hideDetails(); 410 } else { 411 super.onBackPressed(); 412 } 413 } 414 415 @Override 416 protected boolean onCreateActionBar(Menu menu) { 417 MenuInflater inflater = ((Activity) mActivity).getMenuInflater(); 418 inflater.inflate(R.menu.photo, menu); 419 mShareActionProvider = GalleryActionBar.initializeShareActionProvider(menu); 420 if (mPendingSharePath != null) updateShareURI(mPendingSharePath); 421 mMenu = menu; 422 mShowBars = true; 423 updateMenuOperations(); 424 updateTitle(); 425 return true; 426 } 427 428 @Override 429 protected boolean onItemSelected(MenuItem item) { 430 MediaItem current = mModel.getCurrentMediaItem(); 431 432 if (current == null) { 433 // item is not ready, ignore 434 return true; 435 } 436 437 int currentIndex = mModel.getCurrentIndex(); 438 Path path = current.getPath(); 439 440 DataManager manager = mActivity.getDataManager(); 441 int action = item.getItemId(); 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_setas: 488 case R.id.action_confirm_delete: 489 case R.id.action_rotate_ccw: 490 case R.id.action_rotate_cw: 491 case R.id.action_show_on_map: 492 case R.id.action_edit: 493 mSelectionManager.deSelectAll(); 494 mSelectionManager.toggle(path); 495 mMenuExecutor.onMenuClicked(item, null); 496 return true; 497 case R.id.action_import: 498 mSelectionManager.deSelectAll(); 499 mSelectionManager.toggle(path); 500 mMenuExecutor.onMenuClicked(item, 501 new ImportCompleteListener(mActivity)); 502 return true; 503 default : 504 return false; 505 } 506 } 507 508 private void hideDetails() { 509 mShowDetails = false; 510 mDetailsHelper.hide(); 511 } 512 513 private void showDetails(int index) { 514 mShowDetails = true; 515 if (mDetailsHelper == null) { 516 mDetailsHelper = new DetailsHelper(mActivity, mRootPane, new MyDetailsSource()); 517 mDetailsHelper.setCloseListener(new CloseListener() { 518 public void onClose() { 519 hideDetails(); 520 } 521 }); 522 } 523 mDetailsHelper.reloadDetails(index); 524 mDetailsHelper.show(); 525 } 526 527 public void onSingleTapUp(int x, int y) { 528 if (mPageTapListener != null) { 529 if (mPageTapListener.onSingleTapUp(x, y)) return; 530 } 531 532 MediaItem item = mModel.getCurrentMediaItem(); 533 if (item == null) { 534 // item is not ready, ignore 535 return; 536 } 537 538 boolean playVideo = 539 (item.getSupportedOperations() & MediaItem.SUPPORT_PLAY) != 0; 540 541 if (playVideo) { 542 // determine if the point is at center (1/6) of the photo view. 543 // (The position of the "play" icon is at center (1/6) of the photo) 544 int w = mPhotoView.getWidth(); 545 int h = mPhotoView.getHeight(); 546 playVideo = (Math.abs(x - w / 2) * 12 <= w) 547 && (Math.abs(y - h / 2) * 12 <= h); 548 } 549 550 if (playVideo) { 551 playVideo((Activity) mActivity, item.getPlayUri(), item.getName()); 552 } else { 553 onUserInteractionTap(); 554 } 555 } 556 557 public static void playVideo(Activity activity, Uri uri, String title) { 558 try { 559 Intent intent = new Intent(Intent.ACTION_VIEW) 560 .setDataAndType(uri, "video/*"); 561 intent.putExtra(Intent.EXTRA_TITLE, title); 562 activity.startActivity(intent); 563 } catch (ActivityNotFoundException e) { 564 Toast.makeText(activity, activity.getString(R.string.video_err), 565 Toast.LENGTH_SHORT).show(); 566 } 567 } 568 569 @Override 570 protected void onStateResult(int requestCode, int resultCode, Intent data) { 571 switch (requestCode) { 572 case REQUEST_CROP: 573 if (resultCode == Activity.RESULT_OK) { 574 if (data == null) break; 575 Path path = mApplication 576 .getDataManager().findPathByUri(data.getData()); 577 if (path != null) { 578 mModel.setCurrentPhoto(path, mCurrentIndex); 579 } 580 } 581 break; 582 case REQUEST_CROP_PICASA: { 583 int message = resultCode == Activity.RESULT_OK 584 ? R.string.crop_saved 585 : R.string.crop_not_saved; 586 Toast.makeText(mActivity.getAndroidContext(), 587 message, Toast.LENGTH_SHORT).show(); 588 break; 589 } 590 case REQUEST_SLIDESHOW: { 591 if (data == null) break; 592 String path = data.getStringExtra(SlideshowPage.KEY_ITEM_PATH); 593 int index = data.getIntExtra(SlideshowPage.KEY_PHOTO_INDEX, 0); 594 if (path != null) { 595 mModel.setCurrentPhoto(Path.fromString(path), index); 596 } 597 } 598 } 599 } 600 601 @Override 602 public void onPause() { 603 super.onPause(); 604 mIsActive = false; 605 DetailsHelper.pause(); 606 mPhotoView.pause(); 607 mModel.pause(); 608 mHandler.removeMessages(MSG_HIDE_BARS); 609 mActionBar.removeOnMenuVisibilityListener(mMenuVisibilityListener); 610 611 mMenuExecutor.pause(); 612 } 613 614 @Override 615 protected void onResume() { 616 super.onResume(); 617 mIsActive = true; 618 setContentPane(mRootPane); 619 620 mModel.resume(); 621 mPhotoView.resume(); 622 if (mMenuVisibilityListener == null) { 623 mMenuVisibilityListener = new MyMenuVisibilityListener(); 624 } 625 mActionBar.setDisplayOptions(mSetPathString != null, true); 626 mActionBar.addOnMenuVisibilityListener(mMenuVisibilityListener); 627 628 onUserInteraction(); 629 } 630 631 @Override 632 protected void onDestroy() { 633 if (mScreenNailHolder != null) { 634 // Unregister the ScreenNail and notify mScreenNailHolder. 635 SnailSource.unregisterScreenNail(mScreenNail); 636 mScreenNailHolder.detach(); 637 mScreenNailHolder = null; 638 mScreenNail = null; 639 } 640 super.onDestroy(); 641 } 642 643 private class MyDetailsSource implements DetailsSource { 644 private int mIndex; 645 646 @Override 647 public MediaDetails getDetails() { 648 return mModel.getCurrentMediaItem().getDetails(); 649 } 650 651 @Override 652 public int size() { 653 return mMediaSet != null ? mMediaSet.getMediaItemCount() : 1; 654 } 655 656 @Override 657 public int findIndex(int indexHint) { 658 mIndex = indexHint; 659 return indexHint; 660 } 661 662 @Override 663 public int getIndex() { 664 return mIndex; 665 } 666 } 667 668} 669