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