PhotoPage.java revision 8fe22ecb8e744a09a2e0d867f876ecbf74c82298
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.content.pm.PackageManager;
26import android.graphics.Rect;
27import android.net.Uri;
28import android.nfc.NfcAdapter;
29import android.nfc.NfcAdapter.CreateBeamUrisCallback;
30import android.nfc.NfcEvent;
31import android.os.Bundle;
32import android.os.Handler;
33import android.os.Message;
34import android.view.animation.AccelerateInterpolator;
35import android.widget.RelativeLayout;
36import android.widget.Toast;
37
38import com.actionbarsherlock.app.ActionBar.OnMenuVisibilityListener;
39import com.actionbarsherlock.view.Menu;
40import com.actionbarsherlock.view.MenuItem;
41import com.android.gallery3d.R;
42import com.android.gallery3d.anim.FloatAnimation;
43import com.android.gallery3d.common.ApiHelper;
44import com.android.gallery3d.common.Utils;
45import com.android.gallery3d.data.DataManager;
46import com.android.gallery3d.data.FilterDeleteSet;
47import com.android.gallery3d.data.MediaDetails;
48import com.android.gallery3d.data.MediaItem;
49import com.android.gallery3d.data.MediaObject;
50import com.android.gallery3d.data.MediaSet;
51import com.android.gallery3d.data.MtpSource;
52import com.android.gallery3d.data.Path;
53import com.android.gallery3d.data.SecureAlbum;
54import com.android.gallery3d.data.SecureSource;
55import com.android.gallery3d.data.SnailAlbum;
56import com.android.gallery3d.data.SnailItem;
57import com.android.gallery3d.data.SnailSource;
58import com.android.gallery3d.picasasource.PicasaSource;
59import com.android.gallery3d.ui.AnimationTime;
60import com.android.gallery3d.ui.BitmapScreenNail;
61import com.android.gallery3d.ui.DetailsHelper;
62import com.android.gallery3d.ui.DetailsHelper.CloseListener;
63import com.android.gallery3d.ui.DetailsHelper.DetailsSource;
64import com.android.gallery3d.ui.GLCanvas;
65import com.android.gallery3d.ui.GLRoot;
66import com.android.gallery3d.ui.GLRoot.OnGLIdleListener;
67import com.android.gallery3d.ui.GLView;
68import com.android.gallery3d.ui.ImportCompleteListener;
69import com.android.gallery3d.ui.MenuExecutor;
70import com.android.gallery3d.ui.PhotoFallbackEffect;
71import com.android.gallery3d.ui.PhotoView;
72import com.android.gallery3d.ui.PreparePageFadeoutTexture;
73import com.android.gallery3d.ui.RawTexture;
74import com.android.gallery3d.ui.SelectionManager;
75import com.android.gallery3d.ui.SynchronizedHandler;
76import com.android.gallery3d.util.GalleryUtils;
77import com.android.gallery3d.util.LightCycleHelper;
78import com.android.gallery3d.util.MediaSetUtils;
79
80public class PhotoPage extends ActivityState implements
81        PhotoView.Listener, OrientationManager.Listener, AppBridge.Server,
82        PhotoPageBottomControls.Delegate {
83    private static final String TAG = "PhotoPage";
84
85    private static final int MSG_HIDE_BARS = 1;
86    private static final int MSG_LOCK_ORIENTATION = 2;
87    private static final int MSG_UNLOCK_ORIENTATION = 3;
88    private static final int MSG_ON_FULL_SCREEN_CHANGED = 4;
89    private static final int MSG_UPDATE_ACTION_BAR = 5;
90    private static final int MSG_UNFREEZE_GLROOT = 6;
91    private static final int MSG_WANT_BARS = 7;
92    private static final int MSG_REFRESH_GRID_BUTTON = 8;
93    private static final int MSG_REFRESH_BOTTOM_CONTROLS = 9;
94
95    private static final int HIDE_BARS_TIMEOUT = 3500;
96    private static final int UNFREEZE_GLROOT_TIMEOUT = 250;
97
98    private static final int REQUEST_SLIDESHOW = 1;
99    private static final int REQUEST_CROP = 2;
100    private static final int REQUEST_CROP_PICASA = 3;
101    private static final int REQUEST_EDIT = 4;
102    private static final int REQUEST_PLAY_VIDEO = 5;
103    private static final int REQUEST_TRIM = 6;
104
105    public static final String KEY_MEDIA_SET_PATH = "media-set-path";
106    public static final String KEY_MEDIA_ITEM_PATH = "media-item-path";
107    public static final String KEY_INDEX_HINT = "index-hint";
108    public static final String KEY_OPEN_ANIMATION_RECT = "open-animation-rect";
109    public static final String KEY_APP_BRIDGE = "app-bridge";
110    public static final String KEY_TREAT_BACK_AS_UP = "treat-back-as-up";
111    public static final String KEY_START_IN_FILMSTRIP = "start-in-filmstrip";
112    public static final String KEY_RETURN_INDEX_HINT = "return-index-hint";
113    public static final String KEY_SHOW_WHEN_LOCKED = "show_when_locked";
114
115    public static final String KEY_ALBUMPAGE_TRANSITION = "albumpage-transition";
116    public static final int MSG_ALBUMPAGE_NONE = 0;
117    public static final int MSG_ALBUMPAGE_STARTED = 1;
118    public static final int MSG_ALBUMPAGE_RESUMED = 2;
119    public static final int MSG_ALBUMPAGE_PICKED = 4;
120
121    public static final String ACTION_NEXTGEN_EDIT = "action_nextgen_edit";
122
123    private GalleryApp mApplication;
124    private SelectionManager mSelectionManager;
125
126    private PhotoView mPhotoView;
127    private PhotoPage.Model mModel;
128    private DetailsHelper mDetailsHelper;
129    private boolean mShowDetails;
130
131    // mMediaSet could be null if there is no KEY_MEDIA_SET_PATH supplied.
132    // E.g., viewing a photo in gmail attachment
133    private FilterDeleteSet mMediaSet;
134
135    // The mediaset used by camera launched from secure lock screen.
136    private SecureAlbum mSecureAlbum;
137
138    private int mCurrentIndex = 0;
139    private Handler mHandler;
140    private boolean mShowBars = true;
141    private volatile boolean mActionBarAllowed = true;
142    private GalleryActionBar mActionBar;
143    private boolean mIsMenuVisible;
144    private boolean mHaveImageEditor;
145    private PhotoPageBottomControls mBottomControls;
146    private MediaItem mCurrentPhoto = null;
147    private MenuExecutor mMenuExecutor;
148    private boolean mIsActive;
149    private String mSetPathString;
150    // This is the original mSetPathString before adding the camera preview item.
151    private String mOriginalSetPathString;
152    private AppBridge mAppBridge;
153    private SnailItem mScreenNailItem;
154    private SnailAlbum mScreenNailSet;
155    private OrientationManager mOrientationManager;
156    private boolean mHasActivityResult;
157    private boolean mTreatBackAsUp;
158    private boolean mStartInFilmstrip;
159    private boolean mStartedFromAlbumPage;
160
161    private RawTexture mFadeOutTexture;
162    private Rect mOpenAnimationRect;
163    public static final int ANIM_TIME_OPENING = 300;
164
165    // The item that is deleted (but it can still be undeleted before commiting)
166    private Path mDeletePath;
167    private boolean mDeleteIsFocus;  // whether the deleted item was in focus
168
169    private Uri[] mNfcPushUris = new Uri[1];
170
171    private final MyMenuVisibilityListener mMenuVisibilityListener =
172            new MyMenuVisibilityListener();
173
174    public static interface Model extends PhotoView.Model {
175        public void resume();
176        public void pause();
177        public boolean isEmpty();
178        public void setCurrentPhoto(Path path, int indexHint);
179    }
180
181    private class MyMenuVisibilityListener implements OnMenuVisibilityListener {
182        @Override
183        public void onMenuVisibilityChanged(boolean isVisible) {
184            mIsMenuVisible = isVisible;
185            refreshHidingMessage();
186        }
187    }
188
189    private static class BackgroundFadeOut extends FloatAnimation {
190        public BackgroundFadeOut() {
191            super(1f, 0f, ANIM_TIME_OPENING);
192            setInterpolator(new AccelerateInterpolator(2f));
193        }
194    }
195
196    private final FloatAnimation mBackgroundFade = new BackgroundFadeOut();
197
198    @Override
199    protected int getBackgroundColorId() {
200        return R.color.photo_background;
201    }
202
203    private final GLView mRootPane = new GLView() {
204        @Override
205        protected void renderBackground(GLCanvas view) {
206            if (mFadeOutTexture != null) {
207                if (mBackgroundFade.calculate(AnimationTime.get())) invalidate();
208                if (!mBackgroundFade.isActive()) {
209                    mFadeOutTexture = null;
210                    mOpenAnimationRect = null;
211                    BitmapScreenNail.enableDrawPlaceholder();
212                } else {
213                    float fadeAlpha = mBackgroundFade.get();
214                    if (fadeAlpha < 1f) {
215                        view.clearBuffer(getBackgroundColor());
216                        view.setAlpha(fadeAlpha);
217                    }
218                    mFadeOutTexture.draw(view, 0, 0);
219                    view.setAlpha(1f - fadeAlpha);
220                    return;
221                }
222            }
223            view.clearBuffer(getBackgroundColor());
224        }
225
226        @Override
227        protected void onLayout(
228                boolean changed, int left, int top, int right, int bottom) {
229            mPhotoView.layout(0, 0, right - left, bottom - top);
230            if (mShowDetails) {
231                mDetailsHelper.layout(left, mActionBar.getHeight(), right, bottom);
232            }
233        }
234    };
235
236    @Override
237    public void onCreate(Bundle data, Bundle restoreState) {
238        super.onCreate(data, restoreState);
239        mActionBar = mActivity.getGalleryActionBar();
240        mSelectionManager = new SelectionManager(mActivity, false);
241        mMenuExecutor = new MenuExecutor(mActivity, mSelectionManager);
242
243        mPhotoView = new PhotoView(mActivity);
244        mPhotoView.setListener(this);
245        mRootPane.addComponent(mPhotoView);
246        mApplication = (GalleryApp) ((Activity) mActivity).getApplication();
247        mOrientationManager = mActivity.getOrientationManager();
248        mOrientationManager.addListener(this);
249        mActivity.getGLRoot().setOrientationSource(mOrientationManager);
250
251        mHandler = new SynchronizedHandler(mActivity.getGLRoot()) {
252            @Override
253            public void handleMessage(Message message) {
254                switch (message.what) {
255                    case MSG_HIDE_BARS: {
256                        hideBars();
257                        break;
258                    }
259                    case MSG_REFRESH_GRID_BUTTON: {
260                        setGridButtonVisibility(mPhotoView.getFilmMode());
261                        break;
262                    }
263                    case MSG_REFRESH_BOTTOM_CONTROLS: {
264                        if (mBottomControls != null) mBottomControls.refresh();
265                        break;
266                    }
267                    case MSG_LOCK_ORIENTATION: {
268                        mOrientationManager.lockOrientation();
269                        break;
270                    }
271                    case MSG_UNLOCK_ORIENTATION: {
272                        mOrientationManager.unlockOrientation();
273                        break;
274                    }
275                    case MSG_ON_FULL_SCREEN_CHANGED: {
276                        mAppBridge.onFullScreenChanged(message.arg1 == 1);
277                        break;
278                    }
279                    case MSG_UPDATE_ACTION_BAR: {
280                        updateBars();
281                        break;
282                    }
283                    case MSG_WANT_BARS: {
284                        wantBars();
285                        break;
286                    }
287                    case MSG_UNFREEZE_GLROOT: {
288                        mActivity.getGLRoot().unfreeze();
289                        break;
290                    }
291                    default: throw new AssertionError(message.what);
292                }
293            }
294        };
295
296        mSetPathString = data.getString(KEY_MEDIA_SET_PATH);
297        mOriginalSetPathString = mSetPathString;
298        setupNfcBeamPush();
299        String itemPathString = data.getString(KEY_MEDIA_ITEM_PATH);
300        Path itemPath = itemPathString != null ?
301                Path.fromString(data.getString(KEY_MEDIA_ITEM_PATH)) :
302                    null;
303        mTreatBackAsUp = data.getBoolean(KEY_TREAT_BACK_AS_UP, false);
304        mStartInFilmstrip =
305            data.getBoolean(KEY_START_IN_FILMSTRIP, false);
306        mStartedFromAlbumPage =
307                data.getInt(KEY_ALBUMPAGE_TRANSITION,
308                        MSG_ALBUMPAGE_NONE) == MSG_ALBUMPAGE_STARTED;
309        setGridButtonVisibility(!mStartedFromAlbumPage);
310        if (mSetPathString != null) {
311            mAppBridge = (AppBridge) data.getParcelable(KEY_APP_BRIDGE);
312            if (mAppBridge != null) {
313                mShowBars = false;
314
315                mAppBridge.setServer(this);
316                mOrientationManager.lockOrientation();
317
318                // Get the ScreenNail from AppBridge and register it.
319                int id = SnailSource.newId();
320                Path screenNailSetPath = SnailSource.getSetPath(id);
321                Path screenNailItemPath = SnailSource.getItemPath(id);
322                mScreenNailSet = (SnailAlbum) mActivity.getDataManager()
323                        .getMediaObject(screenNailSetPath);
324                mScreenNailItem = (SnailItem) mActivity.getDataManager()
325                        .getMediaObject(screenNailItemPath);
326                mScreenNailItem.setScreenNail(mAppBridge.attachScreenNail());
327
328                // Check if the path is a secure album.
329                if (SecureSource.isSecurePath(mSetPathString)) {
330                    mSecureAlbum = (SecureAlbum) mActivity.getDataManager()
331                            .getMediaSet(mSetPathString);
332                }
333                if (data.getBoolean(KEY_SHOW_WHEN_LOCKED, false)) {
334                    // Set the flag to be on top of the lock screen.
335                    mFlags |= FLAG_SHOW_WHEN_LOCKED;
336                }
337
338                // Combine the original MediaSet with the one for ScreenNail
339                // from AppBridge.
340                mSetPathString = "/combo/item/{" + screenNailSetPath +
341                        "," + mSetPathString + "}";
342
343                // Start from the screen nail.
344                itemPath = screenNailItemPath;
345            }
346
347            MediaSet originalSet = mActivity.getDataManager()
348                    .getMediaSet(mSetPathString);
349            mSelectionManager.setSourceMediaSet(originalSet);
350            mSetPathString = "/filter/delete/{" + mSetPathString + "}";
351            mMediaSet = (FilterDeleteSet) mActivity.getDataManager()
352                    .getMediaSet(mSetPathString);
353            mCurrentIndex = data.getInt(KEY_INDEX_HINT, 0);
354            if (mMediaSet == null) {
355                Log.w(TAG, "failed to restore " + mSetPathString);
356            }
357            if (itemPath == null) {
358                int mediaItemCount = mMediaSet.getMediaItemCount();
359                if (mediaItemCount > 0) {
360                    if (mCurrentIndex >= mediaItemCount) mCurrentIndex = 0;
361                    itemPath = mMediaSet.getMediaItem(mCurrentIndex, 1)
362                        .get(0).getPath();
363                } else {
364                    // Bail out, PhotoPage can't load on an empty album
365                    return;
366                }
367            }
368            PhotoDataAdapter pda = new PhotoDataAdapter(
369                    mActivity, mPhotoView, mMediaSet, itemPath, mCurrentIndex,
370                    mAppBridge == null ? -1 : 0,
371                    mAppBridge == null ? false : mAppBridge.isPanorama(),
372                    mAppBridge == null ? false : mAppBridge.isStaticCamera());
373            mModel = pda;
374            mPhotoView.setModel(mModel);
375
376            pda.setDataListener(new PhotoDataAdapter.DataListener() {
377
378                @Override
379                public void onPhotoChanged(int index, Path item) {
380                    int oldIndex = mCurrentIndex;
381                    mCurrentIndex = index;
382                    if (item != null) {
383                        MediaItem photo = mModel.getMediaItem(0);
384                        if (photo != null) updateCurrentPhoto(photo);
385                    }
386                    if (mAppBridge != null) {
387                        if (oldIndex == 0 && mCurrentIndex > 0
388                                && !mPhotoView.getFilmMode()) {
389                            mPhotoView.setFilmMode(true);
390                        }
391                    }
392                    updateBars();
393
394                    // Reset the timeout for the bars after a swipe
395                    refreshHidingMessage();
396                }
397
398                @Override
399                public void onLoadingFinished() {
400                    if (!mModel.isEmpty()) {
401                        MediaItem photo = mModel.getMediaItem(0);
402                        if (photo != null) updateCurrentPhoto(photo);
403                    } else if (mIsActive) {
404                        // We only want to finish the PhotoPage if there is no
405                        // deletion that the user can undo.
406                        if (mMediaSet.getNumberOfDeletions() == 0) {
407                            mActivity.getStateManager().finishState(
408                                    PhotoPage.this);
409                        }
410                    }
411                }
412
413                @Override
414                public void onLoadingStarted() {
415                }
416            });
417        } else {
418            // Get default media set by the URI
419            MediaItem mediaItem = (MediaItem)
420                    mActivity.getDataManager().getMediaObject(itemPath);
421            mModel = new SinglePhotoDataAdapter(mActivity, mPhotoView, mediaItem);
422            mPhotoView.setModel(mModel);
423            updateCurrentPhoto(mediaItem);
424        }
425
426        mPhotoView.setFilmMode(mStartInFilmstrip && mMediaSet.getMediaItemCount() > 1);
427        if (mSecureAlbum == null) {
428            RelativeLayout galleryRoot = (RelativeLayout) ((Activity) mActivity)
429                        .findViewById(mAppBridge != null ? R.id.content : R.id.gallery_root);
430            if (galleryRoot != null) {
431                mBottomControls = new PhotoPageBottomControls(this, mActivity, galleryRoot);
432            }
433        }
434    }
435
436    public boolean canDisplayBottomControls() {
437        return mShowBars && !mPhotoView.getFilmMode();
438    }
439
440    public boolean canDisplayBottomControl(int control) {
441        if (mCurrentPhoto == null) return false;
442        switch(control) {
443            case R.id.photopage_bottom_control_edit:
444                return mCurrentPhoto.getMediaType() == MediaObject.MEDIA_TYPE_IMAGE;
445            case R.id.photopage_bottom_control_panorama:
446                return (mCurrentPhoto.getSupportedOperations()
447                        & MediaItem.SUPPORT_PANORAMA) != 0;
448            default:
449                return false;
450        }
451    }
452
453    public void onBottomControlClicked(int control) {
454        switch(control) {
455            case R.id.photopage_bottom_control_edit:
456                launchPhotoEditor();
457                return;
458            case R.id.photopage_bottom_control_panorama:
459                LightCycleHelper.viewPanorama(mActivity, mCurrentPhoto.getContentUri());
460                return;
461            default:
462                return;
463        }
464    }
465
466    @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
467    private void setupNfcBeamPush() {
468        if (!ApiHelper.HAS_SET_BEAM_PUSH_URIS) return;
469
470        NfcAdapter adapter = NfcAdapter.getDefaultAdapter(mActivity);
471        if (adapter != null) {
472            adapter.setBeamPushUrisCallback(new CreateBeamUrisCallback() {
473                @Override
474                public Uri[] createBeamUris(NfcEvent event) {
475                    return mNfcPushUris;
476                }
477            }, mActivity);
478        }
479    }
480
481    private void setNfcBeamPushUri(Uri uri) {
482        mNfcPushUris[0] = uri;
483    }
484
485    private Intent createShareIntent(Path path) {
486        DataManager manager = mActivity.getDataManager();
487        int type = manager.getMediaType(path);
488        int support = manager.getSupportedOperations(path);
489        boolean isPanorama = (support & MediaObject.SUPPORT_PANORAMA) != 0;
490        Intent intent = new Intent(Intent.ACTION_SEND);
491        intent.setType(MenuExecutor.getMimeType(type, isPanorama));
492        Uri uri = manager.getContentUri(path);
493        intent.putExtra(Intent.EXTRA_STREAM, uri);
494        return intent;
495
496    }
497
498    private void launchPhotoEditor() {
499        MediaItem current = mModel.getMediaItem(0);
500        if (current == null) return;
501
502        Intent intent = new Intent(ACTION_NEXTGEN_EDIT);
503        intent.setData(mActivity.getDataManager().getContentUri(current.getPath())).setFlags(
504                Intent.FLAG_GRANT_READ_URI_PERMISSION);
505        if (mActivity.getPackageManager()
506                .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY).size() == 0) {
507            intent.setAction(Intent.ACTION_EDIT);
508        }
509        ((Activity) mActivity).startActivityForResult(Intent.createChooser(intent, null),
510                REQUEST_EDIT);
511    }
512
513    private void updateShareURI(Path path) {
514        DataManager manager = mActivity.getDataManager();
515        Uri uri = manager.getContentUri(path);
516        mActionBar.setShareIntent(createShareIntent(path));
517        setNfcBeamPushUri(uri);
518    }
519
520    private void updateCurrentPhoto(MediaItem photo) {
521        if (mCurrentPhoto == photo) return;
522        mCurrentPhoto = photo;
523        if (mCurrentPhoto == null) return;
524        updateMenuOperations();
525        updateTitle();
526        if (mBottomControls != null) mBottomControls.refresh();
527        if (mShowDetails) {
528            mDetailsHelper.reloadDetails();
529        }
530        if ((mSecureAlbum == null)
531                && (photo.getSupportedOperations() & MediaItem.SUPPORT_SHARE) != 0) {
532            updateShareURI(photo.getPath());
533        }
534    }
535
536    private void updateTitle() {
537        if (mCurrentPhoto == null) return;
538        boolean showTitle = mActivity.getAndroidContext().getResources().getBoolean(
539                R.bool.show_action_bar_title);
540        if (showTitle && mCurrentPhoto.getName() != null) {
541            mActionBar.setTitle(mCurrentPhoto.getName());
542        } else {
543            mActionBar.setTitle("");
544        }
545    }
546
547    private void updateMenuOperations() {
548        Menu menu = mActionBar.getMenu();
549
550        // it could be null if onCreateActionBar has not been called yet
551        if (menu == null) return;
552
553        setGridButtonVisibility(mPhotoView.getFilmMode());
554
555        MenuItem item = menu.findItem(R.id.action_slideshow);
556        item.setVisible((mSecureAlbum == null) && canDoSlideShow());
557        if (mCurrentPhoto == null) return;
558
559        int supportedOperations = mCurrentPhoto.getSupportedOperations();
560        if (mSecureAlbum != null) {
561            supportedOperations &= MediaObject.SUPPORT_DELETE;
562        } else if (!mHaveImageEditor) {
563            supportedOperations &= ~MediaObject.SUPPORT_EDIT;
564        }
565        MenuExecutor.updateMenuOperation(menu, supportedOperations);
566    }
567
568    private boolean canDoSlideShow() {
569        if (mMediaSet == null || mCurrentPhoto == null) {
570            return false;
571        }
572        if (mCurrentPhoto.getMediaType() != MediaObject.MEDIA_TYPE_IMAGE) {
573            return false;
574        }
575        if (MtpSource.isMtpPath(mOriginalSetPathString)) {
576            return false;
577        }
578        return true;
579    }
580
581    //////////////////////////////////////////////////////////////////////////
582    //  Action Bar show/hide management
583    //////////////////////////////////////////////////////////////////////////
584
585    private void showBars() {
586        if (mShowBars) return;
587        mShowBars = true;
588        mOrientationManager.unlockOrientation();
589        mActionBar.show();
590        mActivity.getGLRoot().setLightsOutMode(false);
591        refreshHidingMessage();
592        if (mBottomControls != null) mBottomControls.refresh();
593    }
594
595    private void hideBars() {
596        if (!mShowBars) return;
597        mShowBars = false;
598        mActionBar.hide();
599        mActivity.getGLRoot().setLightsOutMode(true);
600        mHandler.removeMessages(MSG_HIDE_BARS);
601        if (mBottomControls != null) mBottomControls.refresh();
602    }
603
604    private void refreshHidingMessage() {
605        mHandler.removeMessages(MSG_HIDE_BARS);
606        if (!mIsMenuVisible && !mPhotoView.getFilmMode()) {
607            mHandler.sendEmptyMessageDelayed(MSG_HIDE_BARS, HIDE_BARS_TIMEOUT);
608        }
609    }
610
611    private boolean canShowBars() {
612        // No bars if we are showing camera preview.
613        if (mAppBridge != null && mCurrentIndex == 0
614                && !mPhotoView.getFilmMode()) return false;
615
616        // No bars if it's not allowed.
617        if (!mActionBarAllowed) return false;
618
619        return true;
620    }
621
622    private void wantBars() {
623        if (canShowBars()) showBars();
624    }
625
626    private void toggleBars() {
627        if (mShowBars) {
628            hideBars();
629        } else {
630            if (canShowBars()) showBars();
631        }
632    }
633
634    private void updateBars() {
635        if (!canShowBars()) {
636            hideBars();
637        }
638    }
639
640    @Override
641    public void onOrientationCompensationChanged() {
642        mActivity.getGLRoot().requestLayoutContentPane();
643    }
644
645    @Override
646    protected void onBackPressed() {
647        if (mShowDetails) {
648            hideDetails();
649        } else if (mAppBridge == null || !switchWithCaptureAnimation(-1)) {
650            // We are leaving this page. Set the result now.
651            setResult();
652            if (mStartInFilmstrip && !mPhotoView.getFilmMode()) {
653                mPhotoView.setFilmMode(true);
654            } else if (mTreatBackAsUp) {
655                onUpPressed();
656            } else {
657                super.onBackPressed();
658            }
659        }
660    }
661
662    private void onUpPressed() {
663        if ((mStartInFilmstrip || mAppBridge != null)
664                && !mPhotoView.getFilmMode()) {
665            mPhotoView.setFilmMode(true);
666            return;
667        }
668
669        if (mActivity.getStateManager().getStateCount() > 1) {
670            setResult();
671            super.onBackPressed();
672            return;
673        }
674
675        if (mOriginalSetPathString == null) return;
676
677        if (mAppBridge == null) {
678            // We're in view mode so set up the stacks on our own.
679            Bundle data = new Bundle(getData());
680            data.putString(AlbumPage.KEY_MEDIA_PATH, mOriginalSetPathString);
681            data.putString(AlbumPage.KEY_PARENT_MEDIA_PATH,
682                    mActivity.getDataManager().getTopSetPath(
683                            DataManager.INCLUDE_ALL));
684            mActivity.getStateManager().switchState(this, AlbumPage.class, data);
685        } else {
686            GalleryUtils.startGalleryActivity(mActivity);
687        }
688    }
689
690    private void setResult() {
691        Intent result = null;
692        result = new Intent();
693        result.putExtra(KEY_RETURN_INDEX_HINT, mCurrentIndex);
694        setStateResult(Activity.RESULT_OK, result);
695    }
696
697    //////////////////////////////////////////////////////////////////////////
698    //  AppBridge.Server interface
699    //////////////////////////////////////////////////////////////////////////
700
701    @Override
702    public void setCameraRelativeFrame(Rect frame) {
703        mPhotoView.setCameraRelativeFrame(frame);
704    }
705
706    @Override
707    public boolean switchWithCaptureAnimation(int offset) {
708        return mPhotoView.switchWithCaptureAnimation(offset);
709    }
710
711    @Override
712    public void setSwipingEnabled(boolean enabled) {
713        mPhotoView.setSwipingEnabled(enabled);
714    }
715
716    @Override
717    public void notifyScreenNailChanged() {
718        mScreenNailItem.setScreenNail(mAppBridge.attachScreenNail());
719        mScreenNailSet.notifyChange();
720    }
721
722    @Override
723    public void addSecureAlbumItem(boolean isVideo, int id) {
724        mSecureAlbum.addMediaItem(isVideo, id);
725    }
726
727    @Override
728    protected boolean onCreateActionBar(Menu menu) {
729        mActionBar.createActionBarMenu(R.menu.photo, menu);
730        mHaveImageEditor = GalleryUtils.isEditorAvailable(mActivity, "image/*");
731        updateMenuOperations();
732        updateTitle();
733        return true;
734    }
735
736    private MenuExecutor.ProgressListener mConfirmDialogListener =
737            new MenuExecutor.ProgressListener() {
738        @Override
739        public void onProgressUpdate(int index) {}
740
741        @Override
742        public void onProgressComplete(int result) {}
743
744        @Override
745        public void onConfirmDialogShown() {
746            mHandler.removeMessages(MSG_HIDE_BARS);
747        }
748
749        @Override
750        public void onConfirmDialogDismissed(boolean confirmed) {
751            refreshHidingMessage();
752        }
753
754        @Override
755        public void onProgressStart() {}
756    };
757
758    @Override
759    protected boolean onItemSelected(MenuItem item) {
760        if (mModel == null) return true;
761        refreshHidingMessage();
762        MediaItem current = mModel.getMediaItem(0);
763
764        if (current == null) {
765            // item is not ready, ignore
766            return true;
767        }
768
769        int currentIndex = mModel.getCurrentIndex();
770        Path path = current.getPath();
771
772        DataManager manager = mActivity.getDataManager();
773        int action = item.getItemId();
774        String confirmMsg = null;
775        switch (action) {
776            case android.R.id.home: {
777                onUpPressed();
778                return true;
779            }
780            case R.id.action_grid: {
781                if (mStartedFromAlbumPage) {
782                    onUpPressed();
783                } else {
784                    preparePhotoFallbackView();
785                    Bundle data = new Bundle(getData());
786                    data.putString(AlbumPage.KEY_MEDIA_PATH, mOriginalSetPathString);
787                    data.putString(AlbumPage.KEY_PARENT_MEDIA_PATH,
788                            mActivity.getDataManager().getTopSetPath(
789                                    DataManager.INCLUDE_ALL));
790
791                    // We only show cluster menu in the first AlbumPage in stack
792                    // TODO: Enable this when running from the camera app
793                    boolean inAlbum = mActivity.getStateManager().hasStateClass(AlbumPage.class);
794                    data.putBoolean(AlbumPage.KEY_SHOW_CLUSTER_MENU, !inAlbum
795                            && mAppBridge == null);
796
797                    data.putBoolean(PhotoPage.KEY_APP_BRIDGE, mAppBridge != null);
798
799                    // Account for live preview being first item
800                    mActivity.getTransitionStore().put(KEY_RETURN_INDEX_HINT,
801                            mAppBridge != null ? mCurrentIndex - 1 : mCurrentIndex);
802
803                    mActivity.getStateManager().startState(AlbumPage.class, data);
804                }
805                return true;
806            }
807            case R.id.action_slideshow: {
808                Bundle data = new Bundle();
809                data.putString(SlideshowPage.KEY_SET_PATH, mMediaSet.getPath().toString());
810                data.putString(SlideshowPage.KEY_ITEM_PATH, path.toString());
811                data.putInt(SlideshowPage.KEY_PHOTO_INDEX, currentIndex);
812                data.putBoolean(SlideshowPage.KEY_REPEAT, true);
813                mActivity.getStateManager().startStateForResult(
814                        SlideshowPage.class, REQUEST_SLIDESHOW, data);
815                return true;
816            }
817            case R.id.action_crop: {
818                Activity activity = mActivity;
819                Intent intent = new Intent(CropImage.CROP_ACTION);
820                intent.setClass(activity, CropImage.class);
821                intent.setData(manager.getContentUri(path));
822                activity.startActivityForResult(intent, PicasaSource.isPicasaImage(current)
823                        ? REQUEST_CROP_PICASA
824                        : REQUEST_CROP);
825                return true;
826            }
827            case R.id.action_trim: {
828                Intent intent = new Intent(mActivity, TrimVideo.class);
829                intent.setData(manager.getContentUri(path));
830                // We need the file path to wrap this into a RandomAccessFile.
831                intent.putExtra(KEY_MEDIA_ITEM_PATH, current.getFilePath());
832                mActivity.startActivityForResult(intent, REQUEST_TRIM);
833                return true;
834            }
835            case R.id.action_edit: {
836                launchPhotoEditor();
837                return true;
838            }
839            case R.id.action_details: {
840                if (mShowDetails) {
841                    hideDetails();
842                } else {
843                    showDetails();
844                }
845                return true;
846            }
847            case R.id.action_delete:
848                confirmMsg = mActivity.getResources().getQuantityString(
849                        R.plurals.delete_selection, 1);
850            case R.id.action_setas:
851            case R.id.action_rotate_ccw:
852            case R.id.action_rotate_cw:
853            case R.id.action_show_on_map:
854                mSelectionManager.deSelectAll();
855                mSelectionManager.toggle(path);
856                mMenuExecutor.onMenuClicked(item, confirmMsg, mConfirmDialogListener);
857                return true;
858            case R.id.action_import:
859                mSelectionManager.deSelectAll();
860                mSelectionManager.toggle(path);
861                mMenuExecutor.onMenuClicked(item, confirmMsg,
862                        new ImportCompleteListener(mActivity));
863                return true;
864            case R.id.action_share:
865                Activity activity = mActivity;
866                Intent intent = createShareIntent(mCurrentPhoto.getPath());
867                activity.startActivity(Intent.createChooser(intent,
868                        activity.getString(R.string.share)));
869                return true;
870            default :
871                return false;
872        }
873    }
874
875    private void hideDetails() {
876        mShowDetails = false;
877        mDetailsHelper.hide();
878    }
879
880    private void showDetails() {
881        mShowDetails = true;
882        if (mDetailsHelper == null) {
883            mDetailsHelper = new DetailsHelper(mActivity, mRootPane, new MyDetailsSource());
884            mDetailsHelper.setCloseListener(new CloseListener() {
885                @Override
886                public void onClose() {
887                    hideDetails();
888                }
889            });
890        }
891        mDetailsHelper.show();
892    }
893
894    ////////////////////////////////////////////////////////////////////////////
895    //  Callbacks from PhotoView
896    ////////////////////////////////////////////////////////////////////////////
897    @Override
898    public void onSingleTapUp(int x, int y) {
899        if (mAppBridge != null) {
900            if (mAppBridge.onSingleTapUp(x, y)) return;
901        }
902
903        MediaItem item = mModel.getMediaItem(0);
904        if (item == null || item == mScreenNailItem) {
905            // item is not ready or it is camera preview, ignore
906            return;
907        }
908
909        int supported = item.getSupportedOperations();
910        boolean playVideo = (mSecureAlbum == null) &&
911                ((supported & MediaItem.SUPPORT_PLAY) != 0);
912        boolean viewPanorama = (mSecureAlbum == null) &&
913                ((supported & MediaItem.SUPPORT_PANORAMA) != 0);
914        boolean unlock = ((supported & MediaItem.SUPPORT_UNLOCK) != 0);
915
916        if (playVideo) {
917            // determine if the point is at center (1/6) of the photo view.
918            // (The position of the "play" icon is at center (1/6) of the photo)
919            int w = mPhotoView.getWidth();
920            int h = mPhotoView.getHeight();
921            playVideo = (Math.abs(x - w / 2) * 12 <= w)
922                && (Math.abs(y - h / 2) * 12 <= h);
923        }
924
925        if (playVideo) {
926            playVideo(mActivity, item.getPlayUri(), item.getName());
927        } else if (viewPanorama) {
928            LightCycleHelper.viewPanorama(mActivity, item.getContentUri());
929        } else if (unlock) {
930            mActivity.getStateManager().finishState(this);
931        } else {
932            toggleBars();
933        }
934    }
935
936    @Override
937    public void lockOrientation() {
938        mHandler.sendEmptyMessage(MSG_LOCK_ORIENTATION);
939    }
940
941    @Override
942    public void unlockOrientation() {
943        mHandler.sendEmptyMessage(MSG_UNLOCK_ORIENTATION);
944    }
945
946    @Override
947    public void onActionBarAllowed(boolean allowed) {
948        mActionBarAllowed = allowed;
949        mHandler.sendEmptyMessage(MSG_UPDATE_ACTION_BAR);
950    }
951
952    @Override
953    public void onActionBarWanted() {
954        mHandler.sendEmptyMessage(MSG_WANT_BARS);
955    }
956
957    @Override
958    public void onFullScreenChanged(boolean full) {
959        Message m = mHandler.obtainMessage(
960                MSG_ON_FULL_SCREEN_CHANGED, full ? 1 : 0, 0);
961        m.sendToTarget();
962    }
963
964    // How we do delete/undo:
965    //
966    // When the user choose to delete a media item, we just tell the
967    // FilterDeleteSet to hide that item. If the user choose to undo it, we
968    // again tell FilterDeleteSet not to hide it. If the user choose to commit
969    // the deletion, we then actually delete the media item.
970    @Override
971    public void onDeleteImage(Path path, int offset) {
972        onCommitDeleteImage();  // commit the previous deletion
973        mDeletePath = path;
974        mDeleteIsFocus = (offset == 0);
975        mMediaSet.addDeletion(path, mCurrentIndex + offset);
976    }
977
978    @Override
979    public void onUndoDeleteImage() {
980        if (mDeletePath == null) return;
981        // If the deletion was done on the focused item, we want the model to
982        // focus on it when it is undeleted.
983        if (mDeleteIsFocus) mModel.setFocusHintPath(mDeletePath);
984        mMediaSet.removeDeletion(mDeletePath);
985        mDeletePath = null;
986    }
987
988    @Override
989    public void onCommitDeleteImage() {
990        if (mDeletePath == null) return;
991        mSelectionManager.deSelectAll();
992        mSelectionManager.toggle(mDeletePath);
993        mMenuExecutor.onMenuClicked(R.id.action_delete, null, true, false);
994        mDeletePath = null;
995    }
996
997    public static void playVideo(Activity activity, Uri uri, String title) {
998        try {
999            Intent intent = new Intent(Intent.ACTION_VIEW)
1000                    .setDataAndType(uri, "video/*")
1001                    .putExtra(Intent.EXTRA_TITLE, title)
1002                    .putExtra(MovieActivity.KEY_TREAT_UP_AS_BACK, true);
1003            activity.startActivityForResult(intent, REQUEST_PLAY_VIDEO);
1004        } catch (ActivityNotFoundException e) {
1005            Toast.makeText(activity, activity.getString(R.string.video_err),
1006                    Toast.LENGTH_SHORT).show();
1007        }
1008    }
1009
1010    private void setCurrentPhotoByIntent(Intent intent) {
1011        if (intent == null) return;
1012        Path path = mApplication.getDataManager()
1013                .findPathByUri(intent.getData(), intent.getType());
1014        if (path != null) {
1015            mModel.setCurrentPhoto(path, mCurrentIndex);
1016        }
1017    }
1018
1019    @Override
1020    protected void onStateResult(int requestCode, int resultCode, Intent data) {
1021        mHasActivityResult = true;
1022        switch (requestCode) {
1023            case REQUEST_EDIT:
1024                setCurrentPhotoByIntent(data);
1025                break;
1026            case REQUEST_CROP:
1027                if (resultCode == Activity.RESULT_OK) {
1028                    setCurrentPhotoByIntent(data);
1029                }
1030                break;
1031            case REQUEST_CROP_PICASA: {
1032                if (resultCode == Activity.RESULT_OK) {
1033                    Context context = mActivity.getAndroidContext();
1034                    String message = context.getString(R.string.crop_saved,
1035                            context.getString(R.string.folder_download));
1036                    Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
1037                }
1038                break;
1039            }
1040            case REQUEST_SLIDESHOW: {
1041                if (data == null) break;
1042                String path = data.getStringExtra(SlideshowPage.KEY_ITEM_PATH);
1043                int index = data.getIntExtra(SlideshowPage.KEY_PHOTO_INDEX, 0);
1044                if (path != null) {
1045                    mModel.setCurrentPhoto(Path.fromString(path), index);
1046                }
1047            }
1048        }
1049    }
1050
1051    @Override
1052    protected void clearStateResult() {
1053        mHasActivityResult = false;
1054    }
1055
1056    private class PreparePhotoFallback implements OnGLIdleListener {
1057        private PhotoFallbackEffect mPhotoFallback = new PhotoFallbackEffect();
1058        private boolean mResultReady = false;
1059
1060        public synchronized PhotoFallbackEffect get() {
1061            while (!mResultReady) {
1062                Utils.waitWithoutInterrupt(this);
1063            }
1064            return mPhotoFallback;
1065        }
1066
1067        @Override
1068        public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) {
1069            mPhotoFallback = mPhotoView.buildFallbackEffect(mRootPane, canvas);
1070            synchronized (this) {
1071                mResultReady = true;
1072                notifyAll();
1073            }
1074            return false;
1075        }
1076    }
1077
1078    private void preparePhotoFallbackView() {
1079        GLRoot root = mActivity.getGLRoot();
1080        PreparePhotoFallback task = new PreparePhotoFallback();
1081        root.unlockRenderThread();
1082        PhotoFallbackEffect anim;
1083        try {
1084            root.addOnGLIdleListener(task);
1085            anim = task.get();
1086        } finally {
1087            root.lockRenderThread();
1088        }
1089        mActivity.getTransitionStore().put(
1090                AlbumPage.KEY_RESUME_ANIMATION, anim);
1091    }
1092
1093    @Override
1094    public void onPause() {
1095        super.onPause();
1096        mIsActive = false;
1097
1098        mActivity.getGLRoot().unfreeze();
1099        mHandler.removeMessages(MSG_UNFREEZE_GLROOT);
1100
1101        DetailsHelper.pause();
1102        if (mModel != null) {
1103            if (isFinishing()) preparePhotoFallbackView();
1104            mModel.pause();
1105        }
1106        mPhotoView.pause();
1107        mHandler.removeMessages(MSG_HIDE_BARS);
1108        mActionBar.removeOnMenuVisibilityListener(mMenuVisibilityListener);
1109
1110        onCommitDeleteImage();
1111        mMenuExecutor.pause();
1112        if (mMediaSet != null) mMediaSet.clearDeletion();
1113    }
1114
1115    @Override
1116    public void onCurrentImageUpdated() {
1117        mActivity.getGLRoot().unfreeze();
1118    }
1119
1120    private void setGridButtonVisibility(boolean enabled) {
1121        Menu menu = mActionBar.getMenu();
1122        if (menu == null) return;
1123        MenuItem item = menu.findItem(R.id.action_grid);
1124        if (item != null) item.setVisible((mSecureAlbum == null) && enabled);
1125    }
1126
1127    public void onFilmModeChanged(boolean enabled) {
1128        mHandler.sendEmptyMessage(MSG_REFRESH_GRID_BUTTON);
1129        mHandler.sendEmptyMessage(MSG_REFRESH_BOTTOM_CONTROLS);
1130        if (enabled) {
1131            mHandler.removeMessages(MSG_HIDE_BARS);
1132        } else {
1133            refreshHidingMessage();
1134        }
1135    }
1136
1137    private void transitionFromAlbumPageIfNeeded() {
1138        TransitionStore transitions = mActivity.getTransitionStore();
1139
1140        int albumPageTransition = transitions.get(
1141                KEY_ALBUMPAGE_TRANSITION, MSG_ALBUMPAGE_NONE);
1142
1143        if (albumPageTransition == MSG_ALBUMPAGE_NONE && mAppBridge != null) {
1144            // Generally, resuming the PhotoPage when in Camera should
1145            // reset to the capture mode to allow quick photo taking
1146            mCurrentIndex = 0;
1147            mPhotoView.resetToFirstPicture();
1148        } else {
1149            int resumeIndex = transitions.get(KEY_INDEX_HINT, -1);
1150            if (resumeIndex >= 0) {
1151                if (mAppBridge != null) {
1152                    // Account for live preview being the first item
1153                    resumeIndex++;
1154                }
1155                if (resumeIndex < mMediaSet.getMediaItemCount()) {
1156                    mCurrentIndex = resumeIndex;
1157                    mModel.moveTo(mCurrentIndex);
1158                }
1159            }
1160        }
1161
1162        if (albumPageTransition == MSG_ALBUMPAGE_RESUMED) {
1163            mPhotoView.setFilmMode(mStartInFilmstrip || mAppBridge != null);
1164        } else if (albumPageTransition == MSG_ALBUMPAGE_PICKED) {
1165            mPhotoView.setFilmMode(false);
1166        }
1167
1168        mFadeOutTexture = transitions.get(PreparePageFadeoutTexture.KEY_FADE_TEXTURE);
1169        if (mFadeOutTexture != null) {
1170            mBackgroundFade.start();
1171            BitmapScreenNail.disableDrawPlaceholder();
1172            mOpenAnimationRect =
1173                    albumPageTransition == MSG_ALBUMPAGE_NONE ?
1174                    (Rect) mData.getParcelable(KEY_OPEN_ANIMATION_RECT) :
1175                    (Rect) transitions.get(KEY_OPEN_ANIMATION_RECT);
1176            mPhotoView.setOpenAnimationRect(mOpenAnimationRect);
1177            mBackgroundFade.start();
1178        }
1179    }
1180
1181    @Override
1182    protected void onResume() {
1183        super.onResume();
1184
1185        if (mModel == null) {
1186            mActivity.getStateManager().finishState(this);
1187            return;
1188        }
1189        transitionFromAlbumPageIfNeeded();
1190
1191        mActivity.getGLRoot().freeze();
1192        mIsActive = true;
1193        setContentPane(mRootPane);
1194
1195        mModel.resume();
1196        mPhotoView.resume();
1197        mActionBar.setDisplayOptions(
1198                ((mSecureAlbum == null) && (mSetPathString != null)), true);
1199        mActionBar.addOnMenuVisibilityListener(mMenuVisibilityListener);
1200        if (!mShowBars) {
1201            mActionBar.hide();
1202            mActivity.getGLRoot().setLightsOutMode(true);
1203        }
1204        boolean haveImageEditor = GalleryUtils.isEditorAvailable(mActivity, "image/*");
1205        if (haveImageEditor != mHaveImageEditor) {
1206            mHaveImageEditor = haveImageEditor;
1207            updateMenuOperations();
1208        }
1209
1210        mHasActivityResult = false;
1211        mHandler.sendEmptyMessageDelayed(MSG_UNFREEZE_GLROOT, UNFREEZE_GLROOT_TIMEOUT);
1212    }
1213
1214    @Override
1215    protected void onDestroy() {
1216        if (mAppBridge != null) {
1217            mAppBridge.setServer(null);
1218            mScreenNailItem.setScreenNail(null);
1219            mAppBridge.detachScreenNail();
1220            mAppBridge = null;
1221            mScreenNailSet = null;
1222            mScreenNailItem = null;
1223        }
1224        mOrientationManager.removeListener(this);
1225        mActivity.getGLRoot().setOrientationSource(null);
1226        if (mBottomControls != null) mBottomControls.cleanup();
1227
1228        // Remove all pending messages.
1229        mHandler.removeCallbacksAndMessages(null);
1230        super.onDestroy();
1231    }
1232
1233    private class MyDetailsSource implements DetailsSource {
1234
1235        @Override
1236        public MediaDetails getDetails() {
1237            return mModel.getMediaItem(0).getDetails();
1238        }
1239
1240        @Override
1241        public int size() {
1242            return mMediaSet != null ? mMediaSet.getMediaItemCount() : 1;
1243        }
1244
1245        @Override
1246        public int setIndex() {
1247            return mModel.getCurrentIndex();
1248        }
1249    }
1250}
1251