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