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