Home | History | Annotate | Download | only in app
      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 
     17 package com.android.gallery3d.app;
     18 
     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.nfc.NfcAdapter;
     28 import android.os.Bundle;
     29 import android.os.Handler;
     30 import android.os.Message;
     31 import android.view.Menu;
     32 import android.view.MenuInflater;
     33 import android.view.MenuItem;
     34 import android.view.View;
     35 import android.view.WindowManager;
     36 import android.widget.ShareActionProvider;
     37 import android.widget.Toast;
     38 
     39 import com.android.gallery3d.R;
     40 import com.android.gallery3d.common.Utils;
     41 import com.android.gallery3d.data.DataManager;
     42 import com.android.gallery3d.data.FilterDeleteSet;
     43 import com.android.gallery3d.data.MediaDetails;
     44 import com.android.gallery3d.data.MediaItem;
     45 import com.android.gallery3d.data.MediaObject;
     46 import com.android.gallery3d.data.MediaSet;
     47 import com.android.gallery3d.data.MtpSource;
     48 import com.android.gallery3d.data.Path;
     49 import com.android.gallery3d.data.SnailAlbum;
     50 import com.android.gallery3d.data.SnailItem;
     51 import com.android.gallery3d.data.SnailSource;
     52 import com.android.gallery3d.picasasource.PicasaSource;
     53 import com.android.gallery3d.ui.DetailsHelper;
     54 import com.android.gallery3d.ui.DetailsHelper.CloseListener;
     55 import com.android.gallery3d.ui.DetailsHelper.DetailsSource;
     56 import com.android.gallery3d.ui.GLCanvas;
     57 import com.android.gallery3d.ui.GLRoot;
     58 import com.android.gallery3d.ui.GLRoot.OnGLIdleListener;
     59 import com.android.gallery3d.ui.GLView;
     60 import com.android.gallery3d.ui.ImportCompleteListener;
     61 import com.android.gallery3d.ui.MenuExecutor;
     62 import com.android.gallery3d.ui.PhotoFallbackEffect;
     63 import com.android.gallery3d.ui.PhotoView;
     64 import com.android.gallery3d.ui.SelectionManager;
     65 import com.android.gallery3d.ui.SynchronizedHandler;
     66 import com.android.gallery3d.util.GalleryUtils;
     67 import com.android.gallery3d.util.MediaSetUtils;
     68 
     69 public class PhotoPage extends ActivityState implements
     70         PhotoView.Listener, OrientationManager.Listener, AppBridge.Server {
     71     private static final String TAG = "PhotoPage";
     72 
     73     private static final int MSG_HIDE_BARS = 1;
     74     private static final int MSG_LOCK_ORIENTATION = 2;
     75     private static final int MSG_UNLOCK_ORIENTATION = 3;
     76     private static final int MSG_ON_FULL_SCREEN_CHANGED = 4;
     77     private static final int MSG_UPDATE_ACTION_BAR = 5;
     78     private static final int MSG_UNFREEZE_GLROOT = 6;
     79     private static final int MSG_WANT_BARS = 7;
     80 
     81     private static final int HIDE_BARS_TIMEOUT = 3500;
     82     private static final int UNFREEZE_GLROOT_TIMEOUT = 250;
     83 
     84     private static final int REQUEST_SLIDESHOW = 1;
     85     private static final int REQUEST_CROP = 2;
     86     private static final int REQUEST_CROP_PICASA = 3;
     87     private static final int REQUEST_EDIT = 4;
     88     private static final int REQUEST_PLAY_VIDEO = 5;
     89 
     90     public static final String KEY_MEDIA_SET_PATH = "media-set-path";
     91     public static final String KEY_MEDIA_ITEM_PATH = "media-item-path";
     92     public static final String KEY_INDEX_HINT = "index-hint";
     93     public static final String KEY_OPEN_ANIMATION_RECT = "open-animation-rect";
     94     public static final String KEY_APP_BRIDGE = "app-bridge";
     95     public static final String KEY_TREAT_BACK_AS_UP = "treat-back-as-up";
     96 
     97     public static final String KEY_RETURN_INDEX_HINT = "return-index-hint";
     98 
     99     private GalleryApp mApplication;
    100     private SelectionManager mSelectionManager;
    101 
    102     private PhotoView mPhotoView;
    103     private PhotoPage.Model mModel;
    104     private DetailsHelper mDetailsHelper;
    105     private boolean mShowDetails;
    106     private Path mPendingSharePath;
    107 
    108     // mMediaSet could be null if there is no KEY_MEDIA_SET_PATH supplied.
    109     // E.g., viewing a photo in gmail attachment
    110     private FilterDeleteSet mMediaSet;
    111     private Menu mMenu;
    112 
    113     private int mCurrentIndex = 0;
    114     private Handler mHandler;
    115     private boolean mShowBars = true;
    116     private volatile boolean mActionBarAllowed = true;
    117     private GalleryActionBar mActionBar;
    118     private MyMenuVisibilityListener mMenuVisibilityListener;
    119     private boolean mIsMenuVisible;
    120     private MediaItem mCurrentPhoto = null;
    121     private MenuExecutor mMenuExecutor;
    122     private boolean mIsActive;
    123     private ShareActionProvider mShareActionProvider;
    124     private String mSetPathString;
    125     // This is the original mSetPathString before adding the camera preview item.
    126     private String mOriginalSetPathString;
    127     private AppBridge mAppBridge;
    128     private SnailItem mScreenNailItem;
    129     private SnailAlbum mScreenNailSet;
    130     private OrientationManager mOrientationManager;
    131     private boolean mHasActivityResult;
    132     private boolean mTreatBackAsUp;
    133 
    134     // The item that is deleted (but it can still be undeleted before commiting)
    135     private Path mDeletePath;
    136     private boolean mDeleteIsFocus;  // whether the deleted item was in focus
    137 
    138     private NfcAdapter mNfcAdapter;
    139 
    140     public static interface Model extends PhotoView.Model {
    141         public void resume();
    142         public void pause();
    143         public boolean isEmpty();
    144         public void setCurrentPhoto(Path path, int indexHint);
    145     }
    146 
    147     private class MyMenuVisibilityListener implements OnMenuVisibilityListener {
    148         @Override
    149         public void onMenuVisibilityChanged(boolean isVisible) {
    150             mIsMenuVisible = isVisible;
    151             refreshHidingMessage();
    152         }
    153     }
    154 
    155     private final GLView mRootPane = new GLView() {
    156 
    157         @Override
    158         protected void renderBackground(GLCanvas view) {
    159             view.clearBuffer();
    160         }
    161 
    162         @Override
    163         protected void onLayout(
    164                 boolean changed, int left, int top, int right, int bottom) {
    165             mPhotoView.layout(0, 0, right - left, bottom - top);
    166             if (mShowDetails) {
    167                 mDetailsHelper.layout(left, mActionBar.getHeight(), right, bottom);
    168             }
    169         }
    170     };
    171 
    172     @Override
    173     public void onCreate(Bundle data, Bundle restoreState) {
    174         mActionBar = mActivity.getGalleryActionBar();
    175         mSelectionManager = new SelectionManager(mActivity, false);
    176         mMenuExecutor = new MenuExecutor(mActivity, mSelectionManager);
    177 
    178         mPhotoView = new PhotoView(mActivity);
    179         mPhotoView.setListener(this);
    180         mRootPane.addComponent(mPhotoView);
    181         mApplication = (GalleryApp)((Activity) mActivity).getApplication();
    182         mOrientationManager = mActivity.getOrientationManager();
    183         mOrientationManager.addListener(this);
    184         mActivity.getGLRoot().setOrientationSource(mOrientationManager);
    185 
    186         mSetPathString = data.getString(KEY_MEDIA_SET_PATH);
    187         mOriginalSetPathString = mSetPathString;
    188         mNfcAdapter = NfcAdapter.getDefaultAdapter(mActivity.getAndroidContext());
    189         Path itemPath = Path.fromString(data.getString(KEY_MEDIA_ITEM_PATH));
    190         mTreatBackAsUp = data.getBoolean(KEY_TREAT_BACK_AS_UP, false);
    191 
    192         if (mSetPathString != null) {
    193             mAppBridge = (AppBridge) data.getParcelable(KEY_APP_BRIDGE);
    194             if (mAppBridge != null) {
    195                 mAppBridge.setServer(this);
    196                 mOrientationManager.lockOrientation();
    197 
    198                 // Get the ScreenNail from AppBridge and register it.
    199                 int id = SnailSource.newId();
    200                 Path screenNailSetPath = SnailSource.getSetPath(id);
    201                 Path screenNailItemPath = SnailSource.getItemPath(id);
    202                 mScreenNailSet = (SnailAlbum) mActivity.getDataManager()
    203                         .getMediaObject(screenNailSetPath);
    204                 mScreenNailItem = (SnailItem) mActivity.getDataManager()
    205                         .getMediaObject(screenNailItemPath);
    206                 mScreenNailItem.setScreenNail(mAppBridge.attachScreenNail());
    207 
    208                 // Combine the original MediaSet with the one for ScreenNail
    209                 // from AppBridge.
    210                 mSetPathString = "/combo/item/{" + screenNailSetPath +
    211                         "," + mSetPathString + "}";
    212 
    213                 // Start from the screen nail.
    214                 itemPath = screenNailItemPath;
    215 
    216                 // Action bar should not be displayed when camera starts.
    217                 mFlags |= FLAG_HIDE_ACTION_BAR | FLAG_HIDE_STATUS_BAR;
    218                 mShowBars = false;
    219             }
    220 
    221             MediaSet originalSet = mActivity.getDataManager()
    222                     .getMediaSet(mSetPathString);
    223             mSelectionManager.setSourceMediaSet(originalSet);
    224             mSetPathString = "/filter/delete/{" + mSetPathString + "}";
    225             mMediaSet = (FilterDeleteSet) mActivity.getDataManager()
    226                     .getMediaSet(mSetPathString);
    227             mCurrentIndex = data.getInt(KEY_INDEX_HINT, 0);
    228             if (mMediaSet == null) {
    229                 Log.w(TAG, "failed to restore " + mSetPathString);
    230             }
    231             PhotoDataAdapter pda = new PhotoDataAdapter(
    232                     mActivity, mPhotoView, mMediaSet, itemPath, mCurrentIndex,
    233                     mAppBridge == null ? -1 : 0,
    234                     mAppBridge == null ? false : mAppBridge.isPanorama());
    235             mModel = pda;
    236             mPhotoView.setModel(mModel);
    237 
    238             pda.setDataListener(new PhotoDataAdapter.DataListener() {
    239 
    240                 @Override
    241                 public void onPhotoChanged(int index, Path item) {
    242                     mCurrentIndex = index;
    243                     if (item != null) {
    244                         MediaItem photo = mModel.getMediaItem(0);
    245                         if (photo != null) updateCurrentPhoto(photo);
    246                     }
    247                     updateBars();
    248                 }
    249 
    250                 @Override
    251                 public void onLoadingFinished() {
    252                     if (!mModel.isEmpty()) {
    253                         MediaItem photo = mModel.getMediaItem(0);
    254                         if (photo != null) updateCurrentPhoto(photo);
    255                     } else if (mIsActive) {
    256                         // We only want to finish the PhotoPage if there is no
    257                         // deletion that the user can undo.
    258                         if (mMediaSet.getNumberOfDeletions() == 0) {
    259                             mActivity.getStateManager().finishState(
    260                                     PhotoPage.this);
    261                         }
    262                     }
    263                 }
    264 
    265                 @Override
    266                 public void onLoadingStarted() {
    267                 }
    268             });
    269         } else {
    270             // Get default media set by the URI
    271             MediaItem mediaItem = (MediaItem)
    272                     mActivity.getDataManager().getMediaObject(itemPath);
    273             mModel = new SinglePhotoDataAdapter(mActivity, mPhotoView, mediaItem);
    274             mPhotoView.setModel(mModel);
    275             updateCurrentPhoto(mediaItem);
    276         }
    277 
    278         mHandler = new SynchronizedHandler(mActivity.getGLRoot()) {
    279             @Override
    280             public void handleMessage(Message message) {
    281                 switch (message.what) {
    282                     case MSG_HIDE_BARS: {
    283                         hideBars();
    284                         break;
    285                     }
    286                     case MSG_LOCK_ORIENTATION: {
    287                         mOrientationManager.lockOrientation();
    288                         break;
    289                     }
    290                     case MSG_UNLOCK_ORIENTATION: {
    291                         mOrientationManager.unlockOrientation();
    292                         break;
    293                     }
    294                     case MSG_ON_FULL_SCREEN_CHANGED: {
    295                         mAppBridge.onFullScreenChanged(message.arg1 == 1);
    296                         break;
    297                     }
    298                     case MSG_UPDATE_ACTION_BAR: {
    299                         updateBars();
    300                         break;
    301                     }
    302                     case MSG_WANT_BARS: {
    303                         wantBars();
    304                         break;
    305                     }
    306                     case MSG_UNFREEZE_GLROOT: {
    307                         mActivity.getGLRoot().unfreeze();
    308                         break;
    309                     }
    310                     default: throw new AssertionError(message.what);
    311                 }
    312             }
    313         };
    314 
    315         // start the opening animation only if it's not restored.
    316         if (restoreState == null) {
    317             mPhotoView.setOpenAnimationRect((Rect) data.getParcelable(KEY_OPEN_ANIMATION_RECT));
    318         }
    319     }
    320 
    321     private void updateShareURI(Path path) {
    322         if (mShareActionProvider != null) {
    323             DataManager manager = mActivity.getDataManager();
    324             int type = manager.getMediaType(path);
    325             Intent intent = new Intent(Intent.ACTION_SEND);
    326             intent.setType(MenuExecutor.getMimeType(type));
    327             intent.putExtra(Intent.EXTRA_STREAM, manager.getContentUri(path));
    328             mShareActionProvider.setShareIntent(intent);
    329             if (mNfcAdapter != null) {
    330                 mNfcAdapter.setBeamPushUris(new Uri[]{manager.getContentUri(path)},
    331                         (Activity)mActivity);
    332             }
    333             mPendingSharePath = null;
    334         } else {
    335             // This happens when ActionBar is not created yet.
    336             mPendingSharePath = path;
    337         }
    338     }
    339 
    340     private void updateCurrentPhoto(MediaItem photo) {
    341         if (mCurrentPhoto == photo) return;
    342         mCurrentPhoto = photo;
    343         if (mCurrentPhoto == null) return;
    344         updateMenuOperations();
    345         updateTitle();
    346         if (mShowDetails) {
    347             mDetailsHelper.reloadDetails(mModel.getCurrentIndex());
    348         }
    349         if ((photo.getSupportedOperations() & MediaItem.SUPPORT_SHARE) != 0) {
    350             updateShareURI(photo.getPath());
    351         }
    352     }
    353 
    354     private void updateTitle() {
    355         if (mCurrentPhoto == null) return;
    356         boolean showTitle = mActivity.getAndroidContext().getResources().getBoolean(
    357                 R.bool.show_action_bar_title);
    358         if (showTitle && mCurrentPhoto.getName() != null)
    359             mActionBar.setTitle(mCurrentPhoto.getName());
    360         else
    361             mActionBar.setTitle("");
    362     }
    363 
    364     private void updateMenuOperations() {
    365         if (mMenu == null) return;
    366         MenuItem item = mMenu.findItem(R.id.action_slideshow);
    367         if (item != null) {
    368             item.setVisible(canDoSlideShow());
    369         }
    370         if (mCurrentPhoto == null) return;
    371         int supportedOperations = mCurrentPhoto.getSupportedOperations();
    372         if (!GalleryUtils.isEditorAvailable((Context) mActivity, "image/*")) {
    373             supportedOperations &= ~MediaObject.SUPPORT_EDIT;
    374         }
    375 
    376         MenuExecutor.updateMenuOperation(mMenu, supportedOperations);
    377     }
    378 
    379     private boolean canDoSlideShow() {
    380         if (mMediaSet == null || mCurrentPhoto == null) {
    381             return false;
    382         }
    383         if (mCurrentPhoto.getMediaType() != MediaObject.MEDIA_TYPE_IMAGE) {
    384             return false;
    385         }
    386         if (MtpSource.isMtpPath(mOriginalSetPathString)) {
    387             return false;
    388         }
    389         return true;
    390     }
    391 
    392     //////////////////////////////////////////////////////////////////////////
    393     //  Action Bar show/hide management
    394     //////////////////////////////////////////////////////////////////////////
    395 
    396     private void showBars() {
    397         if (mShowBars) return;
    398         mShowBars = true;
    399         mOrientationManager.unlockOrientation();
    400         mActionBar.show();
    401         mActivity.getGLRoot().setLightsOutMode(false);
    402         refreshHidingMessage();
    403     }
    404 
    405     private void hideBars() {
    406         if (!mShowBars) return;
    407         mShowBars = false;
    408         mActionBar.hide();
    409         mActivity.getGLRoot().setLightsOutMode(true);
    410         mHandler.removeMessages(MSG_HIDE_BARS);
    411     }
    412 
    413     private void refreshHidingMessage() {
    414         mHandler.removeMessages(MSG_HIDE_BARS);
    415         if (!mIsMenuVisible) {
    416             mHandler.sendEmptyMessageDelayed(MSG_HIDE_BARS, HIDE_BARS_TIMEOUT);
    417         }
    418     }
    419 
    420     private boolean canShowBars() {
    421         // No bars if we are showing camera preview.
    422         if (mAppBridge != null && mCurrentIndex == 0) return false;
    423         // No bars if it's not allowed.
    424         if (!mActionBarAllowed) return false;
    425 
    426         return true;
    427     }
    428 
    429     private void wantBars() {
    430         if (canShowBars()) showBars();
    431     }
    432 
    433     private void toggleBars() {
    434         if (mShowBars) {
    435             hideBars();
    436         } else {
    437             if (canShowBars()) showBars();
    438         }
    439     }
    440 
    441     private void updateBars() {
    442         if (!canShowBars()) {
    443             hideBars();
    444         }
    445     }
    446 
    447     @Override
    448     public void onOrientationCompensationChanged() {
    449         mActivity.getGLRoot().requestLayoutContentPane();
    450     }
    451 
    452     @Override
    453     protected void onBackPressed() {
    454         if (mShowDetails) {
    455             hideDetails();
    456         } else if (mAppBridge == null || !switchWithCaptureAnimation(-1)) {
    457             // We are leaving this page. Set the result now.
    458             setResult();
    459             if (mTreatBackAsUp) {
    460                 onUpPressed();
    461             } else {
    462                 super.onBackPressed();
    463             }
    464         }
    465     }
    466 
    467     private void onUpPressed() {
    468         if (mActivity.getStateManager().getStateCount() > 1) {
    469             super.onBackPressed();
    470             return;
    471         }
    472 
    473         if (mOriginalSetPathString == null) return;
    474 
    475         if (mAppBridge == null) {
    476             // We're in view mode so set up the stacks on our own.
    477             Bundle data = new Bundle(getData());
    478             data.putString(AlbumPage.KEY_MEDIA_PATH, mOriginalSetPathString);
    479             data.putString(AlbumPage.KEY_PARENT_MEDIA_PATH,
    480                     mActivity.getDataManager().getTopSetPath(
    481                             DataManager.INCLUDE_ALL));
    482             mActivity.getStateManager().switchState(this, AlbumPage.class, data);
    483         } else {
    484             // Start the real gallery activity to view the camera roll.
    485             Uri uri = Uri.parse("content://media/external/file?bucketId="
    486                     + MediaSetUtils.CAMERA_BUCKET_ID);
    487             Intent intent = new Intent(Intent.ACTION_VIEW);
    488             intent.setDataAndType(uri, ContentResolver.CURSOR_DIR_BASE_TYPE + "/image");
    489             ((Activity) mActivity).startActivity(intent);
    490         }
    491     }
    492 
    493     private void setResult() {
    494         Intent result = null;
    495         if (!mPhotoView.getFilmMode()) {
    496             result = new Intent();
    497             result.putExtra(KEY_RETURN_INDEX_HINT, mCurrentIndex);
    498         }
    499         setStateResult(Activity.RESULT_OK, result);
    500     }
    501 
    502     //////////////////////////////////////////////////////////////////////////
    503     //  AppBridge.Server interface
    504     //////////////////////////////////////////////////////////////////////////
    505 
    506     @Override
    507     public void setCameraRelativeFrame(Rect frame) {
    508         mPhotoView.setCameraRelativeFrame(frame);
    509     }
    510 
    511     @Override
    512     public boolean switchWithCaptureAnimation(int offset) {
    513         return mPhotoView.switchWithCaptureAnimation(offset);
    514     }
    515 
    516     @Override
    517     public void setSwipingEnabled(boolean enabled) {
    518         mPhotoView.setSwipingEnabled(enabled);
    519     }
    520 
    521     @Override
    522     public void notifyScreenNailChanged() {
    523         mScreenNailItem.setScreenNail(mAppBridge.attachScreenNail());
    524         mScreenNailSet.notifyChange();
    525     }
    526 
    527     @Override
    528     protected boolean onCreateActionBar(Menu menu) {
    529         MenuInflater inflater = ((Activity) mActivity).getMenuInflater();
    530         inflater.inflate(R.menu.photo, menu);
    531         mShareActionProvider = GalleryActionBar.initializeShareActionProvider(menu);
    532         if (mPendingSharePath != null) updateShareURI(mPendingSharePath);
    533         mMenu = menu;
    534         updateMenuOperations();
    535         updateTitle();
    536         return true;
    537     }
    538 
    539     private MenuExecutor.ProgressListener mConfirmDialogListener =
    540             new MenuExecutor.ProgressListener() {
    541         @Override
    542         public void onProgressUpdate(int index) {}
    543 
    544         @Override
    545         public void onProgressComplete(int result) {}
    546 
    547         @Override
    548         public void onConfirmDialogShown() {
    549             mHandler.removeMessages(MSG_HIDE_BARS);
    550         }
    551 
    552         @Override
    553         public void onConfirmDialogDismissed(boolean confirmed) {
    554             refreshHidingMessage();
    555         }
    556     };
    557 
    558     @Override
    559     protected boolean onItemSelected(MenuItem item) {
    560         refreshHidingMessage();
    561         MediaItem current = mModel.getMediaItem(0);
    562 
    563         if (current == null) {
    564             // item is not ready, ignore
    565             return true;
    566         }
    567 
    568         int currentIndex = mModel.getCurrentIndex();
    569         Path path = current.getPath();
    570 
    571         DataManager manager = mActivity.getDataManager();
    572         int action = item.getItemId();
    573         String confirmMsg = null;
    574         switch (action) {
    575             case android.R.id.home: {
    576                 onUpPressed();
    577                 return true;
    578             }
    579             case R.id.action_slideshow: {
    580                 Bundle data = new Bundle();
    581                 data.putString(SlideshowPage.KEY_SET_PATH, mMediaSet.getPath().toString());
    582                 data.putString(SlideshowPage.KEY_ITEM_PATH, path.toString());
    583                 data.putInt(SlideshowPage.KEY_PHOTO_INDEX, currentIndex);
    584                 data.putBoolean(SlideshowPage.KEY_REPEAT, true);
    585                 mActivity.getStateManager().startStateForResult(
    586                         SlideshowPage.class, REQUEST_SLIDESHOW, data);
    587                 return true;
    588             }
    589             case R.id.action_crop: {
    590                 Activity activity = (Activity) mActivity;
    591                 Intent intent = new Intent(CropImage.CROP_ACTION);
    592                 intent.setClass(activity, CropImage.class);
    593                 intent.setData(manager.getContentUri(path));
    594                 activity.startActivityForResult(intent, PicasaSource.isPicasaImage(current)
    595                         ? REQUEST_CROP_PICASA
    596                         : REQUEST_CROP);
    597                 return true;
    598             }
    599             case R.id.action_edit: {
    600                 Intent intent = new Intent(Intent.ACTION_EDIT)
    601                         .setData(manager.getContentUri(path))
    602                         .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    603                 ((Activity) mActivity).startActivityForResult(Intent.createChooser(intent, null),
    604                         REQUEST_EDIT);
    605                 return true;
    606             }
    607             case R.id.action_details: {
    608                 if (mShowDetails) {
    609                     hideDetails();
    610                 } else {
    611                     showDetails(currentIndex);
    612                 }
    613                 return true;
    614             }
    615             case R.id.action_delete:
    616                 confirmMsg = mActivity.getResources().getQuantityString(
    617                         R.plurals.delete_selection, 1);
    618             case R.id.action_setas:
    619             case R.id.action_rotate_ccw:
    620             case R.id.action_rotate_cw:
    621             case R.id.action_show_on_map:
    622                 mSelectionManager.deSelectAll();
    623                 mSelectionManager.toggle(path);
    624                 mMenuExecutor.onMenuClicked(item, confirmMsg, mConfirmDialogListener);
    625                 return true;
    626             case R.id.action_import:
    627                 mSelectionManager.deSelectAll();
    628                 mSelectionManager.toggle(path);
    629                 mMenuExecutor.onMenuClicked(item, confirmMsg,
    630                         new ImportCompleteListener(mActivity));
    631                 return true;
    632             default :
    633                 return false;
    634         }
    635     }
    636 
    637     private void hideDetails() {
    638         mShowDetails = false;
    639         mDetailsHelper.hide();
    640     }
    641 
    642     private void showDetails(int index) {
    643         mShowDetails = true;
    644         if (mDetailsHelper == null) {
    645             mDetailsHelper = new DetailsHelper(mActivity, mRootPane, new MyDetailsSource());
    646             mDetailsHelper.setCloseListener(new CloseListener() {
    647                 @Override
    648                 public void onClose() {
    649                     hideDetails();
    650                 }
    651             });
    652         }
    653         mDetailsHelper.reloadDetails(index);
    654         mDetailsHelper.show();
    655     }
    656 
    657     ////////////////////////////////////////////////////////////////////////////
    658     //  Callbacks from PhotoView
    659     ////////////////////////////////////////////////////////////////////////////
    660     @Override
    661     public void onSingleTapUp(int x, int y) {
    662         if (mAppBridge != null) {
    663             if (mAppBridge.onSingleTapUp(x, y)) return;
    664         }
    665 
    666         MediaItem item = mModel.getMediaItem(0);
    667         if (item == null || item == mScreenNailItem) {
    668             // item is not ready or it is camera preview, ignore
    669             return;
    670         }
    671 
    672         boolean playVideo =
    673                 (item.getSupportedOperations() & MediaItem.SUPPORT_PLAY) != 0;
    674 
    675         if (playVideo) {
    676             // determine if the point is at center (1/6) of the photo view.
    677             // (The position of the "play" icon is at center (1/6) of the photo)
    678             int w = mPhotoView.getWidth();
    679             int h = mPhotoView.getHeight();
    680             playVideo = (Math.abs(x - w / 2) * 12 <= w)
    681                 && (Math.abs(y - h / 2) * 12 <= h);
    682         }
    683 
    684         if (playVideo) {
    685             playVideo((Activity) mActivity, item.getPlayUri(), item.getName());
    686         } else {
    687             toggleBars();
    688         }
    689     }
    690 
    691     @Override
    692     public void lockOrientation() {
    693         mHandler.sendEmptyMessage(MSG_LOCK_ORIENTATION);
    694     }
    695 
    696     @Override
    697     public void unlockOrientation() {
    698         mHandler.sendEmptyMessage(MSG_UNLOCK_ORIENTATION);
    699     }
    700 
    701     @Override
    702     public void onActionBarAllowed(boolean allowed) {
    703         mActionBarAllowed = allowed;
    704         mHandler.sendEmptyMessage(MSG_UPDATE_ACTION_BAR);
    705     }
    706 
    707     @Override
    708     public void onActionBarWanted() {
    709         mHandler.sendEmptyMessage(MSG_WANT_BARS);
    710     }
    711 
    712     @Override
    713     public void onFullScreenChanged(boolean full) {
    714         Message m = mHandler.obtainMessage(
    715                 MSG_ON_FULL_SCREEN_CHANGED, full ? 1 : 0, 0);
    716         m.sendToTarget();
    717     }
    718 
    719     // How we do delete/undo:
    720     //
    721     // When the user choose to delete a media item, we just tell the
    722     // FilterDeleteSet to hide that item. If the user choose to undo it, we
    723     // again tell FilterDeleteSet not to hide it. If the user choose to commit
    724     // the deletion, we then actually delete the media item.
    725     @Override
    726     public void onDeleteImage(Path path, int offset) {
    727         onCommitDeleteImage();  // commit the previous deletion
    728         mDeletePath = path;
    729         mDeleteIsFocus = (offset == 0);
    730         mMediaSet.addDeletion(path, mCurrentIndex + offset);
    731     }
    732 
    733     @Override
    734     public void onUndoDeleteImage() {
    735         if (mDeletePath == null) return;
    736         // If the deletion was done on the focused item, we want the model to
    737         // focus on it when it is undeleted.
    738         if (mDeleteIsFocus) mModel.setFocusHintPath(mDeletePath);
    739         mMediaSet.removeDeletion(mDeletePath);
    740         mDeletePath = null;
    741     }
    742 
    743     @Override
    744     public void onCommitDeleteImage() {
    745         if (mDeletePath == null) return;
    746         mSelectionManager.deSelectAll();
    747         mSelectionManager.toggle(mDeletePath);
    748         mMenuExecutor.onMenuClicked(R.id.action_delete, null, true, false);
    749         mDeletePath = null;
    750     }
    751 
    752     public static void playVideo(Activity activity, Uri uri, String title) {
    753         try {
    754             Intent intent = new Intent(Intent.ACTION_VIEW)
    755                     .setDataAndType(uri, "video/*")
    756                     .putExtra(Intent.EXTRA_TITLE, title)
    757                     .putExtra(MovieActivity.KEY_TREAT_UP_AS_BACK, true);
    758             activity.startActivityForResult(intent, REQUEST_PLAY_VIDEO);
    759         } catch (ActivityNotFoundException e) {
    760             Toast.makeText(activity, activity.getString(R.string.video_err),
    761                     Toast.LENGTH_SHORT).show();
    762         }
    763     }
    764 
    765     private void setCurrentPhotoByIntent(Intent intent) {
    766         if (intent == null) return;
    767         Path path = mApplication.getDataManager()
    768                 .findPathByUri(intent.getData(), intent.getType());
    769         if (path != null) {
    770             mModel.setCurrentPhoto(path, mCurrentIndex);
    771         }
    772     }
    773 
    774     @Override
    775     protected void onStateResult(int requestCode, int resultCode, Intent data) {
    776         mHasActivityResult = true;
    777         switch (requestCode) {
    778             case REQUEST_EDIT:
    779                 setCurrentPhotoByIntent(data);
    780                 break;
    781             case REQUEST_CROP:
    782                 if (resultCode == Activity.RESULT_OK) {
    783                     setCurrentPhotoByIntent(data);
    784                 }
    785                 break;
    786             case REQUEST_CROP_PICASA: {
    787                 if (resultCode == Activity.RESULT_OK) {
    788                     Context context = mActivity.getAndroidContext();
    789                     String message = context.getString(R.string.crop_saved,
    790                             context.getString(R.string.folder_download));
    791                     Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
    792                 }
    793                 break;
    794             }
    795             case REQUEST_SLIDESHOW: {
    796                 if (data == null) break;
    797                 String path = data.getStringExtra(SlideshowPage.KEY_ITEM_PATH);
    798                 int index = data.getIntExtra(SlideshowPage.KEY_PHOTO_INDEX, 0);
    799                 if (path != null) {
    800                     mModel.setCurrentPhoto(Path.fromString(path), index);
    801                 }
    802             }
    803         }
    804     }
    805 
    806     private class PreparePhotoFallback implements OnGLIdleListener {
    807         private PhotoFallbackEffect mPhotoFallback = new PhotoFallbackEffect();
    808         private boolean mResultReady = false;
    809 
    810         public synchronized PhotoFallbackEffect get() {
    811             while (!mResultReady) {
    812                 Utils.waitWithoutInterrupt(this);
    813             }
    814             return mPhotoFallback;
    815         }
    816 
    817         @Override
    818         public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) {
    819             mPhotoFallback = mPhotoView.buildFallbackEffect(mRootPane, canvas);
    820             synchronized (this) {
    821                 mResultReady = true;
    822                 notifyAll();
    823             }
    824             return false;
    825         }
    826     }
    827 
    828     private void preparePhotoFallbackView() {
    829         GLRoot root = mActivity.getGLRoot();
    830         PreparePhotoFallback task = new PreparePhotoFallback();
    831         root.unlockRenderThread();
    832         PhotoFallbackEffect anim;
    833         try {
    834             root.addOnGLIdleListener(task);
    835             anim = task.get();
    836         } finally {
    837             root.lockRenderThread();
    838         }
    839         mActivity.getTransitionStore().put(
    840                 AlbumPage.KEY_RESUME_ANIMATION, anim);
    841     }
    842 
    843     @Override
    844     public void onPause() {
    845         super.onPause();
    846         mIsActive = false;
    847 
    848         mActivity.getGLRoot().unfreeze();
    849         mHandler.removeMessages(MSG_UNFREEZE_GLROOT);
    850         if (isFinishing()) preparePhotoFallbackView();
    851 
    852         DetailsHelper.pause();
    853         mPhotoView.pause();
    854         mModel.pause();
    855         mHandler.removeMessages(MSG_HIDE_BARS);
    856         mActionBar.removeOnMenuVisibilityListener(mMenuVisibilityListener);
    857 
    858         onCommitDeleteImage();
    859         mMenuExecutor.pause();
    860         if (mMediaSet != null) mMediaSet.clearDeletion();
    861     }
    862 
    863     @Override
    864     public void onCurrentImageUpdated() {
    865         mActivity.getGLRoot().unfreeze();
    866     }
    867 
    868     @Override
    869     protected void onResume() {
    870         super.onResume();
    871         mActivity.getGLRoot().freeze();
    872         mIsActive = true;
    873         setContentPane(mRootPane);
    874 
    875         mModel.resume();
    876         mPhotoView.resume();
    877         if (mMenuVisibilityListener == null) {
    878             mMenuVisibilityListener = new MyMenuVisibilityListener();
    879         }
    880         mActionBar.setDisplayOptions(mSetPathString != null, true);
    881         mActionBar.addOnMenuVisibilityListener(mMenuVisibilityListener);
    882 
    883         if (mAppBridge != null && !mHasActivityResult) {
    884             mPhotoView.resetToFirstPicture();
    885         }
    886         mHasActivityResult = false;
    887         mHandler.sendEmptyMessageDelayed(MSG_UNFREEZE_GLROOT, UNFREEZE_GLROOT_TIMEOUT);
    888     }
    889 
    890     @Override
    891     protected void onDestroy() {
    892         if (mAppBridge != null) {
    893             mAppBridge.setServer(null);
    894             mScreenNailItem.setScreenNail(null);
    895             mAppBridge.detachScreenNail();
    896             mAppBridge = null;
    897             mScreenNailSet = null;
    898             mScreenNailItem = null;
    899         }
    900         mOrientationManager.removeListener(this);
    901         mActivity.getGLRoot().setOrientationSource(null);
    902 
    903         // Remove all pending messages.
    904         mHandler.removeCallbacksAndMessages(null);
    905         super.onDestroy();
    906     }
    907 
    908     private class MyDetailsSource implements DetailsSource {
    909         private int mIndex;
    910 
    911         @Override
    912         public MediaDetails getDetails() {
    913             return mModel.getMediaItem(0).getDetails();
    914         }
    915 
    916         @Override
    917         public int size() {
    918             return mMediaSet != null ? mMediaSet.getMediaItemCount() : 1;
    919         }
    920 
    921         @Override
    922         public int findIndex(int indexHint) {
    923             mIndex = indexHint;
    924             return indexHint;
    925         }
    926 
    927         @Override
    928         public int getIndex() {
    929             return mIndex;
    930         }
    931     }
    932 }
    933