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