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