2 * Copyright (C) 2010 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.gallery3d.app;
19 import android.app.ActionBar.OnMenuVisibilityListener;
20 import android.app.Activity;
21 import android.content.ActivityNotFoundException;
22 import android.content.ContentResolver;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.graphics.Rect;
26 import android.net.Uri;
27 import android.os.Bundle;
28 import android.os.Handler;
29 import android.os.Message;
30 import android.view.Menu;
31 import android.view.MenuInflater;
32 import android.view.MenuItem;
33 import android.view.View;
34 import android.view.ViewGroup;
35 import android.view.View.MeasureSpec;
36 import android.view.WindowManager;
37 import android.widget.ShareActionProvider;
38 import android.widget.Toast;
40 import com.android.gallery3d.R;
41 import com.android.gallery3d.data.DataManager;
42 import com.android.gallery3d.data.MediaDetails;
43 import com.android.gallery3d.data.MediaItem;
44 import com.android.gallery3d.data.MediaObject;
45 import com.android.gallery3d.data.MediaSet;
46 import com.android.gallery3d.data.MtpDevice;
47 import com.android.gallery3d.data.Path;
48 import com.android.gallery3d.data.SnailSource;
49 import com.android.gallery3d.picasasource.PicasaSource;
50 import com.android.gallery3d.ui.DetailsHelper;
51 import com.android.gallery3d.ui.DetailsHelper.CloseListener;
52 import com.android.gallery3d.ui.DetailsHelper.DetailsSource;
53 import com.android.gallery3d.ui.FilmStripView;
54 import com.android.gallery3d.ui.GLCanvas;
55 import com.android.gallery3d.ui.GLView;
56 import com.android.gallery3d.ui.ImportCompleteListener;
57 import com.android.gallery3d.ui.MenuExecutor;
58 import com.android.gallery3d.ui.PhotoView;
59 import com.android.gallery3d.ui.PositionRepository;
60 import com.android.gallery3d.ui.PositionRepository.Position;
61 import com.android.gallery3d.ui.SelectionManager;
62 import com.android.gallery3d.ui.SynchronizedHandler;
63 import com.android.gallery3d.ui.UserInteractionListener;
64 import com.android.gallery3d.util.GalleryUtils;
66 public class PhotoPage extends ActivityState
67 implements PhotoView.PhotoTapListener, FilmStripView.Listener,
68 UserInteractionListener {
69 private static final String TAG = "PhotoPage";
71 private static final int MSG_HIDE_BARS = 1;
73 private static final int HIDE_BARS_TIMEOUT = 3500;
75 private static final int REQUEST_SLIDESHOW = 1;
76 private static final int REQUEST_CROP = 2;
77 private static final int REQUEST_CROP_PICASA = 3;
79 public static final String KEY_MEDIA_SET_PATH = "media-set-path";
80 public static final String KEY_MEDIA_ITEM_PATH = "media-item-path";
81 public static final String KEY_INDEX_HINT = "index-hint";
82 public static final String KEY_OPEN_ANIMATION_RECT = "open-animation-rect";
84 private GalleryApp mApplication;
85 private SelectionManager mSelectionManager;
87 private PhotoView mPhotoView;
88 private PhotoPage.Model mModel;
89 private FilmStripView mFilmStripView;
90 private DetailsHelper mDetailsHelper;
91 private boolean mShowDetails;
92 private Path mPendingSharePath;
94 // mMediaSet could be null if there is no KEY_MEDIA_SET_PATH supplied.
95 // E.g., viewing a photo in gmail attachment
96 private MediaSet mMediaSet;
99 private final Intent mResultIntent = new Intent();
100 private int mCurrentIndex = 0;
101 private Handler mHandler;
102 private boolean mShowBars = true;
103 private GalleryActionBar mActionBar;
104 private MyMenuVisibilityListener mMenuVisibilityListener;
105 private boolean mIsMenuVisible;
106 private boolean mIsInteracting;
107 private MediaItem mCurrentPhoto = null;
108 private MenuExecutor mMenuExecutor;
109 private boolean mIsActive;
110 private ShareActionProvider mShareActionProvider;
111 private String mSetPathString;
113 // This is for testing only. It should be removed once we have the real
115 private CameraView mCameraView;
116 private ScreenNailBridge mScreenNail;
118 public static interface Model extends PhotoView.Model {
119 public void resume();
121 public boolean isEmpty();
122 public MediaItem getCurrentMediaItem();
123 public int getCurrentIndex();
124 public void setCurrentPhoto(Path path, int indexHint);
127 private class MyMenuVisibilityListener implements OnMenuVisibilityListener {
128 public void onMenuVisibilityChanged(boolean isVisible) {
129 mIsMenuVisible = isVisible;
130 refreshHidingMessage();
134 private final GLView mRootPane = new GLView() {
137 protected void renderBackground(GLCanvas view) {
142 protected void onLayout(
143 boolean changed, int left, int top, int right, int bottom) {
144 mPhotoView.layout(0, 0, right - left, bottom - top);
145 // Reset position offset after the layout is changed.
146 PositionRepository.getInstance(mActivity).setOffset(0, 0);
147 int filmStripHeight = 0;
148 if (mFilmStripView != null) {
149 mFilmStripView.measure(
150 MeasureSpec.makeMeasureSpec(right - left, MeasureSpec.EXACTLY),
151 MeasureSpec.UNSPECIFIED);
152 filmStripHeight = mFilmStripView.getMeasuredHeight();
153 mFilmStripView.layout(0, bottom - top - filmStripHeight,
154 right - left, bottom - top);
157 mDetailsHelper.layout(left, mActionBar.getHeight(), right, bottom);
162 private void initFilmStripView() {
163 Config.PhotoPage config = Config.PhotoPage.get((Context) mActivity);
164 mFilmStripView = new FilmStripView(mActivity, mMediaSet,
165 config.filmstripTopMargin, config.filmstripMidMargin, config.filmstripBottomMargin,
166 config.filmstripContentSize, config.filmstripThumbSize, config.filmstripBarSize,
167 config.filmstripGripSize, config.filmstripGripWidth);
168 mRootPane.addComponent(mFilmStripView);
169 mFilmStripView.setListener(this);
170 mFilmStripView.setUserInteractionListener(this);
171 mFilmStripView.setFocusIndex(mCurrentIndex);
172 mFilmStripView.setStartIndex(mCurrentIndex);
173 mRootPane.requestLayout();
174 if (mIsActive) mFilmStripView.resume();
175 if (!mShowBars) mFilmStripView.setVisibility(GLView.INVISIBLE);
179 public void onCreate(Bundle data, Bundle restoreState) {
180 mActionBar = mActivity.getGalleryActionBar();
181 mSelectionManager = new SelectionManager(mActivity, false);
182 mMenuExecutor = new MenuExecutor(mActivity, mSelectionManager);
184 mPhotoView = new PhotoView(mActivity);
185 mPhotoView.setPhotoTapListener(this);
186 mRootPane.addComponent(mPhotoView);
187 mApplication = (GalleryApp)((Activity) mActivity).getApplication();
189 mSetPathString = data.getString(KEY_MEDIA_SET_PATH);
190 Path itemPath = Path.fromString(data.getString(KEY_MEDIA_ITEM_PATH));
192 if (mSetPathString != null) {
193 // Uncomment the block below to test camera screennail.
195 Path cameraScreenNailSetPath = addCameraScreenNail();
197 // Combine the original MediaSet with the one for camera ScreenNail.
198 mSetPathString = "/combo/item/{" + cameraScreenNailSetPath + "," +
199 mSetPathString + "}";
201 mMediaSet = mActivity.getDataManager().getMediaSet(mSetPathString);
202 mCurrentIndex = data.getInt(KEY_INDEX_HINT, 0);
203 mMediaSet = (MediaSet)
204 mActivity.getDataManager().getMediaObject(mSetPathString);
205 if (mMediaSet == null) {
206 Log.w(TAG, "failed to restore " + mSetPathString);
208 PhotoDataAdapter pda = new PhotoDataAdapter(
209 mActivity, mPhotoView, mMediaSet, itemPath, mCurrentIndex);
211 mPhotoView.setModel(mModel);
213 mResultIntent.putExtra(KEY_INDEX_HINT, mCurrentIndex);
214 setStateResult(Activity.RESULT_OK, mResultIntent);
216 pda.setDataListener(new PhotoDataAdapter.DataListener() {
219 public void onPhotoChanged(int index, Path item) {
220 if (mFilmStripView != null) mFilmStripView.setFocusIndex(index);
221 mCurrentIndex = index;
222 mResultIntent.putExtra(KEY_INDEX_HINT, index);
224 mResultIntent.putExtra(KEY_MEDIA_ITEM_PATH, item.toString());
225 MediaItem photo = mModel.getCurrentMediaItem();
226 if (photo != null) updateCurrentPhoto(photo);
228 mResultIntent.removeExtra(KEY_MEDIA_ITEM_PATH);
230 setStateResult(Activity.RESULT_OK, mResultIntent);
234 public void onLoadingFinished() {
235 GalleryUtils.setSpinnerVisibility((Activity) mActivity, false);
236 if (!mModel.isEmpty()) {
237 MediaItem photo = mModel.getCurrentMediaItem();
238 if (photo != null) updateCurrentPhoto(photo);
239 } else if (mIsActive) {
240 mActivity.getStateManager().finishState(PhotoPage.this);
245 public void onLoadingStarted() {
246 GalleryUtils.setSpinnerVisibility((Activity) mActivity, true);
250 public void onPhotoAvailable(long version, boolean fullImage) {
251 if (mFilmStripView == null) initFilmStripView();
255 // Get default media set by the URI
256 MediaItem mediaItem = (MediaItem)
257 mActivity.getDataManager().getMediaObject(itemPath);
258 mModel = new SinglePhotoDataAdapter(mActivity, mPhotoView, mediaItem);
259 mPhotoView.setModel(mModel);
260 updateCurrentPhoto(mediaItem);
263 mHandler = new SynchronizedHandler(mActivity.getGLRoot()) {
265 public void handleMessage(Message message) {
266 switch (message.what) {
267 case MSG_HIDE_BARS: {
271 default: throw new AssertionError(message.what);
276 // start the opening animation only if it's not restored.
277 if (restoreState == null) {
278 mPhotoView.setOpenAnimationRect((Rect) data.getParcelable(KEY_OPEN_ANIMATION_RECT));
282 // We create a Camera View and a ScreenNail. The two work together
283 // to present the view together with other pictures. Returns the
284 // Path of the MediaItem hosting the ScreenNail.
285 private Path addCameraScreenNail() {
286 // Create a camera view and add it to the root.
287 Activity activity = (Activity) mActivity;
288 mCameraView = new CameraView(activity);
289 ViewGroup galleryRoot = (ViewGroup) activity.findViewById(R.id.gallery_root);
290 galleryRoot.addView(mCameraView);
292 // Create a ScreenNail and register it.
293 mScreenNail = new ScreenNailBridge(mCameraView);
294 mCameraView.setScreenNailBridge(mScreenNail);
295 return SnailSource.registerScreenNail(mScreenNail);
298 private void removeCameraScreenNail() {
299 if (mCameraView == null) return;
301 // Remove the camera view.
302 ((ViewGroup) mCameraView.getParent()).removeView(mCameraView);
305 // Unregister the ScreenNail.
306 SnailSource.unregisterScreenNail(mScreenNail);
310 private void updateShareURI(Path path) {
311 if (mShareActionProvider != null) {
312 DataManager manager = mActivity.getDataManager();
313 int type = manager.getMediaType(path);
314 Intent intent = new Intent(Intent.ACTION_SEND);
315 intent.setType(MenuExecutor.getMimeType(type));
316 intent.putExtra(Intent.EXTRA_STREAM, manager.getContentUri(path));
317 mShareActionProvider.setShareIntent(intent);
318 mPendingSharePath = null;
320 // This happens when ActionBar is not created yet.
321 mPendingSharePath = path;
325 private void setTitle(String title) {
326 if (title == null) return;
327 boolean showTitle = mActivity.getAndroidContext().getResources().getBoolean(
328 R.bool.show_action_bar_title);
330 mActionBar.setTitle(title);
332 mActionBar.setTitle("");
335 private void updateCurrentPhoto(MediaItem photo) {
336 if (mCurrentPhoto == photo) return;
337 mCurrentPhoto = photo;
338 if (mCurrentPhoto == null) return;
339 updateMenuOperations();
341 mDetailsHelper.reloadDetails(mModel.getCurrentIndex());
343 setTitle(photo.getName());
344 mPhotoView.showVideoPlayIcon(
345 photo.getMediaType() == MediaObject.MEDIA_TYPE_VIDEO);
347 if ((photo.getSupportedOperations() & MediaItem.SUPPORT_SHARE) != 0) {
348 updateShareURI(photo.getPath());
352 private void updateMenuOperations() {
353 if (mMenu == null) return;
354 MenuItem item = mMenu.findItem(R.id.action_slideshow);
356 item.setVisible(canDoSlideShow());
358 if (mCurrentPhoto == null) return;
359 int supportedOperations = mCurrentPhoto.getSupportedOperations();
360 if (!GalleryUtils.isEditorAvailable((Context) mActivity, "image/*")) {
361 supportedOperations &= ~MediaObject.SUPPORT_EDIT;
364 MenuExecutor.updateMenuOperation(mMenu, supportedOperations);
367 private boolean canDoSlideShow() {
368 if (mMediaSet == null || mCurrentPhoto == null) {
371 if (mCurrentPhoto.getMediaType() != MediaObject.MEDIA_TYPE_IMAGE) {
374 if (mMediaSet instanceof MtpDevice) {
380 private void showBars() {
381 if (mShowBars) return;
384 WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes();
385 params.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE;
386 ((Activity) mActivity).getWindow().setAttributes(params);
387 if (mFilmStripView != null) {
388 mFilmStripView.show();
392 private void hideBars() {
393 if (!mShowBars) return;
396 WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes();
397 params.systemUiVisibility = View. SYSTEM_UI_FLAG_LOW_PROFILE;
398 ((Activity) mActivity).getWindow().setAttributes(params);
399 if (mFilmStripView != null) {
400 mFilmStripView.hide();
404 private void refreshHidingMessage() {
405 mHandler.removeMessages(MSG_HIDE_BARS);
406 if (!mIsMenuVisible && !mIsInteracting) {
407 mHandler.sendEmptyMessageDelayed(MSG_HIDE_BARS, HIDE_BARS_TIMEOUT);
412 public void onUserInteraction() {
414 refreshHidingMessage();
417 public void onUserInteractionTap() {
420 mHandler.removeMessages(MSG_HIDE_BARS);
423 refreshHidingMessage();
428 public void onUserInteractionBegin() {
430 mIsInteracting = true;
431 refreshHidingMessage();
435 public void onUserInteractionEnd() {
436 mIsInteracting = false;
438 // This function could be called from GL thread (in SlotView.render)
439 // and post to the main thread. So, it could be executed while the
440 // activity is paused.
441 if (mIsActive) refreshHidingMessage();
445 protected void onBackPressed() {
449 PositionRepository repository = PositionRepository.getInstance(mActivity);
451 if (mCurrentPhoto != null) {
452 Position position = new Position();
453 position.x = mRootPane.getWidth() / 2;
454 position.y = mRootPane.getHeight() / 2;
456 repository.putPosition(
457 System.identityHashCode(mCurrentPhoto.getPath()),
460 super.onBackPressed();
465 protected boolean onCreateActionBar(Menu menu) {
466 MenuInflater inflater = ((Activity) mActivity).getMenuInflater();
467 inflater.inflate(R.menu.photo, menu);
468 mShareActionProvider = GalleryActionBar.initializeShareActionProvider(menu);
469 if (mPendingSharePath != null) updateShareURI(mPendingSharePath);
472 updateMenuOperations();
477 protected boolean onItemSelected(MenuItem item) {
478 MediaItem current = mModel.getCurrentMediaItem();
480 if (current == null) {
481 // item is not ready, ignore
485 int currentIndex = mModel.getCurrentIndex();
486 Path path = current.getPath();
488 DataManager manager = mActivity.getDataManager();
489 int action = item.getItemId();
491 case android.R.id.home: {
492 if (mSetPathString != null) {
493 if (mActivity.getStateManager().getStateCount() > 1) {
496 Activity a = (Activity) mActivity;
497 Uri uri = mActivity.getDataManager().getContentUri(
498 Path.fromString(mSetPathString));
499 Intent intent = new Intent(Intent.ACTION_VIEW)
500 .setClass(a, Gallery.class)
501 .setDataAndType(uri, ContentResolver.CURSOR_DIR_BASE_TYPE)
502 .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP |
503 Intent.FLAG_ACTIVITY_NEW_TASK);
504 a.startActivity(intent);
509 case R.id.action_slideshow: {
510 Bundle data = new Bundle();
511 data.putString(SlideshowPage.KEY_SET_PATH, mMediaSet.getPath().toString());
512 data.putString(SlideshowPage.KEY_ITEM_PATH, path.toString());
513 data.putInt(SlideshowPage.KEY_PHOTO_INDEX, currentIndex);
514 data.putBoolean(SlideshowPage.KEY_REPEAT, true);
515 mActivity.getStateManager().startStateForResult(
516 SlideshowPage.class, REQUEST_SLIDESHOW, data);
519 case R.id.action_crop: {
520 Activity activity = (Activity) mActivity;
521 Intent intent = new Intent(CropImage.CROP_ACTION);
522 intent.setClass(activity, CropImage.class);
523 intent.setData(manager.getContentUri(path));
524 activity.startActivityForResult(intent, PicasaSource.isPicasaImage(current)
525 ? REQUEST_CROP_PICASA
529 case R.id.action_details: {
533 showDetails(currentIndex);
537 case R.id.action_setas:
538 case R.id.action_confirm_delete:
539 case R.id.action_rotate_ccw:
540 case R.id.action_rotate_cw:
541 case R.id.action_show_on_map:
542 case R.id.action_edit:
543 mSelectionManager.deSelectAll();
544 mSelectionManager.toggle(path);
545 mMenuExecutor.onMenuClicked(item, null);
547 case R.id.action_import:
548 mSelectionManager.deSelectAll();
549 mSelectionManager.toggle(path);
550 mMenuExecutor.onMenuClicked(item,
551 new ImportCompleteListener(mActivity));
558 private void hideDetails() {
559 mShowDetails = false;
560 mDetailsHelper.hide();
563 private void showDetails(int index) {
565 if (mDetailsHelper == null) {
566 mDetailsHelper = new DetailsHelper(mActivity, mRootPane, new MyDetailsSource());
567 mDetailsHelper.setCloseListener(new CloseListener() {
568 public void onClose() {
573 mDetailsHelper.reloadDetails(index);
574 mDetailsHelper.show();
577 public void onSingleTapUp(int x, int y) {
578 MediaItem item = mModel.getCurrentMediaItem();
580 // item is not ready, ignore
585 (item.getSupportedOperations() & MediaItem.SUPPORT_PLAY) != 0;
588 // determine if the point is at center (1/6) of the photo view.
589 // (The position of the "play" icon is at center (1/6) of the photo)
590 int w = mPhotoView.getWidth();
591 int h = mPhotoView.getHeight();
592 playVideo = (Math.abs(x - w / 2) * 12 <= w)
593 && (Math.abs(y - h / 2) * 12 <= h);
597 playVideo((Activity) mActivity, item.getPlayUri(), item.getName());
599 onUserInteractionTap();
603 public static void playVideo(Activity activity, Uri uri, String title) {
605 Intent intent = new Intent(Intent.ACTION_VIEW)
606 .setDataAndType(uri, "video/*");
607 intent.putExtra(Intent.EXTRA_TITLE, title);
608 activity.startActivity(intent);
609 } catch (ActivityNotFoundException e) {
610 Toast.makeText(activity, activity.getString(R.string.video_err),
611 Toast.LENGTH_SHORT).show();
615 // Called by FileStripView.
616 // Returns false if it cannot jump to the specified index at this time.
617 public boolean onSlotSelected(int slotIndex) {
618 return mPhotoView.jumpTo(slotIndex);
622 protected void onStateResult(int requestCode, int resultCode, Intent data) {
623 switch (requestCode) {
625 if (resultCode == Activity.RESULT_OK) {
626 if (data == null) break;
627 Path path = mApplication
628 .getDataManager().findPathByUri(data.getData());
630 mModel.setCurrentPhoto(path, mCurrentIndex);
634 case REQUEST_CROP_PICASA: {
635 int message = resultCode == Activity.RESULT_OK
636 ? R.string.crop_saved
637 : R.string.crop_not_saved;
638 Toast.makeText(mActivity.getAndroidContext(),
639 message, Toast.LENGTH_SHORT).show();
642 case REQUEST_SLIDESHOW: {
643 if (data == null) break;
644 String path = data.getStringExtra(SlideshowPage.KEY_ITEM_PATH);
645 int index = data.getIntExtra(SlideshowPage.KEY_PHOTO_INDEX, 0);
647 mModel.setCurrentPhoto(Path.fromString(path), index);
654 public void onPause() {
657 if (mFilmStripView != null) {
658 mFilmStripView.pause();
660 DetailsHelper.pause();
663 mHandler.removeMessages(MSG_HIDE_BARS);
664 mActionBar.removeOnMenuVisibilityListener(mMenuVisibilityListener);
665 mMenuExecutor.pause();
669 protected void onResume() {
672 setContentPane(mRootPane);
673 // Reset position offset for resuming.
674 PositionRepository.getInstance(mActivity).setOffset(
675 mPhotoView.bounds().left, mPhotoView.bounds().top);
679 if (mFilmStripView != null) {
680 mFilmStripView.resume();
682 if (mMenuVisibilityListener == null) {
683 mMenuVisibilityListener = new MyMenuVisibilityListener();
685 mActionBar.setDisplayOptions(mSetPathString != null, true);
686 mActionBar.addOnMenuVisibilityListener(mMenuVisibilityListener);
691 protected void onDestroy() {
692 removeCameraScreenNail();
696 private class MyDetailsSource implements DetailsSource {
700 public MediaDetails getDetails() {
701 return mModel.getCurrentMediaItem().getDetails();
706 return mMediaSet != null ? mMediaSet.getMediaItemCount() : 1;
710 public int findIndex(int indexHint) {
716 public int getIndex() {