PhotoPage.java revision 8cab3e872dd95e55ba34fdb94269a0c5069e72ae
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.net.Uri; 26import android.os.Bundle; 27import android.os.Handler; 28import android.os.Message; 29import android.view.Menu; 30import android.view.MenuInflater; 31import android.view.MenuItem; 32import android.view.View; 33import android.view.View.MeasureSpec; 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.picasasource.PicasaSource; 47import com.android.gallery3d.ui.DetailsHelper; 48import com.android.gallery3d.ui.DetailsHelper.CloseListener; 49import com.android.gallery3d.ui.DetailsHelper.DetailsSource; 50import com.android.gallery3d.ui.FilmStripView; 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.PositionRepository; 57import com.android.gallery3d.ui.PositionRepository.Position; 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, FilmStripView.Listener, 65 UserInteractionListener { 66 private static final String TAG = "PhotoPage"; 67 68 private static final int MSG_HIDE_BARS = 1; 69 70 private static final int HIDE_BARS_TIMEOUT = 3500; 71 72 private static final int REQUEST_SLIDESHOW = 1; 73 private static final int REQUEST_CROP = 2; 74 private static final int REQUEST_CROP_PICASA = 3; 75 76 public static final String KEY_MEDIA_SET_PATH = "media-set-path"; 77 public static final String KEY_MEDIA_ITEM_PATH = "media-item-path"; 78 public static final String KEY_INDEX_HINT = "index-hint"; 79 80 private GalleryApp mApplication; 81 private SelectionManager mSelectionManager; 82 83 private PhotoView mPhotoView; 84 private PhotoPage.Model mModel; 85 private FilmStripView mFilmStripView; 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 boolean mIsMenuVisible; 102 private boolean mIsInteracting; 103 private MediaItem mCurrentPhoto = null; 104 private MenuExecutor mMenuExecutor; 105 private boolean mIsActive; 106 private ShareActionProvider mShareActionProvider; 107 private String mSetPathString; 108 109 public static interface Model extends PhotoView.Model { 110 public void resume(); 111 public void pause(); 112 public boolean isEmpty(); 113 public MediaItem getCurrentMediaItem(); 114 public int getCurrentIndex(); 115 public void setCurrentPhoto(Path path, int indexHint); 116 } 117 118 private class MyMenuVisibilityListener implements OnMenuVisibilityListener { 119 public void onMenuVisibilityChanged(boolean isVisible) { 120 mIsMenuVisible = isVisible; 121 refreshHidingMessage(); 122 } 123 } 124 125 private final GLView mRootPane = new GLView() { 126 127 @Override 128 protected void renderBackground(GLCanvas view) { 129 view.clearBuffer(); 130 } 131 132 @Override 133 protected void onLayout( 134 boolean changed, int left, int top, int right, int bottom) { 135 mPhotoView.layout(0, 0, right - left, bottom - top); 136 PositionRepository.getInstance(mActivity).setOffset(0, 0); 137 int filmStripHeight = 0; 138 if (mFilmStripView != null) { 139 mFilmStripView.measure( 140 MeasureSpec.makeMeasureSpec(right - left, MeasureSpec.EXACTLY), 141 MeasureSpec.UNSPECIFIED); 142 filmStripHeight = mFilmStripView.getMeasuredHeight(); 143 mFilmStripView.layout(0, bottom - top - filmStripHeight, 144 right - left, bottom - top); 145 } 146 if (mShowDetails) { 147 mDetailsHelper.layout(left, mActionBar.getHeight(), right, bottom); 148 } 149 } 150 }; 151 152 private void initFilmStripView() { 153 Config.PhotoPage config = Config.PhotoPage.get((Context) mActivity); 154 mFilmStripView = new FilmStripView(mActivity, mMediaSet, 155 config.filmstripTopMargin, config.filmstripMidMargin, config.filmstripBottomMargin, 156 config.filmstripContentSize, config.filmstripThumbSize, config.filmstripBarSize, 157 config.filmstripGripSize, config.filmstripGripWidth); 158 mRootPane.addComponent(mFilmStripView); 159 mFilmStripView.setListener(this); 160 mFilmStripView.setUserInteractionListener(this); 161 mFilmStripView.setFocusIndex(mCurrentIndex); 162 mFilmStripView.setStartIndex(mCurrentIndex); 163 mRootPane.requestLayout(); 164 if (mIsActive) mFilmStripView.resume(); 165 if (!mShowBars) mFilmStripView.setVisibility(GLView.INVISIBLE); 166 } 167 168 @Override 169 public void onCreate(Bundle data, Bundle restoreState) { 170 mActionBar = mActivity.getGalleryActionBar(); 171 mSelectionManager = new SelectionManager(mActivity, false); 172 mMenuExecutor = new MenuExecutor(mActivity, mSelectionManager); 173 174 mPhotoView = new PhotoView(mActivity); 175 mPhotoView.setPhotoTapListener(this); 176 mRootPane.addComponent(mPhotoView); 177 mApplication = (GalleryApp)((Activity) mActivity).getApplication(); 178 179 mSetPathString = data.getString(KEY_MEDIA_SET_PATH); 180 Path itemPath = Path.fromString(data.getString(KEY_MEDIA_ITEM_PATH)); 181 182 if (mSetPathString != null) { 183 mMediaSet = mActivity.getDataManager().getMediaSet(mSetPathString); 184 mCurrentIndex = data.getInt(KEY_INDEX_HINT, 0); 185 mMediaSet = (MediaSet) 186 mActivity.getDataManager().getMediaObject(mSetPathString); 187 if (mMediaSet == null) { 188 Log.w(TAG, "failed to restore " + mSetPathString); 189 } 190 PhotoDataAdapter pda = new PhotoDataAdapter( 191 mActivity, mPhotoView, mMediaSet, itemPath, mCurrentIndex); 192 mModel = pda; 193 mPhotoView.setModel(mModel); 194 195 mResultIntent.putExtra(KEY_INDEX_HINT, mCurrentIndex); 196 setStateResult(Activity.RESULT_OK, mResultIntent); 197 198 pda.setDataListener(new PhotoDataAdapter.DataListener() { 199 200 @Override 201 public void onPhotoChanged(int index, Path item) { 202 if (mFilmStripView != null) mFilmStripView.setFocusIndex(index); 203 mCurrentIndex = index; 204 mResultIntent.putExtra(KEY_INDEX_HINT, index); 205 if (item != null) { 206 mResultIntent.putExtra(KEY_MEDIA_ITEM_PATH, item.toString()); 207 MediaItem photo = mModel.getCurrentMediaItem(); 208 if (photo != null) updateCurrentPhoto(photo); 209 } else { 210 mResultIntent.removeExtra(KEY_MEDIA_ITEM_PATH); 211 } 212 setStateResult(Activity.RESULT_OK, mResultIntent); 213 } 214 215 @Override 216 public void onLoadingFinished() { 217 GalleryUtils.setSpinnerVisibility((Activity) mActivity, false); 218 if (!mModel.isEmpty()) { 219 MediaItem photo = mModel.getCurrentMediaItem(); 220 if (photo != null) updateCurrentPhoto(photo); 221 } else if (mIsActive) { 222 mActivity.getStateManager().finishState(PhotoPage.this); 223 } 224 } 225 226 @Override 227 public void onLoadingStarted() { 228 GalleryUtils.setSpinnerVisibility((Activity) mActivity, true); 229 } 230 231 @Override 232 public void onPhotoAvailable(long version, boolean fullImage) { 233 if (mFilmStripView == null) initFilmStripView(); 234 } 235 }); 236 } else { 237 // Get default media set by the URI 238 MediaItem mediaItem = (MediaItem) 239 mActivity.getDataManager().getMediaObject(itemPath); 240 mModel = new SinglePhotoDataAdapter(mActivity, mPhotoView, mediaItem); 241 mPhotoView.setModel(mModel); 242 updateCurrentPhoto(mediaItem); 243 } 244 245 mHandler = new SynchronizedHandler(mActivity.getGLRoot()) { 246 @Override 247 public void handleMessage(Message message) { 248 switch (message.what) { 249 case MSG_HIDE_BARS: { 250 hideBars(); 251 break; 252 } 253 default: throw new AssertionError(message.what); 254 } 255 } 256 }; 257 258 // start the opening animation 259 mPhotoView.setOpenedItem(itemPath); 260 } 261 262 private void updateShareURI(Path path) { 263 if (mShareActionProvider != null) { 264 DataManager manager = mActivity.getDataManager(); 265 int type = manager.getMediaType(path); 266 Intent intent = new Intent(Intent.ACTION_SEND); 267 intent.setType(MenuExecutor.getMimeType(type)); 268 intent.putExtra(Intent.EXTRA_STREAM, manager.getContentUri(path)); 269 mShareActionProvider.setShareIntent(intent); 270 mPendingSharePath = null; 271 } else { 272 // This happens when ActionBar is not created yet. 273 mPendingSharePath = path; 274 } 275 } 276 277 private void setTitle(String title) { 278 if (title == null) return; 279 boolean showTitle = mActivity.getAndroidContext().getResources().getBoolean( 280 R.bool.show_action_bar_title); 281 if (showTitle) 282 mActionBar.setTitle(title); 283 else 284 mActionBar.setTitle(""); 285 } 286 287 private void updateCurrentPhoto(MediaItem photo) { 288 if (mCurrentPhoto == photo) return; 289 mCurrentPhoto = photo; 290 if (mCurrentPhoto == null) return; 291 updateMenuOperations(); 292 if (mShowDetails) { 293 mDetailsHelper.reloadDetails(mModel.getCurrentIndex()); 294 } 295 setTitle(photo.getName()); 296 mPhotoView.showVideoPlayIcon( 297 photo.getMediaType() == MediaObject.MEDIA_TYPE_VIDEO); 298 299 updateShareURI(photo.getPath()); 300 } 301 302 private void updateMenuOperations() { 303 if (mMenu == null) return; 304 MenuItem item = mMenu.findItem(R.id.action_slideshow); 305 if (item != null) { 306 item.setVisible(canDoSlideShow()); 307 } 308 if (mCurrentPhoto == null) return; 309 int supportedOperations = mCurrentPhoto.getSupportedOperations(); 310 if (!GalleryUtils.isEditorAvailable((Context) mActivity, "image/*")) { 311 supportedOperations &= ~MediaObject.SUPPORT_EDIT; 312 } 313 314 MenuExecutor.updateMenuOperation(mMenu, supportedOperations); 315 } 316 317 private boolean canDoSlideShow() { 318 if (mMediaSet == null || mCurrentPhoto == null) { 319 return false; 320 } 321 if (mCurrentPhoto.getMediaType() != MediaObject.MEDIA_TYPE_IMAGE) { 322 return false; 323 } 324 if (mMediaSet instanceof MtpDevice) { 325 return false; 326 } 327 return true; 328 } 329 330 private void showBars() { 331 if (mShowBars) return; 332 mShowBars = true; 333 mActionBar.show(); 334 WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes(); 335 params.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE; 336 ((Activity) mActivity).getWindow().setAttributes(params); 337 if (mFilmStripView != null) { 338 mFilmStripView.show(); 339 } 340 } 341 342 private void hideBars() { 343 if (!mShowBars) return; 344 mShowBars = false; 345 mActionBar.hide(); 346 WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes(); 347 params.systemUiVisibility = View. SYSTEM_UI_FLAG_LOW_PROFILE; 348 ((Activity) mActivity).getWindow().setAttributes(params); 349 if (mFilmStripView != null) { 350 mFilmStripView.hide(); 351 } 352 } 353 354 private void refreshHidingMessage() { 355 mHandler.removeMessages(MSG_HIDE_BARS); 356 if (!mIsMenuVisible && !mIsInteracting) { 357 mHandler.sendEmptyMessageDelayed(MSG_HIDE_BARS, HIDE_BARS_TIMEOUT); 358 } 359 } 360 361 @Override 362 public void onUserInteraction() { 363 showBars(); 364 refreshHidingMessage(); 365 } 366 367 public void onUserInteractionTap() { 368 if (mShowBars) { 369 hideBars(); 370 mHandler.removeMessages(MSG_HIDE_BARS); 371 } else { 372 showBars(); 373 refreshHidingMessage(); 374 } 375 } 376 377 @Override 378 public void onUserInteractionBegin() { 379 showBars(); 380 mIsInteracting = true; 381 refreshHidingMessage(); 382 } 383 384 @Override 385 public void onUserInteractionEnd() { 386 mIsInteracting = false; 387 388 // This function could be called from GL thread (in SlotView.render) 389 // and post to the main thread. So, it could be executed while the 390 // activity is paused. 391 if (mIsActive) refreshHidingMessage(); 392 } 393 394 @Override 395 protected void onBackPressed() { 396 if (mShowDetails) { 397 hideDetails(); 398 } else { 399 PositionRepository repository = PositionRepository.getInstance(mActivity); 400 repository.clear(); 401 if (mCurrentPhoto != null) { 402 Position position = new Position(); 403 position.x = mRootPane.getWidth() / 2; 404 position.y = mRootPane.getHeight() / 2; 405 position.z = -1000; 406 repository.putPosition( 407 System.identityHashCode(mCurrentPhoto.getPath()), 408 position); 409 } 410 super.onBackPressed(); 411 } 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 return true; 424 } 425 426 @Override 427 protected boolean onItemSelected(MenuItem item) { 428 MediaItem current = mModel.getCurrentMediaItem(); 429 430 if (current == null) { 431 // item is not ready, ignore 432 return true; 433 } 434 435 int currentIndex = mModel.getCurrentIndex(); 436 Path path = current.getPath(); 437 438 DataManager manager = mActivity.getDataManager(); 439 int action = item.getItemId(); 440 switch (action) { 441 case android.R.id.home: { 442 if (mSetPathString != null) { 443 if (mActivity.getStateManager().getStateCount() > 1) { 444 onBackPressed(); 445 } else { 446 Activity a = (Activity) mActivity; 447 Uri uri = mActivity.getDataManager().getContentUri( 448 Path.fromString(mSetPathString)); 449 Intent intent = new Intent(Intent.ACTION_VIEW) 450 .setClass(a, Gallery.class) 451 .setDataAndType(uri, ContentResolver.CURSOR_DIR_BASE_TYPE) 452 .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | 453 Intent.FLAG_ACTIVITY_NEW_TASK); 454 a.startActivity(intent); 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 MediaItem item = mModel.getCurrentMediaItem(); 529 if (item == null) { 530 // item is not ready, ignore 531 return; 532 } 533 534 boolean playVideo = 535 (item.getSupportedOperations() & MediaItem.SUPPORT_PLAY) != 0; 536 537 if (playVideo) { 538 // determine if the point is at center (1/6) of the photo view. 539 // (The position of the "play" icon is at center (1/6) of the photo) 540 int w = mPhotoView.getWidth(); 541 int h = mPhotoView.getHeight(); 542 playVideo = (Math.abs(x - w / 2) * 12 <= w) 543 && (Math.abs(y - h / 2) * 12 <= h); 544 } 545 546 if (playVideo) { 547 playVideo((Activity) mActivity, item.getPlayUri(), item.getName()); 548 } else { 549 onUserInteractionTap(); 550 } 551 } 552 553 public static void playVideo(Activity activity, Uri uri, String title) { 554 try { 555 Intent intent = new Intent(Intent.ACTION_VIEW) 556 .setDataAndType(uri, "video/*"); 557 intent.putExtra(Intent.EXTRA_TITLE, title); 558 activity.startActivity(intent); 559 } catch (ActivityNotFoundException e) { 560 Toast.makeText(activity, activity.getString(R.string.video_err), 561 Toast.LENGTH_SHORT).show(); 562 } 563 } 564 565 // Called by FileStripView. 566 // Returns false if it cannot jump to the specified index at this time. 567 public boolean onSlotSelected(int slotIndex) { 568 return mPhotoView.jumpTo(slotIndex); 569 } 570 571 @Override 572 protected void onStateResult(int requestCode, int resultCode, Intent data) { 573 switch (requestCode) { 574 case REQUEST_CROP: 575 if (resultCode == Activity.RESULT_OK) { 576 if (data == null) break; 577 Path path = mApplication 578 .getDataManager().findPathByUri(data.getData()); 579 if (path != null) { 580 mModel.setCurrentPhoto(path, mCurrentIndex); 581 } 582 } 583 break; 584 case REQUEST_CROP_PICASA: { 585 int message = resultCode == Activity.RESULT_OK 586 ? R.string.crop_saved 587 : R.string.crop_not_saved; 588 Toast.makeText(mActivity.getAndroidContext(), 589 message, Toast.LENGTH_SHORT).show(); 590 break; 591 } 592 case REQUEST_SLIDESHOW: { 593 if (data == null) break; 594 String path = data.getStringExtra(SlideshowPage.KEY_ITEM_PATH); 595 int index = data.getIntExtra(SlideshowPage.KEY_PHOTO_INDEX, 0); 596 if (path != null) { 597 mModel.setCurrentPhoto(Path.fromString(path), index); 598 } 599 } 600 } 601 } 602 603 @Override 604 public void onPause() { 605 super.onPause(); 606 mIsActive = false; 607 if (mFilmStripView != null) { 608 mFilmStripView.pause(); 609 } 610 DetailsHelper.pause(); 611 mPhotoView.pause(); 612 mModel.pause(); 613 mHandler.removeMessages(MSG_HIDE_BARS); 614 mActionBar.removeOnMenuVisibilityListener(mMenuVisibilityListener); 615 mMenuExecutor.pause(); 616 } 617 618 @Override 619 protected void onResume() { 620 super.onResume(); 621 mIsActive = true; 622 setContentPane(mRootPane); 623 mModel.resume(); 624 mPhotoView.resume(); 625 if (mFilmStripView != null) { 626 mFilmStripView.resume(); 627 } 628 if (mMenuVisibilityListener == null) { 629 mMenuVisibilityListener = new MyMenuVisibilityListener(); 630 } 631 mActionBar.setDisplayOptions(mSetPathString != null, true); 632 mActionBar.addOnMenuVisibilityListener(mMenuVisibilityListener); 633 onUserInteraction(); 634 } 635 636 private class MyDetailsSource implements DetailsSource { 637 private int mIndex; 638 639 @Override 640 public MediaDetails getDetails() { 641 return mModel.getCurrentMediaItem().getDetails(); 642 } 643 644 @Override 645 public int size() { 646 return mMediaSet != null ? mMediaSet.getMediaItemCount() : 1; 647 } 648 649 @Override 650 public int findIndex(int indexHint) { 651 mIndex = indexHint; 652 return indexHint; 653 } 654 655 @Override 656 public int getIndex() { 657 return mIndex; 658 } 659 } 660} 661