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