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