PhotoPage.java revision 53fe9f72433f2fdae30e1708c5933390202cbcf5
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        if (item != null) {
557            item.setVisible((mSecureAlbum == null) && canDoSlideShow());
558        }
559        if (mCurrentPhoto == null) return;
560
561        int supportedOperations = mCurrentPhoto.getSupportedOperations();
562        if (mSecureAlbum != null) {
563            supportedOperations &= MediaObject.SUPPORT_DELETE;
564        } else if (!mHaveImageEditor) {
565            supportedOperations &= ~MediaObject.SUPPORT_EDIT;
566        }
567        MenuExecutor.updateMenuOperation(menu, supportedOperations);
568    }
569
570    private boolean canDoSlideShow() {
571        if (mMediaSet == null || mCurrentPhoto == null) {
572            return false;
573        }
574        if (mCurrentPhoto.getMediaType() != MediaObject.MEDIA_TYPE_IMAGE) {
575            return false;
576        }
577        if (MtpSource.isMtpPath(mOriginalSetPathString)) {
578            return false;
579        }
580        return true;
581    }
582
583    //////////////////////////////////////////////////////////////////////////
584    //  Action Bar show/hide management
585    //////////////////////////////////////////////////////////////////////////
586
587    private void showBars() {
588        if (mShowBars) return;
589        mShowBars = true;
590        mOrientationManager.unlockOrientation();
591        mActionBar.show();
592        mActivity.getGLRoot().setLightsOutMode(false);
593        refreshHidingMessage();
594        if (mBottomControls != null) mBottomControls.refresh();
595    }
596
597    private void hideBars() {
598        if (!mShowBars) return;
599        mShowBars = false;
600        mActionBar.hide();
601        mActivity.getGLRoot().setLightsOutMode(true);
602        mHandler.removeMessages(MSG_HIDE_BARS);
603        if (mBottomControls != null) mBottomControls.refresh();
604    }
605
606    private void refreshHidingMessage() {
607        mHandler.removeMessages(MSG_HIDE_BARS);
608        if (!mIsMenuVisible && !mPhotoView.getFilmMode()) {
609            mHandler.sendEmptyMessageDelayed(MSG_HIDE_BARS, HIDE_BARS_TIMEOUT);
610        }
611    }
612
613    private boolean canShowBars() {
614        // No bars if we are showing camera preview.
615        if (mAppBridge != null && mCurrentIndex == 0
616                && !mPhotoView.getFilmMode()) return false;
617
618        // No bars if it's not allowed.
619        if (!mActionBarAllowed) return false;
620
621        return true;
622    }
623
624    private void wantBars() {
625        if (canShowBars()) showBars();
626    }
627
628    private void toggleBars() {
629        if (mShowBars) {
630            hideBars();
631        } else {
632            if (canShowBars()) showBars();
633        }
634    }
635
636    private void updateBars() {
637        if (!canShowBars()) {
638            hideBars();
639        }
640    }
641
642    @Override
643    public void onOrientationCompensationChanged() {
644        mActivity.getGLRoot().requestLayoutContentPane();
645    }
646
647    @Override
648    protected void onBackPressed() {
649        if (mShowDetails) {
650            hideDetails();
651        } else if (mAppBridge == null || !switchWithCaptureAnimation(-1)) {
652            // We are leaving this page. Set the result now.
653            setResult();
654            if (mStartInFilmstrip && !mPhotoView.getFilmMode()) {
655                mPhotoView.setFilmMode(true);
656            } else if (mTreatBackAsUp) {
657                onUpPressed();
658            } else {
659                super.onBackPressed();
660            }
661        }
662    }
663
664    private void onUpPressed() {
665        if ((mStartInFilmstrip || mAppBridge != null)
666                && !mPhotoView.getFilmMode()) {
667            mPhotoView.setFilmMode(true);
668            return;
669        }
670
671        if (mActivity.getStateManager().getStateCount() > 1) {
672            setResult();
673            super.onBackPressed();
674            return;
675        }
676
677        if (mOriginalSetPathString == null) return;
678
679        if (mAppBridge == null) {
680            // We're in view mode so set up the stacks on our own.
681            Bundle data = new Bundle(getData());
682            data.putString(AlbumPage.KEY_MEDIA_PATH, mOriginalSetPathString);
683            data.putString(AlbumPage.KEY_PARENT_MEDIA_PATH,
684                    mActivity.getDataManager().getTopSetPath(
685                            DataManager.INCLUDE_ALL));
686            mActivity.getStateManager().switchState(this, AlbumPage.class, data);
687        } else {
688            GalleryUtils.startGalleryActivity(mActivity);
689        }
690    }
691
692    private void setResult() {
693        Intent result = null;
694        result = new Intent();
695        result.putExtra(KEY_RETURN_INDEX_HINT, mCurrentIndex);
696        setStateResult(Activity.RESULT_OK, result);
697    }
698
699    //////////////////////////////////////////////////////////////////////////
700    //  AppBridge.Server interface
701    //////////////////////////////////////////////////////////////////////////
702
703    @Override
704    public void setCameraRelativeFrame(Rect frame) {
705        mPhotoView.setCameraRelativeFrame(frame);
706    }
707
708    @Override
709    public boolean switchWithCaptureAnimation(int offset) {
710        return mPhotoView.switchWithCaptureAnimation(offset);
711    }
712
713    @Override
714    public void setSwipingEnabled(boolean enabled) {
715        mPhotoView.setSwipingEnabled(enabled);
716    }
717
718    @Override
719    public void notifyScreenNailChanged() {
720        mScreenNailItem.setScreenNail(mAppBridge.attachScreenNail());
721        mScreenNailSet.notifyChange();
722    }
723
724    @Override
725    public void addSecureAlbumItem(boolean isVideo, int id) {
726        mSecureAlbum.addMediaItem(isVideo, id);
727    }
728
729    @Override
730    protected boolean onCreateActionBar(Menu menu) {
731        mActionBar.createActionBarMenu(R.menu.photo, menu);
732        mHaveImageEditor = GalleryUtils.isEditorAvailable(mActivity, "image/*");
733        updateMenuOperations();
734        updateTitle();
735        return true;
736    }
737
738    private MenuExecutor.ProgressListener mConfirmDialogListener =
739            new MenuExecutor.ProgressListener() {
740        @Override
741        public void onProgressUpdate(int index) {}
742
743        @Override
744        public void onProgressComplete(int result) {}
745
746        @Override
747        public void onConfirmDialogShown() {
748            mHandler.removeMessages(MSG_HIDE_BARS);
749        }
750
751        @Override
752        public void onConfirmDialogDismissed(boolean confirmed) {
753            refreshHidingMessage();
754        }
755
756        @Override
757        public void onProgressStart() {}
758    };
759
760    @Override
761    protected boolean onItemSelected(MenuItem item) {
762        if (mModel == null) return true;
763        refreshHidingMessage();
764        MediaItem current = mModel.getMediaItem(0);
765
766        if (current == null) {
767            // item is not ready, ignore
768            return true;
769        }
770
771        int currentIndex = mModel.getCurrentIndex();
772        Path path = current.getPath();
773
774        DataManager manager = mActivity.getDataManager();
775        int action = item.getItemId();
776        String confirmMsg = null;
777        switch (action) {
778            case android.R.id.home: {
779                onUpPressed();
780                return true;
781            }
782            case R.id.action_grid: {
783                if (mStartedFromAlbumPage) {
784                    onUpPressed();
785                } else {
786                    preparePhotoFallbackView();
787                    Bundle data = new Bundle(getData());
788                    data.putString(AlbumPage.KEY_MEDIA_PATH, mOriginalSetPathString);
789                    data.putString(AlbumPage.KEY_PARENT_MEDIA_PATH,
790                            mActivity.getDataManager().getTopSetPath(
791                                    DataManager.INCLUDE_ALL));
792
793                    // We only show cluster menu in the first AlbumPage in stack
794                    // TODO: Enable this when running from the camera app
795                    boolean inAlbum = mActivity.getStateManager().hasStateClass(AlbumPage.class);
796                    data.putBoolean(AlbumPage.KEY_SHOW_CLUSTER_MENU, !inAlbum
797                            && mAppBridge == null);
798
799                    data.putBoolean(PhotoPage.KEY_APP_BRIDGE, mAppBridge != null);
800
801                    // Account for live preview being first item
802                    mActivity.getTransitionStore().put(KEY_RETURN_INDEX_HINT,
803                            mAppBridge != null ? mCurrentIndex - 1 : mCurrentIndex);
804
805                    mActivity.getStateManager().startState(AlbumPage.class, data);
806                }
807                return true;
808            }
809            case R.id.action_slideshow: {
810                Bundle data = new Bundle();
811                data.putString(SlideshowPage.KEY_SET_PATH, mMediaSet.getPath().toString());
812                data.putString(SlideshowPage.KEY_ITEM_PATH, path.toString());
813                data.putInt(SlideshowPage.KEY_PHOTO_INDEX, currentIndex);
814                data.putBoolean(SlideshowPage.KEY_REPEAT, true);
815                mActivity.getStateManager().startStateForResult(
816                        SlideshowPage.class, REQUEST_SLIDESHOW, data);
817                return true;
818            }
819            case R.id.action_crop: {
820                Activity activity = mActivity;
821                Intent intent = new Intent(CropImage.CROP_ACTION);
822                intent.setClass(activity, CropImage.class);
823                intent.setData(manager.getContentUri(path));
824                activity.startActivityForResult(intent, PicasaSource.isPicasaImage(current)
825                        ? REQUEST_CROP_PICASA
826                        : REQUEST_CROP);
827                return true;
828            }
829            case R.id.action_trim: {
830                Intent intent = new Intent(mActivity, TrimVideo.class);
831                intent.setData(manager.getContentUri(path));
832                // We need the file path to wrap this into a RandomAccessFile.
833                intent.putExtra(KEY_MEDIA_ITEM_PATH, current.getFilePath());
834                mActivity.startActivityForResult(intent, REQUEST_TRIM);
835                return true;
836            }
837            case R.id.action_edit: {
838                launchPhotoEditor();
839                return true;
840            }
841            case R.id.action_details: {
842                if (mShowDetails) {
843                    hideDetails();
844                } else {
845                    showDetails();
846                }
847                return true;
848            }
849            case R.id.action_delete:
850                confirmMsg = mActivity.getResources().getQuantityString(
851                        R.plurals.delete_selection, 1);
852            case R.id.action_setas:
853            case R.id.action_rotate_ccw:
854            case R.id.action_rotate_cw:
855            case R.id.action_show_on_map:
856                mSelectionManager.deSelectAll();
857                mSelectionManager.toggle(path);
858                mMenuExecutor.onMenuClicked(item, confirmMsg, mConfirmDialogListener);
859                return true;
860            case R.id.action_import:
861                mSelectionManager.deSelectAll();
862                mSelectionManager.toggle(path);
863                mMenuExecutor.onMenuClicked(item, confirmMsg,
864                        new ImportCompleteListener(mActivity));
865                return true;
866            case R.id.action_share:
867                Activity activity = mActivity;
868                Intent intent = createShareIntent(mCurrentPhoto.getPath());
869                activity.startActivity(Intent.createChooser(intent,
870                        activity.getString(R.string.share)));
871                return true;
872            default :
873                return false;
874        }
875    }
876
877    private void hideDetails() {
878        mShowDetails = false;
879        mDetailsHelper.hide();
880    }
881
882    private void showDetails() {
883        mShowDetails = true;
884        if (mDetailsHelper == null) {
885            mDetailsHelper = new DetailsHelper(mActivity, mRootPane, new MyDetailsSource());
886            mDetailsHelper.setCloseListener(new CloseListener() {
887                @Override
888                public void onClose() {
889                    hideDetails();
890                }
891            });
892        }
893        mDetailsHelper.show();
894    }
895
896    ////////////////////////////////////////////////////////////////////////////
897    //  Callbacks from PhotoView
898    ////////////////////////////////////////////////////////////////////////////
899    @Override
900    public void onSingleTapUp(int x, int y) {
901        if (mAppBridge != null) {
902            if (mAppBridge.onSingleTapUp(x, y)) return;
903        }
904
905        MediaItem item = mModel.getMediaItem(0);
906        if (item == null || item == mScreenNailItem) {
907            // item is not ready or it is camera preview, ignore
908            return;
909        }
910
911        int supported = item.getSupportedOperations();
912        boolean playVideo = (mSecureAlbum == null) &&
913                ((supported & MediaItem.SUPPORT_PLAY) != 0);
914        boolean viewPanorama = (mSecureAlbum == null) &&
915                ((supported & MediaItem.SUPPORT_PANORAMA) != 0);
916        boolean unlock = ((supported & MediaItem.SUPPORT_UNLOCK) != 0);
917
918        if (playVideo) {
919            // determine if the point is at center (1/6) of the photo view.
920            // (The position of the "play" icon is at center (1/6) of the photo)
921            int w = mPhotoView.getWidth();
922            int h = mPhotoView.getHeight();
923            playVideo = (Math.abs(x - w / 2) * 12 <= w)
924                && (Math.abs(y - h / 2) * 12 <= h);
925        }
926
927        if (playVideo) {
928            playVideo(mActivity, item.getPlayUri(), item.getName());
929        } else if (viewPanorama) {
930            LightCycleHelper.viewPanorama(mActivity, item.getContentUri());
931        } else if (unlock) {
932            mActivity.getStateManager().finishState(this);
933        } else {
934            toggleBars();
935        }
936    }
937
938    @Override
939    public void lockOrientation() {
940        mHandler.sendEmptyMessage(MSG_LOCK_ORIENTATION);
941    }
942
943    @Override
944    public void unlockOrientation() {
945        mHandler.sendEmptyMessage(MSG_UNLOCK_ORIENTATION);
946    }
947
948    @Override
949    public void onActionBarAllowed(boolean allowed) {
950        mActionBarAllowed = allowed;
951        mHandler.sendEmptyMessage(MSG_UPDATE_ACTION_BAR);
952    }
953
954    @Override
955    public void onActionBarWanted() {
956        mHandler.sendEmptyMessage(MSG_WANT_BARS);
957    }
958
959    @Override
960    public void onFullScreenChanged(boolean full) {
961        Message m = mHandler.obtainMessage(
962                MSG_ON_FULL_SCREEN_CHANGED, full ? 1 : 0, 0);
963        m.sendToTarget();
964    }
965
966    // How we do delete/undo:
967    //
968    // When the user choose to delete a media item, we just tell the
969    // FilterDeleteSet to hide that item. If the user choose to undo it, we
970    // again tell FilterDeleteSet not to hide it. If the user choose to commit
971    // the deletion, we then actually delete the media item.
972    @Override
973    public void onDeleteImage(Path path, int offset) {
974        onCommitDeleteImage();  // commit the previous deletion
975        mDeletePath = path;
976        mDeleteIsFocus = (offset == 0);
977        mMediaSet.addDeletion(path, mCurrentIndex + offset);
978    }
979
980    @Override
981    public void onUndoDeleteImage() {
982        if (mDeletePath == null) return;
983        // If the deletion was done on the focused item, we want the model to
984        // focus on it when it is undeleted.
985        if (mDeleteIsFocus) mModel.setFocusHintPath(mDeletePath);
986        mMediaSet.removeDeletion(mDeletePath);
987        mDeletePath = null;
988    }
989
990    @Override
991    public void onCommitDeleteImage() {
992        if (mDeletePath == null) return;
993        mSelectionManager.deSelectAll();
994        mSelectionManager.toggle(mDeletePath);
995        mMenuExecutor.onMenuClicked(R.id.action_delete, null, true, false);
996        mDeletePath = null;
997    }
998
999    public static void playVideo(Activity activity, Uri uri, String title) {
1000        try {
1001            Intent intent = new Intent(Intent.ACTION_VIEW)
1002                    .setDataAndType(uri, "video/*")
1003                    .putExtra(Intent.EXTRA_TITLE, title)
1004                    .putExtra(MovieActivity.KEY_TREAT_UP_AS_BACK, true);
1005            activity.startActivityForResult(intent, REQUEST_PLAY_VIDEO);
1006        } catch (ActivityNotFoundException e) {
1007            Toast.makeText(activity, activity.getString(R.string.video_err),
1008                    Toast.LENGTH_SHORT).show();
1009        }
1010    }
1011
1012    private void setCurrentPhotoByIntent(Intent intent) {
1013        if (intent == null) return;
1014        Path path = mApplication.getDataManager()
1015                .findPathByUri(intent.getData(), intent.getType());
1016        if (path != null) {
1017            mModel.setCurrentPhoto(path, mCurrentIndex);
1018        }
1019    }
1020
1021    @Override
1022    protected void onStateResult(int requestCode, int resultCode, Intent data) {
1023        mHasActivityResult = true;
1024        switch (requestCode) {
1025            case REQUEST_EDIT:
1026                setCurrentPhotoByIntent(data);
1027                break;
1028            case REQUEST_CROP:
1029                if (resultCode == Activity.RESULT_OK) {
1030                    setCurrentPhotoByIntent(data);
1031                }
1032                break;
1033            case REQUEST_CROP_PICASA: {
1034                if (resultCode == Activity.RESULT_OK) {
1035                    Context context = mActivity.getAndroidContext();
1036                    String message = context.getString(R.string.crop_saved,
1037                            context.getString(R.string.folder_download));
1038                    Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
1039                }
1040                break;
1041            }
1042            case REQUEST_SLIDESHOW: {
1043                if (data == null) break;
1044                String path = data.getStringExtra(SlideshowPage.KEY_ITEM_PATH);
1045                int index = data.getIntExtra(SlideshowPage.KEY_PHOTO_INDEX, 0);
1046                if (path != null) {
1047                    mModel.setCurrentPhoto(Path.fromString(path), index);
1048                }
1049            }
1050        }
1051    }
1052
1053    @Override
1054    protected void clearStateResult() {
1055        mHasActivityResult = false;
1056    }
1057
1058    private class PreparePhotoFallback implements OnGLIdleListener {
1059        private PhotoFallbackEffect mPhotoFallback = new PhotoFallbackEffect();
1060        private boolean mResultReady = false;
1061
1062        public synchronized PhotoFallbackEffect get() {
1063            while (!mResultReady) {
1064                Utils.waitWithoutInterrupt(this);
1065            }
1066            return mPhotoFallback;
1067        }
1068
1069        @Override
1070        public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) {
1071            mPhotoFallback = mPhotoView.buildFallbackEffect(mRootPane, canvas);
1072            synchronized (this) {
1073                mResultReady = true;
1074                notifyAll();
1075            }
1076            return false;
1077        }
1078    }
1079
1080    private void preparePhotoFallbackView() {
1081        GLRoot root = mActivity.getGLRoot();
1082        PreparePhotoFallback task = new PreparePhotoFallback();
1083        root.unlockRenderThread();
1084        PhotoFallbackEffect anim;
1085        try {
1086            root.addOnGLIdleListener(task);
1087            anim = task.get();
1088        } finally {
1089            root.lockRenderThread();
1090        }
1091        mActivity.getTransitionStore().put(
1092                AlbumPage.KEY_RESUME_ANIMATION, anim);
1093    }
1094
1095    @Override
1096    public void onPause() {
1097        super.onPause();
1098        mIsActive = false;
1099
1100        mActivity.getGLRoot().unfreeze();
1101        mHandler.removeMessages(MSG_UNFREEZE_GLROOT);
1102
1103        DetailsHelper.pause();
1104        if (mModel != null) {
1105            if (isFinishing()) preparePhotoFallbackView();
1106            mModel.pause();
1107        }
1108        mPhotoView.pause();
1109        mHandler.removeMessages(MSG_HIDE_BARS);
1110        mActionBar.removeOnMenuVisibilityListener(mMenuVisibilityListener);
1111
1112        onCommitDeleteImage();
1113        mMenuExecutor.pause();
1114        if (mMediaSet != null) mMediaSet.clearDeletion();
1115    }
1116
1117    @Override
1118    public void onCurrentImageUpdated() {
1119        mActivity.getGLRoot().unfreeze();
1120    }
1121
1122    private void setGridButtonVisibility(boolean enabled) {
1123        Menu menu = mActionBar.getMenu();
1124        if (menu == null) return;
1125        MenuItem item = menu.findItem(R.id.action_grid);
1126        if (item != null) item.setVisible((mSecureAlbum == null) && enabled);
1127    }
1128
1129    public void onFilmModeChanged(boolean enabled) {
1130        mHandler.sendEmptyMessage(MSG_REFRESH_GRID_BUTTON);
1131        mHandler.sendEmptyMessage(MSG_REFRESH_BOTTOM_CONTROLS);
1132        if (enabled) {
1133            mHandler.removeMessages(MSG_HIDE_BARS);
1134        } else {
1135            refreshHidingMessage();
1136        }
1137    }
1138
1139    private void transitionFromAlbumPageIfNeeded() {
1140        TransitionStore transitions = mActivity.getTransitionStore();
1141
1142        int albumPageTransition = transitions.get(
1143                KEY_ALBUMPAGE_TRANSITION, MSG_ALBUMPAGE_NONE);
1144
1145        if (albumPageTransition == MSG_ALBUMPAGE_NONE && mAppBridge != null) {
1146            // Generally, resuming the PhotoPage when in Camera should
1147            // reset to the capture mode to allow quick photo taking
1148            mCurrentIndex = 0;
1149            mPhotoView.resetToFirstPicture();
1150        } else {
1151            int resumeIndex = transitions.get(KEY_INDEX_HINT, -1);
1152            if (resumeIndex >= 0) {
1153                if (mAppBridge != null) {
1154                    // Account for live preview being the first item
1155                    resumeIndex++;
1156                }
1157                if (resumeIndex < mMediaSet.getMediaItemCount()) {
1158                    mCurrentIndex = resumeIndex;
1159                    mModel.moveTo(mCurrentIndex);
1160                }
1161            }
1162        }
1163
1164        if (albumPageTransition == MSG_ALBUMPAGE_RESUMED) {
1165            mPhotoView.setFilmMode(mStartInFilmstrip || mAppBridge != null);
1166        } else if (albumPageTransition == MSG_ALBUMPAGE_PICKED) {
1167            mPhotoView.setFilmMode(false);
1168        }
1169
1170        mFadeOutTexture = transitions.get(PreparePageFadeoutTexture.KEY_FADE_TEXTURE);
1171        if (mFadeOutTexture != null) {
1172            mBackgroundFade.start();
1173            BitmapScreenNail.disableDrawPlaceholder();
1174            mOpenAnimationRect =
1175                    albumPageTransition == MSG_ALBUMPAGE_NONE ?
1176                    (Rect) mData.getParcelable(KEY_OPEN_ANIMATION_RECT) :
1177                    (Rect) transitions.get(KEY_OPEN_ANIMATION_RECT);
1178            mPhotoView.setOpenAnimationRect(mOpenAnimationRect);
1179            mBackgroundFade.start();
1180        }
1181    }
1182
1183    @Override
1184    protected void onResume() {
1185        super.onResume();
1186
1187        if (mModel == null) {
1188            mActivity.getStateManager().finishState(this);
1189            return;
1190        }
1191        transitionFromAlbumPageIfNeeded();
1192
1193        mActivity.getGLRoot().freeze();
1194        mIsActive = true;
1195        setContentPane(mRootPane);
1196
1197        mModel.resume();
1198        mPhotoView.resume();
1199        mActionBar.setDisplayOptions(
1200                ((mSecureAlbum == null) && (mSetPathString != null)), true);
1201        mActionBar.addOnMenuVisibilityListener(mMenuVisibilityListener);
1202        if (!mShowBars) {
1203            mActionBar.hide();
1204            mActivity.getGLRoot().setLightsOutMode(true);
1205        }
1206        boolean haveImageEditor = GalleryUtils.isEditorAvailable(mActivity, "image/*");
1207        if (haveImageEditor != mHaveImageEditor) {
1208            mHaveImageEditor = haveImageEditor;
1209            updateMenuOperations();
1210        }
1211
1212        mHasActivityResult = false;
1213        mHandler.sendEmptyMessageDelayed(MSG_UNFREEZE_GLROOT, UNFREEZE_GLROOT_TIMEOUT);
1214    }
1215
1216    @Override
1217    protected void onDestroy() {
1218        if (mAppBridge != null) {
1219            mAppBridge.setServer(null);
1220            mScreenNailItem.setScreenNail(null);
1221            mAppBridge.detachScreenNail();
1222            mAppBridge = null;
1223            mScreenNailSet = null;
1224            mScreenNailItem = null;
1225        }
1226        mOrientationManager.removeListener(this);
1227        mActivity.getGLRoot().setOrientationSource(null);
1228        if (mBottomControls != null) mBottomControls.cleanup();
1229
1230        // Remove all pending messages.
1231        mHandler.removeCallbacksAndMessages(null);
1232        super.onDestroy();
1233    }
1234
1235    private class MyDetailsSource implements DetailsSource {
1236
1237        @Override
1238        public MediaDetails getDetails() {
1239            return mModel.getMediaItem(0).getDetails();
1240        }
1241
1242        @Override
1243        public int size() {
1244            return mMediaSet != null ? mMediaSet.getMediaItemCount() : 1;
1245        }
1246
1247        @Override
1248        public int setIndex() {
1249            return mModel.getCurrentIndex();
1250        }
1251    }
1252}
1253