PhotoPage.java revision 8bc5bebba780ca4a322b466b06fc909331697cf4
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; 20import android.app.ActionBar.OnMenuVisibilityListener; 21import android.app.Activity; 22import android.content.ActivityNotFoundException; 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 ActionBar 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 108 public static interface Model extends PhotoView.Model { 109 public void resume(); 110 public void pause(); 111 public boolean isEmpty(); 112 public MediaItem getCurrentMediaItem(); 113 public int getCurrentIndex(); 114 public void setCurrentPhoto(Path path, int indexHint); 115 } 116 117 private class MyMenuVisibilityListener implements OnMenuVisibilityListener { 118 public void onMenuVisibilityChanged(boolean isVisible) { 119 mIsMenuVisible = isVisible; 120 refreshHidingMessage(); 121 } 122 } 123 124 private final GLView mRootPane = new GLView() { 125 126 @Override 127 protected void renderBackground(GLCanvas view) { 128 view.clearBuffer(); 129 } 130 131 @Override 132 protected void onLayout( 133 boolean changed, int left, int top, int right, int bottom) { 134 mPhotoView.layout(0, 0, right - left, bottom - top); 135 PositionRepository.getInstance(mActivity).setOffset(0, 0); 136 int filmStripHeight = 0; 137 if (mFilmStripView != null) { 138 mFilmStripView.measure( 139 MeasureSpec.makeMeasureSpec(right - left, MeasureSpec.EXACTLY), 140 MeasureSpec.UNSPECIFIED); 141 filmStripHeight = mFilmStripView.getMeasuredHeight(); 142 mFilmStripView.layout(0, bottom - top - filmStripHeight, 143 right - left, bottom - top); 144 } 145 if (mShowDetails) { 146 mDetailsHelper.layout(left, GalleryActionBar.getHeight((Activity) mActivity), 147 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 = ((Activity) mActivity).getActionBar(); 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 String setPathString = data.getString(KEY_MEDIA_SET_PATH); 180 Path itemPath = Path.fromString(data.getString(KEY_MEDIA_ITEM_PATH)); 181 182 if (setPathString != null) { 183 mMediaSet = mActivity.getDataManager().getMediaSet(setPathString); 184 mCurrentIndex = data.getInt(KEY_INDEX_HINT, 0); 185 mMediaSet = (MediaSet) 186 mActivity.getDataManager().getMediaObject(setPathString); 187 if (mMediaSet == null) { 188 Log.w(TAG, "failed to restore " + setPathString); 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 (mCurrentPhoto == null || mMenu == null) return; 304 int supportedOperations = mCurrentPhoto.getSupportedOperations(); 305 if (!GalleryUtils.isEditorAvailable((Context) mActivity, "image/*")) { 306 supportedOperations &= ~MediaObject.SUPPORT_EDIT; 307 } 308 MenuItem item = mMenu.findItem(R.id.action_slideshow); 309 if (item != null) { 310 item.setVisible(mCurrentPhoto.getMediaType() == MediaObject.MEDIA_TYPE_IMAGE); 311 } 312 313 MenuExecutor.updateMenuOperation(mMenu, supportedOperations); 314 } 315 316 private void showBars() { 317 if (mShowBars) return; 318 mShowBars = true; 319 mActionBar.show(); 320 WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes(); 321 params.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE; 322 ((Activity) mActivity).getWindow().setAttributes(params); 323 if (mFilmStripView != null) { 324 mFilmStripView.show(); 325 } 326 } 327 328 private void hideBars() { 329 if (!mShowBars) return; 330 mShowBars = false; 331 mActionBar.hide(); 332 WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes(); 333 params.systemUiVisibility = View. SYSTEM_UI_FLAG_LOW_PROFILE; 334 ((Activity) mActivity).getWindow().setAttributes(params); 335 if (mFilmStripView != null) { 336 mFilmStripView.hide(); 337 } 338 } 339 340 private void refreshHidingMessage() { 341 mHandler.removeMessages(MSG_HIDE_BARS); 342 if (!mIsMenuVisible && !mIsInteracting) { 343 mHandler.sendEmptyMessageDelayed(MSG_HIDE_BARS, HIDE_BARS_TIMEOUT); 344 } 345 } 346 347 @Override 348 public void onUserInteraction() { 349 showBars(); 350 refreshHidingMessage(); 351 } 352 353 public void onUserInteractionTap() { 354 if (mShowBars) { 355 hideBars(); 356 mHandler.removeMessages(MSG_HIDE_BARS); 357 } else { 358 showBars(); 359 refreshHidingMessage(); 360 } 361 } 362 363 @Override 364 public void onUserInteractionBegin() { 365 showBars(); 366 mIsInteracting = true; 367 refreshHidingMessage(); 368 } 369 370 @Override 371 public void onUserInteractionEnd() { 372 mIsInteracting = false; 373 374 // This function could be called from GL thread (in SlotView.render) 375 // and post to the main thread. So, it could be executed while the 376 // activity is paused. 377 if (mIsActive) refreshHidingMessage(); 378 } 379 380 @Override 381 protected void onBackPressed() { 382 if (mShowDetails) { 383 hideDetails(); 384 } else { 385 PositionRepository repository = PositionRepository.getInstance(mActivity); 386 repository.clear(); 387 if (mCurrentPhoto != null) { 388 Position position = new Position(); 389 position.x = mRootPane.getWidth() / 2; 390 position.y = mRootPane.getHeight() / 2; 391 position.z = -1000; 392 repository.putPosition( 393 Long.valueOf(System.identityHashCode(mCurrentPhoto.getPath())), 394 position); 395 } 396 super.onBackPressed(); 397 } 398 } 399 400 @Override 401 protected boolean onCreateActionBar(Menu menu) { 402 MenuInflater inflater = ((Activity) mActivity).getMenuInflater(); 403 inflater.inflate(R.menu.photo, menu); 404 menu.findItem(R.id.action_slideshow).setVisible( 405 mMediaSet != null && !(mMediaSet instanceof MtpDevice)); 406 mShareActionProvider = GalleryActionBar.initializeShareActionProvider(menu); 407 if (mPendingSharePath != null) updateShareURI(mPendingSharePath); 408 mMenu = menu; 409 mShowBars = true; 410 updateMenuOperations(); 411 return true; 412 } 413 414 @Override 415 protected boolean onItemSelected(MenuItem item) { 416 MediaItem current = mModel.getCurrentMediaItem(); 417 418 if (current == null) { 419 // item is not ready, ignore 420 return true; 421 } 422 423 int currentIndex = mModel.getCurrentIndex(); 424 Path path = current.getPath(); 425 426 DataManager manager = mActivity.getDataManager(); 427 int action = item.getItemId(); 428 switch (action) { 429 case R.id.action_slideshow: { 430 Bundle data = new Bundle(); 431 data.putString(SlideshowPage.KEY_SET_PATH, mMediaSet.getPath().toString()); 432 data.putString(SlideshowPage.KEY_ITEM_PATH, path.toString()); 433 data.putInt(SlideshowPage.KEY_PHOTO_INDEX, currentIndex); 434 data.putBoolean(SlideshowPage.KEY_REPEAT, true); 435 mActivity.getStateManager().startStateForResult( 436 SlideshowPage.class, REQUEST_SLIDESHOW, data); 437 return true; 438 } 439 case R.id.action_crop: { 440 Activity activity = (Activity) mActivity; 441 Intent intent = new Intent(CropImage.CROP_ACTION); 442 intent.setClass(activity, CropImage.class); 443 intent.setData(manager.getContentUri(path)); 444 activity.startActivityForResult(intent, PicasaSource.isPicasaImage(current) 445 ? REQUEST_CROP_PICASA 446 : REQUEST_CROP); 447 return true; 448 } 449 case R.id.action_details: { 450 if (mShowDetails) { 451 hideDetails(); 452 } else { 453 showDetails(currentIndex); 454 } 455 return true; 456 } 457 case R.id.action_setas: 458 case R.id.action_confirm_delete: 459 case R.id.action_rotate_ccw: 460 case R.id.action_rotate_cw: 461 case R.id.action_show_on_map: 462 case R.id.action_edit: 463 mSelectionManager.deSelectAll(); 464 mSelectionManager.toggle(path); 465 mMenuExecutor.onMenuClicked(item, null); 466 return true; 467 case R.id.action_import: 468 mSelectionManager.deSelectAll(); 469 mSelectionManager.toggle(path); 470 mMenuExecutor.onMenuClicked(item, 471 new ImportCompleteListener(mActivity)); 472 return true; 473 default : 474 return false; 475 } 476 } 477 478 private void hideDetails() { 479 mShowDetails = false; 480 mDetailsHelper.hide(); 481 } 482 483 private void showDetails(int index) { 484 mShowDetails = true; 485 if (mDetailsHelper == null) { 486 mDetailsHelper = new DetailsHelper(mActivity, mRootPane, new MyDetailsSource()); 487 mDetailsHelper.setCloseListener(new CloseListener() { 488 public void onClose() { 489 hideDetails(); 490 } 491 }); 492 } 493 mDetailsHelper.reloadDetails(index); 494 mDetailsHelper.show(); 495 } 496 497 public void onSingleTapUp(int x, int y) { 498 MediaItem item = mModel.getCurrentMediaItem(); 499 if (item == null) { 500 // item is not ready, ignore 501 return; 502 } 503 504 boolean playVideo = 505 (item.getSupportedOperations() & MediaItem.SUPPORT_PLAY) != 0; 506 507 if (playVideo) { 508 // determine if the point is at center (1/6) of the photo view. 509 // (The position of the "play" icon is at center (1/6) of the photo) 510 int w = mPhotoView.getWidth(); 511 int h = mPhotoView.getHeight(); 512 playVideo = (Math.abs(x - w / 2) * 12 <= w) 513 && (Math.abs(y - h / 2) * 12 <= h); 514 } 515 516 if (playVideo) { 517 playVideo((Activity) mActivity, item.getPlayUri(), item.getName()); 518 } else { 519 onUserInteractionTap(); 520 } 521 } 522 523 public static void playVideo(Activity activity, Uri uri, String title) { 524 try { 525 Intent intent = new Intent(Intent.ACTION_VIEW) 526 .setDataAndType(uri, "video/*"); 527 intent.putExtra(Intent.EXTRA_TITLE, title); 528 activity.startActivity(intent); 529 } catch (ActivityNotFoundException e) { 530 Toast.makeText(activity, activity.getString(R.string.video_err), 531 Toast.LENGTH_SHORT).show(); 532 } 533 } 534 535 // Called by FileStripView. 536 // Returns false if it cannot jump to the specified index at this time. 537 public boolean onSlotSelected(int slotIndex) { 538 return mPhotoView.jumpTo(slotIndex); 539 } 540 541 @Override 542 protected void onStateResult(int requestCode, int resultCode, Intent data) { 543 switch (requestCode) { 544 case REQUEST_CROP: 545 if (resultCode == Activity.RESULT_OK) { 546 if (data == null) break; 547 Path path = mApplication 548 .getDataManager().findPathByUri(data.getData()); 549 if (path != null) { 550 mModel.setCurrentPhoto(path, mCurrentIndex); 551 } 552 } 553 break; 554 case REQUEST_CROP_PICASA: { 555 int message = resultCode == Activity.RESULT_OK 556 ? R.string.crop_saved 557 : R.string.crop_not_saved; 558 Toast.makeText(mActivity.getAndroidContext(), 559 message, Toast.LENGTH_SHORT).show(); 560 break; 561 } 562 case REQUEST_SLIDESHOW: { 563 if (data == null) break; 564 String path = data.getStringExtra(SlideshowPage.KEY_ITEM_PATH); 565 int index = data.getIntExtra(SlideshowPage.KEY_PHOTO_INDEX, 0); 566 if (path != null) { 567 mModel.setCurrentPhoto(Path.fromString(path), index); 568 } 569 } 570 } 571 } 572 573 @Override 574 public void onPause() { 575 super.onPause(); 576 mIsActive = false; 577 if (mFilmStripView != null) { 578 mFilmStripView.pause(); 579 } 580 DetailsHelper.pause(); 581 mPhotoView.pause(); 582 mModel.pause(); 583 mHandler.removeMessages(MSG_HIDE_BARS); 584 mActionBar.removeOnMenuVisibilityListener(mMenuVisibilityListener); 585 mMenuExecutor.pause(); 586 } 587 588 @Override 589 protected void onResume() { 590 super.onResume(); 591 mIsActive = true; 592 setContentPane(mRootPane); 593 mModel.resume(); 594 mPhotoView.resume(); 595 if (mFilmStripView != null) { 596 mFilmStripView.resume(); 597 } 598 if (mMenuVisibilityListener == null) { 599 mMenuVisibilityListener = new MyMenuVisibilityListener(); 600 } 601 mActionBar.addOnMenuVisibilityListener(mMenuVisibilityListener); 602 onUserInteraction(); 603 } 604 605 private class MyDetailsSource implements DetailsSource { 606 private int mIndex; 607 608 @Override 609 public MediaDetails getDetails() { 610 return mModel.getCurrentMediaItem().getDetails(); 611 } 612 613 @Override 614 public int size() { 615 return mMediaSet != null ? mMediaSet.getMediaItemCount() : 1; 616 } 617 618 @Override 619 public int findIndex(int indexHint) { 620 mIndex = indexHint; 621 return indexHint; 622 } 623 624 @Override 625 public int getIndex() { 626 return mIndex; 627 } 628 } 629} 630