/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.gallery3d.app; import android.app.ActionBar; import android.app.ActionBar.OnMenuVisibilityListener; import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.View.MeasureSpec; import android.view.WindowManager; import android.widget.ShareActionProvider; import android.widget.Toast; import com.android.gallery3d.R; import com.android.gallery3d.data.DataManager; import com.android.gallery3d.data.MediaDetails; import com.android.gallery3d.data.MediaItem; import com.android.gallery3d.data.MediaObject; import com.android.gallery3d.data.MediaSet; import com.android.gallery3d.data.MtpDevice; import com.android.gallery3d.data.Path; import com.android.gallery3d.picasasource.PicasaSource; import com.android.gallery3d.ui.DetailsHelper; import com.android.gallery3d.ui.DetailsHelper.CloseListener; import com.android.gallery3d.ui.DetailsHelper.DetailsSource; import com.android.gallery3d.ui.FilmStripView; import com.android.gallery3d.ui.GLCanvas; import com.android.gallery3d.ui.GLView; import com.android.gallery3d.ui.ImportCompleteListener; import com.android.gallery3d.ui.MenuExecutor; import com.android.gallery3d.ui.PhotoView; import com.android.gallery3d.ui.PositionRepository; import com.android.gallery3d.ui.PositionRepository.Position; import com.android.gallery3d.ui.SelectionManager; import com.android.gallery3d.ui.SynchronizedHandler; import com.android.gallery3d.ui.UserInteractionListener; import com.android.gallery3d.util.GalleryUtils; public class PhotoPage extends ActivityState implements PhotoView.PhotoTapListener, FilmStripView.Listener, UserInteractionListener { private static final String TAG = "PhotoPage"; private static final int MSG_HIDE_BARS = 1; private static final int HIDE_BARS_TIMEOUT = 3500; private static final int REQUEST_SLIDESHOW = 1; private static final int REQUEST_CROP = 2; private static final int REQUEST_CROP_PICASA = 3; public static final String KEY_MEDIA_SET_PATH = "media-set-path"; public static final String KEY_MEDIA_ITEM_PATH = "media-item-path"; public static final String KEY_INDEX_HINT = "index-hint"; private GalleryApp mApplication; private SelectionManager mSelectionManager; private PhotoView mPhotoView; private PhotoPage.Model mModel; private FilmStripView mFilmStripView; private DetailsHelper mDetailsHelper; private boolean mShowDetails; private Path mPendingSharePath; // mMediaSet could be null if there is no KEY_MEDIA_SET_PATH supplied. // E.g., viewing a photo in gmail attachment private MediaSet mMediaSet; private Menu mMenu; private final Intent mResultIntent = new Intent(); private int mCurrentIndex = 0; private Handler mHandler; private boolean mShowBars = true; private ActionBar mActionBar; private MyMenuVisibilityListener mMenuVisibilityListener; private boolean mIsMenuVisible; private boolean mIsInteracting; private MediaItem mCurrentPhoto = null; private MenuExecutor mMenuExecutor; private boolean mIsActive; private ShareActionProvider mShareActionProvider; public static interface Model extends PhotoView.Model { public void resume(); public void pause(); public boolean isEmpty(); public MediaItem getCurrentMediaItem(); public int getCurrentIndex(); public void setCurrentPhoto(Path path, int indexHint); } private class MyMenuVisibilityListener implements OnMenuVisibilityListener { public void onMenuVisibilityChanged(boolean isVisible) { mIsMenuVisible = isVisible; refreshHidingMessage(); } } private final GLView mRootPane = new GLView() { @Override protected void renderBackground(GLCanvas view) { view.clearBuffer(); } @Override protected void onLayout( boolean changed, int left, int top, int right, int bottom) { mPhotoView.layout(0, 0, right - left, bottom - top); PositionRepository.getInstance(mActivity).setOffset(0, 0); int filmStripHeight = 0; if (mFilmStripView != null) { mFilmStripView.measure( MeasureSpec.makeMeasureSpec(right - left, MeasureSpec.EXACTLY), MeasureSpec.UNSPECIFIED); filmStripHeight = mFilmStripView.getMeasuredHeight(); mFilmStripView.layout(0, bottom - top - filmStripHeight, right - left, bottom - top); } if (mShowDetails) { mDetailsHelper.layout(left, GalleryActionBar.getHeight((Activity) mActivity), right, bottom); } } }; private void initFilmStripView() { Config.PhotoPage config = Config.PhotoPage.get((Context) mActivity); mFilmStripView = new FilmStripView(mActivity, mMediaSet, config.filmstripTopMargin, config.filmstripMidMargin, config.filmstripBottomMargin, config.filmstripContentSize, config.filmstripThumbSize, config.filmstripBarSize, config.filmstripGripSize, config.filmstripGripWidth); mRootPane.addComponent(mFilmStripView); mFilmStripView.setListener(this); mFilmStripView.setUserInteractionListener(this); mFilmStripView.setFocusIndex(mCurrentIndex); mFilmStripView.setStartIndex(mCurrentIndex); mRootPane.requestLayout(); if (mIsActive) mFilmStripView.resume(); if (!mShowBars) mFilmStripView.setVisibility(GLView.INVISIBLE); } @Override public void onCreate(Bundle data, Bundle restoreState) { mActionBar = ((Activity) mActivity).getActionBar(); mSelectionManager = new SelectionManager(mActivity, false); mMenuExecutor = new MenuExecutor(mActivity, mSelectionManager); mPhotoView = new PhotoView(mActivity); mPhotoView.setPhotoTapListener(this); mRootPane.addComponent(mPhotoView); mApplication = (GalleryApp)((Activity) mActivity).getApplication(); String setPathString = data.getString(KEY_MEDIA_SET_PATH); Path itemPath = Path.fromString(data.getString(KEY_MEDIA_ITEM_PATH)); if (setPathString != null) { mMediaSet = mActivity.getDataManager().getMediaSet(setPathString); mCurrentIndex = data.getInt(KEY_INDEX_HINT, 0); mMediaSet = (MediaSet) mActivity.getDataManager().getMediaObject(setPathString); if (mMediaSet == null) { Log.w(TAG, "failed to restore " + setPathString); } PhotoDataAdapter pda = new PhotoDataAdapter( mActivity, mPhotoView, mMediaSet, itemPath, mCurrentIndex); mModel = pda; mPhotoView.setModel(mModel); mResultIntent.putExtra(KEY_INDEX_HINT, mCurrentIndex); setStateResult(Activity.RESULT_OK, mResultIntent); pda.setDataListener(new PhotoDataAdapter.DataListener() { @Override public void onPhotoChanged(int index, Path item) { if (mFilmStripView != null) mFilmStripView.setFocusIndex(index); mCurrentIndex = index; mResultIntent.putExtra(KEY_INDEX_HINT, index); if (item != null) { mResultIntent.putExtra(KEY_MEDIA_ITEM_PATH, item.toString()); MediaItem photo = mModel.getCurrentMediaItem(); if (photo != null) updateCurrentPhoto(photo); } else { mResultIntent.removeExtra(KEY_MEDIA_ITEM_PATH); } setStateResult(Activity.RESULT_OK, mResultIntent); } @Override public void onLoadingFinished() { GalleryUtils.setSpinnerVisibility((Activity) mActivity, false); if (!mModel.isEmpty()) { MediaItem photo = mModel.getCurrentMediaItem(); if (photo != null) updateCurrentPhoto(photo); } else if (mIsActive) { mActivity.getStateManager().finishState(PhotoPage.this); } } @Override public void onLoadingStarted() { GalleryUtils.setSpinnerVisibility((Activity) mActivity, true); } @Override public void onPhotoAvailable(long version, boolean fullImage) { if (mFilmStripView == null) initFilmStripView(); } }); } else { // Get default media set by the URI MediaItem mediaItem = (MediaItem) mActivity.getDataManager().getMediaObject(itemPath); mModel = new SinglePhotoDataAdapter(mActivity, mPhotoView, mediaItem); mPhotoView.setModel(mModel); updateCurrentPhoto(mediaItem); } mHandler = new SynchronizedHandler(mActivity.getGLRoot()) { @Override public void handleMessage(Message message) { switch (message.what) { case MSG_HIDE_BARS: { hideBars(); break; } default: throw new AssertionError(message.what); } } }; // start the opening animation mPhotoView.setOpenedItem(itemPath); } private void updateShareURI(Path path) { if (mShareActionProvider != null) { DataManager manager = mActivity.getDataManager(); int type = manager.getMediaType(path); Intent intent = new Intent(Intent.ACTION_SEND); intent.setType(MenuExecutor.getMimeType(type)); intent.putExtra(Intent.EXTRA_STREAM, manager.getContentUri(path)); mShareActionProvider.setShareIntent(intent); mPendingSharePath = null; } else { // This happens when ActionBar is not created yet. mPendingSharePath = path; } } private void setTitle(String title) { if (title == null) return; boolean showTitle = mActivity.getAndroidContext().getResources().getBoolean( R.bool.show_action_bar_title); if (showTitle) mActionBar.setTitle(title); else mActionBar.setTitle(""); } private void updateCurrentPhoto(MediaItem photo) { if (mCurrentPhoto == photo) return; mCurrentPhoto = photo; if (mCurrentPhoto == null) return; updateMenuOperations(); if (mShowDetails) { mDetailsHelper.reloadDetails(mModel.getCurrentIndex()); } setTitle(photo.getName()); mPhotoView.showVideoPlayIcon( photo.getMediaType() == MediaObject.MEDIA_TYPE_VIDEO); updateShareURI(photo.getPath()); } private void updateMenuOperations() { if (mMenu == null) return; MenuItem item = mMenu.findItem(R.id.action_slideshow); if (item != null) { item.setVisible(canDoSlideShow()); } if (mCurrentPhoto == null) return; int supportedOperations = mCurrentPhoto.getSupportedOperations(); if (!GalleryUtils.isEditorAvailable((Context) mActivity, "image/*")) { supportedOperations &= ~MediaObject.SUPPORT_EDIT; } MenuExecutor.updateMenuOperation(mMenu, supportedOperations); } private boolean canDoSlideShow() { if (mMediaSet == null || mCurrentPhoto == null) { return false; } if (mCurrentPhoto.getMediaType() != MediaObject.MEDIA_TYPE_IMAGE) { return false; } if (mMediaSet instanceof MtpDevice) { return false; } return true; } private void showBars() { if (mShowBars) return; mShowBars = true; mActionBar.show(); WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes(); params.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE; ((Activity) mActivity).getWindow().setAttributes(params); if (mFilmStripView != null) { mFilmStripView.show(); } } private void hideBars() { if (!mShowBars) return; mShowBars = false; mActionBar.hide(); WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes(); params.systemUiVisibility = View. SYSTEM_UI_FLAG_LOW_PROFILE; ((Activity) mActivity).getWindow().setAttributes(params); if (mFilmStripView != null) { mFilmStripView.hide(); } } private void refreshHidingMessage() { mHandler.removeMessages(MSG_HIDE_BARS); if (!mIsMenuVisible && !mIsInteracting) { mHandler.sendEmptyMessageDelayed(MSG_HIDE_BARS, HIDE_BARS_TIMEOUT); } } @Override public void onUserInteraction() { showBars(); refreshHidingMessage(); } public void onUserInteractionTap() { if (mShowBars) { hideBars(); mHandler.removeMessages(MSG_HIDE_BARS); } else { showBars(); refreshHidingMessage(); } } @Override public void onUserInteractionBegin() { showBars(); mIsInteracting = true; refreshHidingMessage(); } @Override public void onUserInteractionEnd() { mIsInteracting = false; // This function could be called from GL thread (in SlotView.render) // and post to the main thread. So, it could be executed while the // activity is paused. if (mIsActive) refreshHidingMessage(); } @Override protected void onBackPressed() { if (mShowDetails) { hideDetails(); } else { PositionRepository repository = PositionRepository.getInstance(mActivity); repository.clear(); if (mCurrentPhoto != null) { Position position = new Position(); position.x = mRootPane.getWidth() / 2; position.y = mRootPane.getHeight() / 2; position.z = -1000; repository.putPosition( Long.valueOf(System.identityHashCode(mCurrentPhoto.getPath())), position); } super.onBackPressed(); } } @Override protected boolean onCreateActionBar(Menu menu) { MenuInflater inflater = ((Activity) mActivity).getMenuInflater(); inflater.inflate(R.menu.photo, menu); mShareActionProvider = GalleryActionBar.initializeShareActionProvider(menu); if (mPendingSharePath != null) updateShareURI(mPendingSharePath); mMenu = menu; mShowBars = true; updateMenuOperations(); return true; } @Override protected boolean onItemSelected(MenuItem item) { MediaItem current = mModel.getCurrentMediaItem(); if (current == null) { // item is not ready, ignore return true; } int currentIndex = mModel.getCurrentIndex(); Path path = current.getPath(); DataManager manager = mActivity.getDataManager(); int action = item.getItemId(); switch (action) { case R.id.action_slideshow: { Bundle data = new Bundle(); data.putString(SlideshowPage.KEY_SET_PATH, mMediaSet.getPath().toString()); data.putString(SlideshowPage.KEY_ITEM_PATH, path.toString()); data.putInt(SlideshowPage.KEY_PHOTO_INDEX, currentIndex); data.putBoolean(SlideshowPage.KEY_REPEAT, true); mActivity.getStateManager().startStateForResult( SlideshowPage.class, REQUEST_SLIDESHOW, data); return true; } case R.id.action_crop: { Activity activity = (Activity) mActivity; Intent intent = new Intent(CropImage.CROP_ACTION); intent.setClass(activity, CropImage.class); intent.setData(manager.getContentUri(path)); activity.startActivityForResult(intent, PicasaSource.isPicasaImage(current) ? REQUEST_CROP_PICASA : REQUEST_CROP); return true; } case R.id.action_details: { if (mShowDetails) { hideDetails(); } else { showDetails(currentIndex); } return true; } case R.id.action_setas: case R.id.action_confirm_delete: case R.id.action_rotate_ccw: case R.id.action_rotate_cw: case R.id.action_show_on_map: case R.id.action_edit: mSelectionManager.deSelectAll(); mSelectionManager.toggle(path); mMenuExecutor.onMenuClicked(item, null); return true; case R.id.action_import: mSelectionManager.deSelectAll(); mSelectionManager.toggle(path); mMenuExecutor.onMenuClicked(item, new ImportCompleteListener(mActivity)); return true; default : return false; } } private void hideDetails() { mShowDetails = false; mDetailsHelper.hide(); } private void showDetails(int index) { mShowDetails = true; if (mDetailsHelper == null) { mDetailsHelper = new DetailsHelper(mActivity, mRootPane, new MyDetailsSource()); mDetailsHelper.setCloseListener(new CloseListener() { public void onClose() { hideDetails(); } }); } mDetailsHelper.reloadDetails(index); mDetailsHelper.show(); } public void onSingleTapUp(int x, int y) { MediaItem item = mModel.getCurrentMediaItem(); if (item == null) { // item is not ready, ignore return; } boolean playVideo = (item.getSupportedOperations() & MediaItem.SUPPORT_PLAY) != 0; if (playVideo) { // determine if the point is at center (1/6) of the photo view. // (The position of the "play" icon is at center (1/6) of the photo) int w = mPhotoView.getWidth(); int h = mPhotoView.getHeight(); playVideo = (Math.abs(x - w / 2) * 12 <= w) && (Math.abs(y - h / 2) * 12 <= h); } if (playVideo) { playVideo((Activity) mActivity, item.getPlayUri(), item.getName()); } else { onUserInteractionTap(); } } public static void playVideo(Activity activity, Uri uri, String title) { try { Intent intent = new Intent(Intent.ACTION_VIEW) .setDataAndType(uri, "video/*"); intent.putExtra(Intent.EXTRA_TITLE, title); activity.startActivity(intent); } catch (ActivityNotFoundException e) { Toast.makeText(activity, activity.getString(R.string.video_err), Toast.LENGTH_SHORT).show(); } } // Called by FileStripView. // Returns false if it cannot jump to the specified index at this time. public boolean onSlotSelected(int slotIndex) { return mPhotoView.jumpTo(slotIndex); } @Override protected void onStateResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case REQUEST_CROP: if (resultCode == Activity.RESULT_OK) { if (data == null) break; Path path = mApplication .getDataManager().findPathByUri(data.getData()); if (path != null) { mModel.setCurrentPhoto(path, mCurrentIndex); } } break; case REQUEST_CROP_PICASA: { int message = resultCode == Activity.RESULT_OK ? R.string.crop_saved : R.string.crop_not_saved; Toast.makeText(mActivity.getAndroidContext(), message, Toast.LENGTH_SHORT).show(); break; } case REQUEST_SLIDESHOW: { if (data == null) break; String path = data.getStringExtra(SlideshowPage.KEY_ITEM_PATH); int index = data.getIntExtra(SlideshowPage.KEY_PHOTO_INDEX, 0); if (path != null) { mModel.setCurrentPhoto(Path.fromString(path), index); } } } } @Override public void onPause() { super.onPause(); mIsActive = false; if (mFilmStripView != null) { mFilmStripView.pause(); } DetailsHelper.pause(); mPhotoView.pause(); mModel.pause(); mHandler.removeMessages(MSG_HIDE_BARS); mActionBar.removeOnMenuVisibilityListener(mMenuVisibilityListener); mMenuExecutor.pause(); } @Override protected void onResume() { super.onResume(); mIsActive = true; setContentPane(mRootPane); mModel.resume(); mPhotoView.resume(); if (mFilmStripView != null) { mFilmStripView.resume(); } if (mMenuVisibilityListener == null) { mMenuVisibilityListener = new MyMenuVisibilityListener(); } mActionBar.addOnMenuVisibilityListener(mMenuVisibilityListener); onUserInteraction(); } private class MyDetailsSource implements DetailsSource { private int mIndex; @Override public MediaDetails getDetails() { return mModel.getCurrentMediaItem().getDetails(); } @Override public int size() { return mMediaSet != null ? mMediaSet.getMediaItemCount() : 1; } @Override public int findIndex(int indexHint) { mIndex = indexHint; return indexHint; } @Override public int getIndex() { return mIndex; } } }