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