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