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 (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 Long.valueOf(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 R.id.action_slideshow: { 442 Bundle data = new Bundle(); 443 data.putString(SlideshowPage.KEY_SET_PATH, mMediaSet.getPath().toString()); 444 data.putString(SlideshowPage.KEY_ITEM_PATH, path.toString()); 445 data.putInt(SlideshowPage.KEY_PHOTO_INDEX, currentIndex); 446 data.putBoolean(SlideshowPage.KEY_REPEAT, true); 447 mActivity.getStateManager().startStateForResult( 448 SlideshowPage.class, REQUEST_SLIDESHOW, data); 449 return true; 450 } 451 case R.id.action_crop: { 452 Activity activity = (Activity) mActivity; 453 Intent intent = new Intent(CropImage.CROP_ACTION); 454 intent.setClass(activity, CropImage.class); 455 intent.setData(manager.getContentUri(path)); 456 activity.startActivityForResult(intent, PicasaSource.isPicasaImage(current) 457 ? REQUEST_CROP_PICASA 458 : REQUEST_CROP); 459 return true; 460 } 461 case R.id.action_details: { 462 if (mShowDetails) { 463 hideDetails(); 464 } else { 465 showDetails(currentIndex); 466 } 467 return true; 468 } 469 case R.id.action_setas: 470 case R.id.action_confirm_delete: 471 case R.id.action_rotate_ccw: 472 case R.id.action_rotate_cw: 473 case R.id.action_show_on_map: 474 case R.id.action_edit: 475 mSelectionManager.deSelectAll(); 476 mSelectionManager.toggle(path); 477 mMenuExecutor.onMenuClicked(item, null); 478 return true; 479 case R.id.action_import: 480 mSelectionManager.deSelectAll(); 481 mSelectionManager.toggle(path); 482 mMenuExecutor.onMenuClicked(item, 483 new ImportCompleteListener(mActivity)); 484 return true; 485 default : 486 return false; 487 } 488 } 489 490 private void hideDetails() { 491 mShowDetails = false; 492 mDetailsHelper.hide(); 493 } 494 495 private void showDetails(int index) { 496 mShowDetails = true; 497 if (mDetailsHelper == null) { 498 mDetailsHelper = new DetailsHelper(mActivity, mRootPane, new MyDetailsSource()); 499 mDetailsHelper.setCloseListener(new CloseListener() { 500 public void onClose() { 501 hideDetails(); 502 } 503 }); 504 } 505 mDetailsHelper.reloadDetails(index); 506 mDetailsHelper.show(); 507 } 508 509 public void onSingleTapUp(int x, int y) { 510 MediaItem item = mModel.getCurrentMediaItem(); 511 if (item == null) { 512 // item is not ready, ignore 513 return; 514 } 515 516 boolean playVideo = 517 (item.getSupportedOperations() & MediaItem.SUPPORT_PLAY) != 0; 518 519 if (playVideo) { 520 // determine if the point is at center (1/6) of the photo view. 521 // (The position of the "play" icon is at center (1/6) of the photo) 522 int w = mPhotoView.getWidth(); 523 int h = mPhotoView.getHeight(); 524 playVideo = (Math.abs(x - w / 2) * 12 <= w) 525 && (Math.abs(y - h / 2) * 12 <= h); 526 } 527 528 if (playVideo) { 529 playVideo((Activity) mActivity, item.getPlayUri(), item.getName()); 530 } else { 531 onUserInteractionTap(); 532 } 533 } 534 535 public static void playVideo(Activity activity, Uri uri, String title) { 536 try { 537 Intent intent = new Intent(Intent.ACTION_VIEW) 538 .setDataAndType(uri, "video/*"); 539 intent.putExtra(Intent.EXTRA_TITLE, title); 540 activity.startActivity(intent); 541 } catch (ActivityNotFoundException e) { 542 Toast.makeText(activity, activity.getString(R.string.video_err), 543 Toast.LENGTH_SHORT).show(); 544 } 545 } 546 547 // Called by FileStripView. 548 // Returns false if it cannot jump to the specified index at this time. 549 public boolean onSlotSelected(int slotIndex) { 550 return mPhotoView.jumpTo(slotIndex); 551 } 552 553 @Override 554 protected void onStateResult(int requestCode, int resultCode, Intent data) { 555 switch (requestCode) { 556 case REQUEST_CROP: 557 if (resultCode == Activity.RESULT_OK) { 558 if (data == null) break; 559 Path path = mApplication 560 .getDataManager().findPathByUri(data.getData()); 561 if (path != null) { 562 mModel.setCurrentPhoto(path, mCurrentIndex); 563 } 564 } 565 break; 566 case REQUEST_CROP_PICASA: { 567 int message = resultCode == Activity.RESULT_OK 568 ? R.string.crop_saved 569 : R.string.crop_not_saved; 570 Toast.makeText(mActivity.getAndroidContext(), 571 message, Toast.LENGTH_SHORT).show(); 572 break; 573 } 574 case REQUEST_SLIDESHOW: { 575 if (data == null) break; 576 String path = data.getStringExtra(SlideshowPage.KEY_ITEM_PATH); 577 int index = data.getIntExtra(SlideshowPage.KEY_PHOTO_INDEX, 0); 578 if (path != null) { 579 mModel.setCurrentPhoto(Path.fromString(path), index); 580 } 581 } 582 } 583 } 584 585 @Override 586 public void onPause() { 587 super.onPause(); 588 mIsActive = false; 589 if (mFilmStripView != null) { 590 mFilmStripView.pause(); 591 } 592 DetailsHelper.pause(); 593 mPhotoView.pause(); 594 mModel.pause(); 595 mHandler.removeMessages(MSG_HIDE_BARS); 596 mActionBar.removeOnMenuVisibilityListener(mMenuVisibilityListener); 597 mMenuExecutor.pause(); 598 } 599 600 @Override 601 protected void onResume() { 602 super.onResume(); 603 mIsActive = true; 604 setContentPane(mRootPane); 605 mModel.resume(); 606 mPhotoView.resume(); 607 if (mFilmStripView != null) { 608 mFilmStripView.resume(); 609 } 610 if (mMenuVisibilityListener == null) { 611 mMenuVisibilityListener = new MyMenuVisibilityListener(); 612 } 613 mActionBar.addOnMenuVisibilityListener(mMenuVisibilityListener); 614 onUserInteraction(); 615 } 616 617 private class MyDetailsSource implements DetailsSource { 618 private int mIndex; 619 620 @Override 621 public MediaDetails getDetails() { 622 return mModel.getCurrentMediaItem().getDetails(); 623 } 624 625 @Override 626 public int size() { 627 return mMediaSet != null ? mMediaSet.getMediaItemCount() : 1; 628 } 629 630 @Override 631 public int findIndex(int indexHint) { 632 mIndex = indexHint; 633 return indexHint; 634 } 635 636 @Override 637 public int getIndex() { 638 return mIndex; 639 } 640 } 641} 642