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