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