PhotoPage.java revision 56bdcb7272f222fde5e4c6fc133cb490b31c7667
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.MediaDetails;
49import com.android.gallery3d.data.MediaItem;
50import com.android.gallery3d.data.MediaObject;
51import com.android.gallery3d.data.MediaSet;
52import com.android.gallery3d.data.MtpSource;
53import com.android.gallery3d.data.Path;
54import com.android.gallery3d.data.SecureAlbum;
55import com.android.gallery3d.data.SecureSource;
56import com.android.gallery3d.data.SnailAlbum;
57import com.android.gallery3d.data.SnailItem;
58import com.android.gallery3d.data.SnailSource;
59import com.android.gallery3d.picasasource.PicasaSource;
60import com.android.gallery3d.ui.AnimationTime;
61import com.android.gallery3d.ui.BitmapScreenNail;
62import com.android.gallery3d.ui.DetailsHelper;
63import com.android.gallery3d.ui.DetailsHelper.CloseListener;
64import com.android.gallery3d.ui.DetailsHelper.DetailsSource;
65import com.android.gallery3d.ui.GLCanvas;
66import com.android.gallery3d.ui.GLRoot;
67import com.android.gallery3d.ui.GLRoot.OnGLIdleListener;
68import com.android.gallery3d.ui.GLView;
69import com.android.gallery3d.ui.ImportCompleteListener;
70import com.android.gallery3d.ui.MenuExecutor;
71import com.android.gallery3d.ui.PhotoFallbackEffect;
72import com.android.gallery3d.ui.PhotoView;
73import com.android.gallery3d.ui.PreparePageFadeoutTexture;
74import com.android.gallery3d.ui.RawTexture;
75import com.android.gallery3d.ui.SelectionManager;
76import com.android.gallery3d.ui.SynchronizedHandler;
77import com.android.gallery3d.util.GalleryUtils;
78import com.android.gallery3d.util.LightCycleHelper;
79import com.android.gallery3d.util.MediaSetUtils;
80
81public class PhotoPage extends ActivityState implements
82        PhotoView.Listener, OrientationManager.Listener, AppBridge.Server {
83    private static final String TAG = "PhotoPage";
84
85    private static final int MSG_HIDE_BARS = 1;
86    private static final int MSG_LOCK_ORIENTATION = 2;
87    private static final int MSG_UNLOCK_ORIENTATION = 3;
88    private static final int MSG_ON_FULL_SCREEN_CHANGED = 4;
89    private static final int MSG_UPDATE_ACTION_BAR = 5;
90    private static final int MSG_UNFREEZE_GLROOT = 6;
91    private static final int MSG_WANT_BARS = 7;
92    private static final int MSG_REFRESH_GRID_BUTTON = 8;
93    private static final int MSG_REFRESH_EDIT_BUTTON = 9;
94
95    private static final int HIDE_BARS_TIMEOUT = 3500;
96    private static final int UNFREEZE_GLROOT_TIMEOUT = 250;
97
98    private static final int REQUEST_SLIDESHOW = 1;
99    private static final int REQUEST_CROP = 2;
100    private static final int REQUEST_CROP_PICASA = 3;
101    private static final int REQUEST_EDIT = 4;
102    private static final int REQUEST_PLAY_VIDEO = 5;
103    private static final int REQUEST_TRIM = 6;
104
105    public static final String KEY_MEDIA_SET_PATH = "media-set-path";
106    public static final String KEY_MEDIA_ITEM_PATH = "media-item-path";
107    public static final String KEY_INDEX_HINT = "index-hint";
108    public static final String KEY_OPEN_ANIMATION_RECT = "open-animation-rect";
109    public static final String KEY_APP_BRIDGE = "app-bridge";
110    public static final String KEY_TREAT_BACK_AS_UP = "treat-back-as-up";
111    public static final String KEY_START_IN_FILMSTRIP = "start-in-filmstrip";
112    public static final String KEY_RETURN_INDEX_HINT = "return-index-hint";
113
114    public static final String KEY_ALBUMPAGE_TRANSITION = "albumpage-transition";
115    public static final int MSG_ALBUMPAGE_NONE = 0;
116    public static final int MSG_ALBUMPAGE_STARTED = 1;
117    public static final int MSG_ALBUMPAGE_RESUMED = 2;
118    public static final int MSG_ALBUMPAGE_PICKED = 4;
119
120    public static final String ACTION_NEXTGEN_EDIT = "action_nextgen_edit";
121
122    private GalleryApp mApplication;
123    private SelectionManager mSelectionManager;
124
125    private PhotoView mPhotoView;
126    private PhotoPage.Model mModel;
127    private DetailsHelper mDetailsHelper;
128    private boolean mShowDetails;
129
130    // mMediaSet could be null if there is no KEY_MEDIA_SET_PATH supplied.
131    // E.g., viewing a photo in gmail attachment
132    private FilterDeleteSet mMediaSet;
133
134    // The mediaset used by camera launched from secure lock screen.
135    private SecureAlbum mSecureAlbum;
136
137    private int mCurrentIndex = 0;
138    private Handler mHandler;
139    private boolean mShowBars = true;
140    private volatile boolean mActionBarAllowed = true;
141    private GalleryActionBar mActionBar;
142    private boolean mIsMenuVisible;
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_EDIT_BUTTON: {
261                        refreshEditButton();
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                    // Set the flag to be on top of the lock screen.
331                    mFlags |= FLAG_SHOW_WHEN_LOCKED;
332                }
333
334                // Combine the original MediaSet with the one for ScreenNail
335                // from AppBridge.
336                mSetPathString = "/combo/item/{" + screenNailSetPath +
337                        "," + mSetPathString + "}";
338
339                // Start from the screen nail.
340                itemPath = screenNailItemPath;
341            }
342
343            MediaSet originalSet = mActivity.getDataManager()
344                    .getMediaSet(mSetPathString);
345            mSelectionManager.setSourceMediaSet(originalSet);
346            mSetPathString = "/filter/delete/{" + mSetPathString + "}";
347            mMediaSet = (FilterDeleteSet) mActivity.getDataManager()
348                    .getMediaSet(mSetPathString);
349            mCurrentIndex = data.getInt(KEY_INDEX_HINT, 0);
350            if (mMediaSet == null) {
351                Log.w(TAG, "failed to restore " + mSetPathString);
352            }
353            if (itemPath == null) {
354                if (mMediaSet.getMediaItemCount() > 0) {
355                    itemPath = mMediaSet.getMediaItem(mCurrentIndex, 1)
356                        .get(0).getPath();
357                } else {
358                    return;
359                }
360            }
361            PhotoDataAdapter pda = new PhotoDataAdapter(
362                    mActivity, mPhotoView, mMediaSet, itemPath, mCurrentIndex,
363                    mAppBridge == null ? -1 : 0,
364                    mAppBridge == null ? false : mAppBridge.isPanorama(),
365                    mAppBridge == null ? false : mAppBridge.isStaticCamera());
366            mModel = pda;
367            mPhotoView.setModel(mModel);
368
369            pda.setDataListener(new PhotoDataAdapter.DataListener() {
370
371                @Override
372                public void onPhotoChanged(int index, Path item) {
373                    mCurrentIndex = index;
374                    if (item != null) {
375                        MediaItem photo = mModel.getMediaItem(0);
376                        if (photo != null) updateCurrentPhoto(photo);
377                    }
378                    updateBars();
379                }
380
381                @Override
382                public void onLoadingFinished() {
383                    if (!mModel.isEmpty()) {
384                        MediaItem photo = mModel.getMediaItem(0);
385                        if (photo != null) updateCurrentPhoto(photo);
386                    } else if (mIsActive) {
387                        // We only want to finish the PhotoPage if there is no
388                        // deletion that the user can undo.
389                        if (mMediaSet.getNumberOfDeletions() == 0) {
390                            mActivity.getStateManager().finishState(
391                                    PhotoPage.this);
392                        }
393                    }
394                }
395
396                @Override
397                public void onLoadingStarted() {
398                }
399            });
400        } else {
401            // Get default media set by the URI
402            MediaItem mediaItem = (MediaItem)
403                    mActivity.getDataManager().getMediaObject(itemPath);
404            mModel = new SinglePhotoDataAdapter(mActivity, mPhotoView, mediaItem);
405            mPhotoView.setModel(mModel);
406            updateCurrentPhoto(mediaItem);
407        }
408
409        mPhotoView.setFilmMode(mStartInFilmstrip && mMediaSet.getMediaItemCount() > 1);
410        setupEditButton();
411    }
412
413    private ImageView mEditButton;
414    private void setupEditButton() {
415        if (mSecureAlbum != null) return;
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        if (mEditButton == null) return;
438        RelativeLayout galleryRoot = (RelativeLayout) ((Activity) mActivity)
439                .findViewById(mAppBridge != null ? R.id.content : R.id.gallery_root);
440        if (galleryRoot == null) return;
441        galleryRoot.removeView(mEditButton);
442        mEditButton = null;
443    }
444
445    private void refreshEditButton() {
446        if (mEditButton == null) return;
447        if (mShowBars && mCurrentPhoto != null && !mPhotoView.getFilmMode()
448                && mCurrentPhoto.getMediaType() == MediaObject.MEDIA_TYPE_IMAGE) {
449            mEditButton.setVisibility(View.VISIBLE);
450        } else {
451            mEditButton.setVisibility(View.GONE);
452        }
453    }
454
455    @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
456    private void setNfcBeamPushUris(Uri[] uris) {
457        if (mNfcAdapter != null && ApiHelper.HAS_SET_BEAM_PUSH_URIS) {
458            mNfcAdapter.setBeamPushUris(uris, mActivity);
459        }
460    }
461
462    private Intent createShareIntent(Path path) {
463        DataManager manager = mActivity.getDataManager();
464        int type = manager.getMediaType(path);
465        Intent intent = new Intent(Intent.ACTION_SEND);
466        intent.setType(MenuExecutor.getMimeType(type));
467        Uri uri = manager.getContentUri(path);
468        intent.putExtra(Intent.EXTRA_STREAM, uri);
469        return intent;
470
471    }
472
473    private void launchPhotoEditor() {
474        MediaItem current = mModel.getMediaItem(0);
475        if (current == null) return;
476
477        Intent intent = new Intent(ACTION_NEXTGEN_EDIT);
478        intent.setData(mActivity.getDataManager().getContentUri(current.getPath())).setFlags(
479                Intent.FLAG_GRANT_READ_URI_PERMISSION);
480        if (mActivity.getPackageManager()
481                .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY).size() == 0) {
482            intent.setAction(Intent.ACTION_EDIT);
483        }
484        ((Activity) mActivity).startActivityForResult(Intent.createChooser(intent, null),
485                REQUEST_EDIT);
486    }
487
488    private void updateShareURI(Path path) {
489        DataManager manager = mActivity.getDataManager();
490        Uri uri = manager.getContentUri(path);
491        mActionBar.setShareIntent(createShareIntent(path));
492        setNfcBeamPushUris(new Uri[]{uri});
493    }
494
495    private void updateCurrentPhoto(MediaItem photo) {
496        if (mCurrentPhoto == photo) return;
497        mCurrentPhoto = photo;
498        if (mCurrentPhoto == null) return;
499        updateMenuOperations();
500        updateTitle();
501        refreshEditButton();
502        if (mShowDetails) {
503            mDetailsHelper.reloadDetails();
504        }
505        if ((mSecureAlbum == null)
506                && (photo.getSupportedOperations() & MediaItem.SUPPORT_SHARE) != 0) {
507            updateShareURI(photo.getPath());
508        }
509    }
510
511    private void updateTitle() {
512        if (mCurrentPhoto == null) return;
513        boolean showTitle = mActivity.getAndroidContext().getResources().getBoolean(
514                R.bool.show_action_bar_title);
515        if (showTitle && mCurrentPhoto.getName() != null) {
516            mActionBar.setTitle(mCurrentPhoto.getName());
517        } else {
518            mActionBar.setTitle("");
519        }
520    }
521
522    private void updateMenuOperations() {
523        Menu menu = mActionBar.getMenu();
524
525        // it could be null if onCreateActionBar has not been called yet
526        if (menu == null) return;
527
528        setGridButtonVisibility(mPhotoView.getFilmMode());
529
530        MenuItem item = menu.findItem(R.id.action_slideshow);
531        item.setVisible((mSecureAlbum == null) && canDoSlideShow());
532        if (mCurrentPhoto == null) return;
533
534        int supportedOperations = mCurrentPhoto.getSupportedOperations();
535        if (mSecureAlbum != null) {
536            supportedOperations &= MediaObject.SUPPORT_DELETE;
537        } else if (!GalleryUtils.isEditorAvailable(mActivity, "image/*")) {
538            supportedOperations &= ~MediaObject.SUPPORT_EDIT;
539        }
540        MenuExecutor.updateMenuOperation(menu, supportedOperations);
541    }
542
543    private boolean canDoSlideShow() {
544        if (mMediaSet == null || mCurrentPhoto == null) {
545            return false;
546        }
547        if (mCurrentPhoto.getMediaType() != MediaObject.MEDIA_TYPE_IMAGE) {
548            return false;
549        }
550        if (MtpSource.isMtpPath(mOriginalSetPathString)) {
551            return false;
552        }
553        return true;
554    }
555
556    //////////////////////////////////////////////////////////////////////////
557    //  Action Bar show/hide management
558    //////////////////////////////////////////////////////////////////////////
559
560    private void showBars() {
561        if (mShowBars) return;
562        mShowBars = true;
563        mOrientationManager.unlockOrientation();
564        mActionBar.show();
565        mActivity.getGLRoot().setLightsOutMode(false);
566        refreshHidingMessage();
567        refreshEditButton();
568    }
569
570    private void hideBars() {
571        if (!mShowBars) return;
572        mShowBars = false;
573        mActionBar.hide();
574        mActivity.getGLRoot().setLightsOutMode(true);
575        mHandler.removeMessages(MSG_HIDE_BARS);
576        refreshEditButton();
577    }
578
579    private void refreshHidingMessage() {
580        mHandler.removeMessages(MSG_HIDE_BARS);
581        if (!mIsMenuVisible && !mPhotoView.getFilmMode()) {
582            mHandler.sendEmptyMessageDelayed(MSG_HIDE_BARS, HIDE_BARS_TIMEOUT);
583        }
584    }
585
586    private boolean canShowBars() {
587        // No bars if we are showing camera preview.
588        if (mAppBridge != null && mCurrentIndex == 0
589                && !mPhotoView.getFilmMode()) return false;
590
591        // No bars if it's not allowed.
592        if (!mActionBarAllowed) return false;
593
594        return true;
595    }
596
597    private void wantBars() {
598        if (canShowBars()) showBars();
599    }
600
601    private void toggleBars() {
602        if (mShowBars) {
603            hideBars();
604        } else {
605            if (canShowBars()) showBars();
606        }
607    }
608
609    private void updateBars() {
610        if (!canShowBars()) {
611            hideBars();
612        }
613    }
614
615    @Override
616    public void onOrientationCompensationChanged() {
617        mActivity.getGLRoot().requestLayoutContentPane();
618    }
619
620    @Override
621    protected void onBackPressed() {
622        if (mShowDetails) {
623            hideDetails();
624        } else if (mAppBridge == null || !switchWithCaptureAnimation(-1)) {
625            // We are leaving this page. Set the result now.
626            setResult();
627            if (mStartInFilmstrip && !mPhotoView.getFilmMode()) {
628                mPhotoView.setFilmMode(true);
629            } else if (mTreatBackAsUp) {
630                onUpPressed();
631            } else {
632                super.onBackPressed();
633            }
634        }
635    }
636
637    private void onUpPressed() {
638        if (mStartInFilmstrip && !mPhotoView.getFilmMode()) {
639            mPhotoView.setFilmMode(true);
640            return;
641        }
642
643        if (mActivity.getStateManager().getStateCount() > 1) {
644            setResult();
645            super.onBackPressed();
646            return;
647        }
648
649        if (mOriginalSetPathString == null) return;
650
651        if (mAppBridge == null) {
652            // We're in view mode so set up the stacks on our own.
653            Bundle data = new Bundle(getData());
654            data.putString(AlbumPage.KEY_MEDIA_PATH, mOriginalSetPathString);
655            data.putString(AlbumPage.KEY_PARENT_MEDIA_PATH,
656                    mActivity.getDataManager().getTopSetPath(
657                            DataManager.INCLUDE_ALL));
658            mActivity.getStateManager().switchState(this, AlbumPage.class, data);
659        } else {
660            // Start the real gallery activity to view the camera roll.
661            Uri uri = Uri.parse("content://media/external/file?bucketId="
662                    + MediaSetUtils.CAMERA_BUCKET_ID);
663            Intent intent = new Intent(Intent.ACTION_VIEW);
664            intent.setDataAndType(uri, ContentResolver.CURSOR_DIR_BASE_TYPE + "/image");
665            ((Activity) mActivity).startActivity(intent);
666        }
667    }
668
669    private void setResult() {
670        Intent result = null;
671        result = new Intent();
672        result.putExtra(KEY_RETURN_INDEX_HINT, mCurrentIndex);
673        setStateResult(Activity.RESULT_OK, result);
674    }
675
676    //////////////////////////////////////////////////////////////////////////
677    //  AppBridge.Server interface
678    //////////////////////////////////////////////////////////////////////////
679
680    @Override
681    public void setCameraRelativeFrame(Rect frame) {
682        mPhotoView.setCameraRelativeFrame(frame);
683    }
684
685    @Override
686    public boolean switchWithCaptureAnimation(int offset) {
687        return mPhotoView.switchWithCaptureAnimation(offset);
688    }
689
690    @Override
691    public void setSwipingEnabled(boolean enabled) {
692        mPhotoView.setSwipingEnabled(enabled);
693    }
694
695    @Override
696    public void notifyScreenNailChanged() {
697        mScreenNailItem.setScreenNail(mAppBridge.attachScreenNail());
698        mScreenNailSet.notifyChange();
699    }
700
701    @Override
702    public void addSecureAlbumItem(boolean isVideo, int id) {
703        mSecureAlbum.addMediaItem(isVideo, id);
704    }
705
706    @Override
707    protected boolean onCreateActionBar(Menu menu) {
708        mActionBar.createActionBarMenu(R.menu.photo, menu);
709        updateMenuOperations();
710        updateTitle();
711        return true;
712    }
713
714    private MenuExecutor.ProgressListener mConfirmDialogListener =
715            new MenuExecutor.ProgressListener() {
716        @Override
717        public void onProgressUpdate(int index) {}
718
719        @Override
720        public void onProgressComplete(int result) {}
721
722        @Override
723        public void onConfirmDialogShown() {
724            mHandler.removeMessages(MSG_HIDE_BARS);
725        }
726
727        @Override
728        public void onConfirmDialogDismissed(boolean confirmed) {
729            refreshHidingMessage();
730        }
731
732        @Override
733        public void onProgressStart() {}
734    };
735
736    @Override
737    protected boolean onItemSelected(MenuItem item) {
738        if (mModel == null) return true;
739        refreshHidingMessage();
740        MediaItem current = mModel.getMediaItem(0);
741
742        if (current == null) {
743            // item is not ready, ignore
744            return true;
745        }
746
747        int currentIndex = mModel.getCurrentIndex();
748        Path path = current.getPath();
749
750        DataManager manager = mActivity.getDataManager();
751        int action = item.getItemId();
752        String confirmMsg = null;
753        switch (action) {
754            case android.R.id.home: {
755                onUpPressed();
756                return true;
757            }
758            case R.id.action_grid: {
759                if (mStartedFromAlbumPage) {
760                    onUpPressed();
761                } else {
762                    preparePhotoFallbackView();
763                    Bundle data = new Bundle(getData());
764                    data.putString(AlbumPage.KEY_MEDIA_PATH, mOriginalSetPathString);
765                    data.putString(AlbumPage.KEY_PARENT_MEDIA_PATH,
766                            mActivity.getDataManager().getTopSetPath(
767                                    DataManager.INCLUDE_ALL));
768                    mActivity.getTransitionStore().put(
769                            KEY_RETURN_INDEX_HINT, mCurrentIndex);
770                    mActivity.getStateManager().startState(AlbumPage.class, data);
771                }
772                return true;
773            }
774            case R.id.action_slideshow: {
775                Bundle data = new Bundle();
776                data.putString(SlideshowPage.KEY_SET_PATH, mMediaSet.getPath().toString());
777                data.putString(SlideshowPage.KEY_ITEM_PATH, path.toString());
778                data.putInt(SlideshowPage.KEY_PHOTO_INDEX, currentIndex);
779                data.putBoolean(SlideshowPage.KEY_REPEAT, true);
780                mActivity.getStateManager().startStateForResult(
781                        SlideshowPage.class, REQUEST_SLIDESHOW, data);
782                return true;
783            }
784            case R.id.action_crop: {
785                Activity activity = mActivity;
786                Intent intent = new Intent(CropImage.CROP_ACTION);
787                intent.setClass(activity, CropImage.class);
788                intent.setData(manager.getContentUri(path));
789                activity.startActivityForResult(intent, PicasaSource.isPicasaImage(current)
790                        ? REQUEST_CROP_PICASA
791                        : REQUEST_CROP);
792                return true;
793            }
794            case R.id.action_trim: {
795                Intent intent = new Intent(mActivity, TrimVideo.class);
796                intent.setData(manager.getContentUri(path));
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_VIEW_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(enabled);
1086    }
1087
1088    public void onFilmModeChanged(boolean enabled) {
1089        mHandler.sendEmptyMessage(MSG_REFRESH_GRID_BUTTON);
1090        mHandler.sendEmptyMessage(MSG_REFRESH_EDIT_BUTTON);
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        cleanupEditButton();
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