PhotoPage.java revision 2bb717f1ea38e2ce33dd102a23afe6bfacb5675c
10c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi/*
20c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi * Copyright (C) 2010 The Android Open Source Project
30c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi *
40c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi * Licensed under the Apache License, Version 2.0 (the "License");
50c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi * you may not use this file except in compliance with the License.
60c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi * You may obtain a copy of the License at
70c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi *
80c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi *      http://www.apache.org/licenses/LICENSE-2.0
90c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi *
100c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi * Unless required by applicable law or agreed to in writing, software
110c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi * distributed under the License is distributed on an "AS IS" BASIS,
120c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
130c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi * See the License for the specific language governing permissions and
140c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi * limitations under the License.
150c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi */
160c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi
170c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yipackage com.android.gallery3d.app;
180c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi
190c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport android.annotation.TargetApi;
200c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport android.app.Activity;
210c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport android.content.ActivityNotFoundException;
220c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport android.content.ContentResolver;
230c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport android.content.Context;
240c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport android.content.Intent;
250c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport android.graphics.Rect;
260c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport android.net.Uri;
270c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport android.nfc.NfcAdapter;
280c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport android.os.Bundle;
290c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport android.os.Handler;
300c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport android.os.Message;
310c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport android.view.Menu;
320c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport android.view.MenuItem;
330c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport android.widget.Toast;
340c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi
350c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport com.android.gallery3d.R;
360c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport com.android.gallery3d.actionbar.ActionBarInterface.OnMenuVisibilityListener;
370c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport com.android.gallery3d.common.ApiHelper;
380c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport com.android.gallery3d.common.Utils;
390c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport com.android.gallery3d.data.DataManager;
400c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport com.android.gallery3d.data.FilterDeleteSet;
410c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport com.android.gallery3d.data.MediaDetails;
420c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport com.android.gallery3d.data.MediaItem;
430c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport com.android.gallery3d.data.MediaObject;
440c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport com.android.gallery3d.data.MediaSet;
450c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport com.android.gallery3d.data.MtpSource;
460c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport com.android.gallery3d.data.Path;
470c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport com.android.gallery3d.data.SnailAlbum;
480c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport com.android.gallery3d.data.SnailItem;
490c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport com.android.gallery3d.data.SnailSource;
500c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport com.android.gallery3d.picasasource.PicasaSource;
510c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport com.android.gallery3d.ui.DetailsHelper;
520c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport com.android.gallery3d.ui.DetailsHelper.CloseListener;
530c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport com.android.gallery3d.ui.DetailsHelper.DetailsSource;
540c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport com.android.gallery3d.ui.GLCanvas;
550c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport com.android.gallery3d.ui.GLRoot;
560c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport com.android.gallery3d.ui.GLRoot.OnGLIdleListener;
570c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport com.android.gallery3d.ui.GLView;
580c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport com.android.gallery3d.ui.ImportCompleteListener;
590c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport com.android.gallery3d.ui.MenuExecutor;
600c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport com.android.gallery3d.ui.PhotoFallbackEffect;
610c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport com.android.gallery3d.ui.PhotoView;
620c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport com.android.gallery3d.ui.SelectionManager;
630c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport com.android.gallery3d.ui.SynchronizedHandler;
640c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport com.android.gallery3d.util.GalleryUtils;
650c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport com.android.gallery3d.util.MediaSetUtils;
660c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi
670c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yipublic class PhotoPage extends ActivityState implements
680c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        PhotoView.Listener, OrientationManager.Listener, AppBridge.Server {
690c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    private static final String TAG = "PhotoPage";
700c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi
710c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    private static final int MSG_HIDE_BARS = 1;
720c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    private static final int MSG_LOCK_ORIENTATION = 2;
730c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    private static final int MSG_UNLOCK_ORIENTATION = 3;
740c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    private static final int MSG_ON_FULL_SCREEN_CHANGED = 4;
750c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    private static final int MSG_UPDATE_ACTION_BAR = 5;
760c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    private static final int MSG_UNFREEZE_GLROOT = 6;
770c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    private static final int MSG_WANT_BARS = 7;
780c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi
790c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    private static final int HIDE_BARS_TIMEOUT = 3500;
800c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    private static final int UNFREEZE_GLROOT_TIMEOUT = 250;
810c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi
820c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    private static final int REQUEST_SLIDESHOW = 1;
830c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    private static final int REQUEST_CROP = 2;
840c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    private static final int REQUEST_CROP_PICASA = 3;
850c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    private static final int REQUEST_EDIT = 4;
860c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    private static final int REQUEST_PLAY_VIDEO = 5;
870c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi
880c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    public static final String KEY_MEDIA_SET_PATH = "media-set-path";
890c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    public static final String KEY_MEDIA_ITEM_PATH = "media-item-path";
900c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    public static final String KEY_INDEX_HINT = "index-hint";
910c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    public static final String KEY_OPEN_ANIMATION_RECT = "open-animation-rect";
920c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    public static final String KEY_APP_BRIDGE = "app-bridge";
930c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    public static final String KEY_TREAT_BACK_AS_UP = "treat-back-as-up";
940c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi
950c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    public static final String KEY_RETURN_INDEX_HINT = "return-index-hint";
960c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi
97    private GalleryApp mApplication;
98    private SelectionManager mSelectionManager;
99
100    private PhotoView mPhotoView;
101    private PhotoPage.Model mModel;
102    private DetailsHelper mDetailsHelper;
103    private boolean mShowDetails;
104    private Path mPendingSharePath;
105
106    // mMediaSet could be null if there is no KEY_MEDIA_SET_PATH supplied.
107    // E.g., viewing a photo in gmail attachment
108    private FilterDeleteSet mMediaSet;
109
110    private int mCurrentIndex = 0;
111    private Handler mHandler;
112    private boolean mShowBars = true;
113    private volatile boolean mActionBarAllowed = true;
114    private GalleryActionBar mActionBar;
115    private boolean mIsMenuVisible;
116    private MediaItem mCurrentPhoto = null;
117    private MenuExecutor mMenuExecutor;
118    private boolean mIsActive;
119    private String mSetPathString;
120    // This is the original mSetPathString before adding the camera preview item.
121    private String mOriginalSetPathString;
122    private AppBridge mAppBridge;
123    private SnailItem mScreenNailItem;
124    private SnailAlbum mScreenNailSet;
125    private OrientationManager mOrientationManager;
126    private boolean mHasActivityResult;
127    private boolean mTreatBackAsUp;
128
129    // The item that is deleted (but it can still be undeleted before commiting)
130    private Path mDeletePath;
131    private boolean mDeleteIsFocus;  // whether the deleted item was in focus
132
133    private NfcAdapter mNfcAdapter;
134
135    private final MyMenuVisibilityListener mMenuVisibilityListener =
136            new MyMenuVisibilityListener();
137
138    public static interface Model extends PhotoView.Model {
139        public void resume();
140        public void pause();
141        public boolean isEmpty();
142        public void setCurrentPhoto(Path path, int indexHint);
143    }
144
145    private class MyMenuVisibilityListener implements OnMenuVisibilityListener {
146        @Override
147        public void onMenuVisibilityChanged(boolean isVisible) {
148            mIsMenuVisible = isVisible;
149            refreshHidingMessage();
150        }
151    }
152
153    private final GLView mRootPane = new GLView() {
154
155        @Override
156        protected void renderBackground(GLCanvas view) {
157            view.clearBuffer(0f, 0f, 0f, 1f);
158        }
159
160        @Override
161        protected void onLayout(
162                boolean changed, int left, int top, int right, int bottom) {
163            mPhotoView.layout(0, 0, right - left, bottom - top);
164            if (mShowDetails) {
165                mDetailsHelper.layout(left, mActionBar.getHeight(), right, bottom);
166            }
167        }
168    };
169
170    @Override
171    public void onCreate(Bundle data, Bundle restoreState) {
172        mActionBar = mActivity.getGalleryActionBar();
173        mSelectionManager = new SelectionManager(mActivity, false);
174        mMenuExecutor = new MenuExecutor(mActivity, mSelectionManager);
175
176        mPhotoView = new PhotoView(mActivity);
177        mPhotoView.setListener(this);
178        mRootPane.addComponent(mPhotoView);
179        mApplication = (GalleryApp)((Activity) mActivity).getApplication();
180        mOrientationManager = mActivity.getOrientationManager();
181        mOrientationManager.addListener(this);
182        mActivity.getGLRoot().setOrientationSource(mOrientationManager);
183
184        mSetPathString = data.getString(KEY_MEDIA_SET_PATH);
185        mOriginalSetPathString = mSetPathString;
186        mNfcAdapter = NfcAdapter.getDefaultAdapter(mActivity.getAndroidContext());
187        Path itemPath = Path.fromString(data.getString(KEY_MEDIA_ITEM_PATH));
188        mTreatBackAsUp = data.getBoolean(KEY_TREAT_BACK_AS_UP, false);
189
190        if (mSetPathString != null) {
191            mAppBridge = (AppBridge) data.getParcelable(KEY_APP_BRIDGE);
192            if (mAppBridge != null) {
193                mAppBridge.setServer(this);
194                mOrientationManager.lockOrientation();
195
196                // Get the ScreenNail from AppBridge and register it.
197                int id = SnailSource.newId();
198                Path screenNailSetPath = SnailSource.getSetPath(id);
199                Path screenNailItemPath = SnailSource.getItemPath(id);
200                mScreenNailSet = (SnailAlbum) mActivity.getDataManager()
201                        .getMediaObject(screenNailSetPath);
202                mScreenNailItem = (SnailItem) mActivity.getDataManager()
203                        .getMediaObject(screenNailItemPath);
204                mScreenNailItem.setScreenNail(mAppBridge.attachScreenNail());
205
206                // Combine the original MediaSet with the one for ScreenNail
207                // from AppBridge.
208                mSetPathString = "/combo/item/{" + screenNailSetPath +
209                        "," + mSetPathString + "}";
210
211                // Start from the screen nail.
212                itemPath = screenNailItemPath;
213
214                // Action bar should not be displayed when camera starts.
215                mFlags |= FLAG_HIDE_ACTION_BAR | FLAG_HIDE_STATUS_BAR;
216                mShowBars = false;
217            }
218
219            MediaSet originalSet = mActivity.getDataManager()
220                    .getMediaSet(mSetPathString);
221            mSelectionManager.setSourceMediaSet(originalSet);
222            mSetPathString = "/filter/delete/{" + mSetPathString + "}";
223            mMediaSet = (FilterDeleteSet) mActivity.getDataManager()
224                    .getMediaSet(mSetPathString);
225            mCurrentIndex = data.getInt(KEY_INDEX_HINT, 0);
226            if (mMediaSet == null) {
227                Log.w(TAG, "failed to restore " + mSetPathString);
228            }
229            PhotoDataAdapter pda = new PhotoDataAdapter(
230                    mActivity, mPhotoView, mMediaSet, itemPath, mCurrentIndex,
231                    mAppBridge == null ? -1 : 0,
232                    mAppBridge == null ? false : mAppBridge.isPanorama());
233            mModel = pda;
234            mPhotoView.setModel(mModel);
235
236            pda.setDataListener(new PhotoDataAdapter.DataListener() {
237
238                @Override
239                public void onPhotoChanged(int index, Path item) {
240                    mCurrentIndex = index;
241                    if (item != null) {
242                        MediaItem photo = mModel.getMediaItem(0);
243                        if (photo != null) updateCurrentPhoto(photo);
244                    }
245                    updateBars();
246                }
247
248                @Override
249                public void onLoadingFinished() {
250                    if (!mModel.isEmpty()) {
251                        MediaItem photo = mModel.getMediaItem(0);
252                        if (photo != null) updateCurrentPhoto(photo);
253                    } else if (mIsActive) {
254                        // We only want to finish the PhotoPage if there is no
255                        // deletion that the user can undo.
256                        if (mMediaSet.getNumberOfDeletions() == 0) {
257                            mActivity.getStateManager().finishState(
258                                    PhotoPage.this);
259                        }
260                    }
261                }
262
263                @Override
264                public void onLoadingStarted() {
265                }
266            });
267        } else {
268            // Get default media set by the URI
269            MediaItem mediaItem = (MediaItem)
270                    mActivity.getDataManager().getMediaObject(itemPath);
271            mModel = new SinglePhotoDataAdapter(mActivity, mPhotoView, mediaItem);
272            mPhotoView.setModel(mModel);
273            updateCurrentPhoto(mediaItem);
274        }
275
276        mHandler = new SynchronizedHandler(mActivity.getGLRoot()) {
277            @Override
278            public void handleMessage(Message message) {
279                switch (message.what) {
280                    case MSG_HIDE_BARS: {
281                        hideBars();
282                        break;
283                    }
284                    case MSG_LOCK_ORIENTATION: {
285                        mOrientationManager.lockOrientation();
286                        break;
287                    }
288                    case MSG_UNLOCK_ORIENTATION: {
289                        mOrientationManager.unlockOrientation();
290                        break;
291                    }
292                    case MSG_ON_FULL_SCREEN_CHANGED: {
293                        mAppBridge.onFullScreenChanged(message.arg1 == 1);
294                        break;
295                    }
296                    case MSG_UPDATE_ACTION_BAR: {
297                        updateBars();
298                        break;
299                    }
300                    case MSG_WANT_BARS: {
301                        wantBars();
302                        break;
303                    }
304                    case MSG_UNFREEZE_GLROOT: {
305                        mActivity.getGLRoot().unfreeze();
306                        break;
307                    }
308                    default: throw new AssertionError(message.what);
309                }
310            }
311        };
312
313        // start the opening animation only if it's not restored.
314        if (restoreState == null) {
315            mPhotoView.setOpenAnimationRect((Rect) data.getParcelable(KEY_OPEN_ANIMATION_RECT));
316        }
317    }
318
319    @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
320    private void setNfcBeamPushUris(Uri[] uris) {
321        if (mNfcAdapter != null && ApiHelper.HAS_SET_BEAM_PUSH_URIS) {
322            mNfcAdapter.setBeamPushUris(uris, (Activity)mActivity);
323        }
324    }
325
326    private Intent createShareIntent(Path path) {
327        DataManager manager = mActivity.getDataManager();
328        int type = manager.getMediaType(path);
329        Intent intent = new Intent(Intent.ACTION_SEND);
330        intent.setType(MenuExecutor.getMimeType(type));
331        Uri uri = manager.getContentUri(path);
332        intent.putExtra(Intent.EXTRA_STREAM, uri);
333        return intent;
334
335    }
336
337    private void updateShareURI(Path path) {
338        if (mActionBar.hasShareMenuItem()) {
339            DataManager manager = mActivity.getDataManager();
340            Uri uri = manager.getContentUri(path);
341            mActionBar.setShareIntent(createShareIntent(path));
342            setNfcBeamPushUris(new Uri[]{uri});
343            mPendingSharePath = null;
344        } else {
345            // This happens when ActionBar is not created yet.
346            mPendingSharePath = path;
347        }
348    }
349
350    private void updateCurrentPhoto(MediaItem photo) {
351        if (mCurrentPhoto == photo) return;
352        mCurrentPhoto = photo;
353        if (mCurrentPhoto == null) return;
354        updateMenuOperations();
355        updateTitle();
356        if (mShowDetails) {
357            mDetailsHelper.reloadDetails();
358        }
359        if ((photo.getSupportedOperations() & MediaItem.SUPPORT_SHARE) != 0) {
360            updateShareURI(photo.getPath());
361        }
362    }
363
364    private void updateTitle() {
365        if (mCurrentPhoto == null) return;
366        boolean showTitle = mActivity.getAndroidContext().getResources().getBoolean(
367                R.bool.show_action_bar_title);
368        if (showTitle && mCurrentPhoto.getName() != null) {
369            mActionBar.setTitle(mCurrentPhoto.getName());
370        } else {
371            mActionBar.setTitle("");
372        }
373    }
374
375    private void updateMenuOperations() {
376        mActionBar.setMenuItemVisible(R.id.action_slideshow, canDoSlideShow());
377        if (mCurrentPhoto == null) return;
378        int supportedOperations = mCurrentPhoto.getSupportedOperations();
379        if (!GalleryUtils.isEditorAvailable((Context) mActivity, "image/*")) {
380            supportedOperations &= ~MediaObject.SUPPORT_EDIT;
381        }
382        MenuExecutor.updateMenuOperation(mActionBar, supportedOperations);
383    }
384
385    private boolean canDoSlideShow() {
386        if (mMediaSet == null || mCurrentPhoto == null) {
387            return false;
388        }
389        if (mCurrentPhoto.getMediaType() != MediaObject.MEDIA_TYPE_IMAGE) {
390            return false;
391        }
392        if (MtpSource.isMtpPath(mOriginalSetPathString)) {
393            return false;
394        }
395        return true;
396    }
397
398    //////////////////////////////////////////////////////////////////////////
399    //  Action Bar show/hide management
400    //////////////////////////////////////////////////////////////////////////
401
402    private void showBars() {
403        if (mShowBars) return;
404        mShowBars = true;
405        mOrientationManager.unlockOrientation();
406        mActionBar.show();
407        mActivity.getGLRoot().setLightsOutMode(false);
408        refreshHidingMessage();
409    }
410
411    private void hideBars() {
412        if (!mShowBars) return;
413        mShowBars = false;
414        mActionBar.hide();
415        mActivity.getGLRoot().setLightsOutMode(true);
416        mHandler.removeMessages(MSG_HIDE_BARS);
417    }
418
419    private void refreshHidingMessage() {
420        mHandler.removeMessages(MSG_HIDE_BARS);
421        if (!mIsMenuVisible) {
422            mHandler.sendEmptyMessageDelayed(MSG_HIDE_BARS, HIDE_BARS_TIMEOUT);
423        }
424    }
425
426    private boolean canShowBars() {
427        // No bars if we are showing camera preview.
428        if (mAppBridge != null && mCurrentIndex == 0) return false;
429        // No bars if it's not allowed.
430        if (!mActionBarAllowed) return false;
431
432        return true;
433    }
434
435    private void wantBars() {
436        if (canShowBars()) showBars();
437    }
438
439    private void toggleBars() {
440        if (mShowBars) {
441            hideBars();
442        } else {
443            if (canShowBars()) showBars();
444        }
445    }
446
447    private void updateBars() {
448        if (!canShowBars()) {
449            hideBars();
450        }
451    }
452
453    @Override
454    public void onOrientationCompensationChanged() {
455        mActivity.getGLRoot().requestLayoutContentPane();
456    }
457
458    @Override
459    protected void onBackPressed() {
460        if (mShowDetails) {
461            hideDetails();
462        } else if (mAppBridge == null || !switchWithCaptureAnimation(-1)) {
463            // We are leaving this page. Set the result now.
464            setResult();
465            if (mTreatBackAsUp) {
466                onUpPressed();
467            } else {
468                super.onBackPressed();
469            }
470        }
471    }
472
473    private void onUpPressed() {
474        if (mActivity.getStateManager().getStateCount() > 1) {
475            super.onBackPressed();
476            return;
477        }
478
479        if (mOriginalSetPathString == null) return;
480
481        if (mAppBridge == null) {
482            // We're in view mode so set up the stacks on our own.
483            Bundle data = new Bundle(getData());
484            data.putString(AlbumPage.KEY_MEDIA_PATH, mOriginalSetPathString);
485            data.putString(AlbumPage.KEY_PARENT_MEDIA_PATH,
486                    mActivity.getDataManager().getTopSetPath(
487                            DataManager.INCLUDE_ALL));
488            mActivity.getStateManager().switchState(this, AlbumPage.class, data);
489        } else {
490            // Start the real gallery activity to view the camera roll.
491            Uri uri = Uri.parse("content://media/external/file?bucketId="
492                    + MediaSetUtils.CAMERA_BUCKET_ID);
493            Intent intent = new Intent(Intent.ACTION_VIEW);
494            intent.setDataAndType(uri, ContentResolver.CURSOR_DIR_BASE_TYPE + "/image");
495            ((Activity) mActivity).startActivity(intent);
496        }
497    }
498
499    private void setResult() {
500        Intent result = null;
501        if (!mPhotoView.getFilmMode()) {
502            result = new Intent();
503            result.putExtra(KEY_RETURN_INDEX_HINT, mCurrentIndex);
504        }
505        setStateResult(Activity.RESULT_OK, result);
506    }
507
508    //////////////////////////////////////////////////////////////////////////
509    //  AppBridge.Server interface
510    //////////////////////////////////////////////////////////////////////////
511
512    @Override
513    public void setCameraRelativeFrame(Rect frame) {
514        mPhotoView.setCameraRelativeFrame(frame);
515    }
516
517    @Override
518    public boolean switchWithCaptureAnimation(int offset) {
519        return mPhotoView.switchWithCaptureAnimation(offset);
520    }
521
522    @Override
523    public void setSwipingEnabled(boolean enabled) {
524        mPhotoView.setSwipingEnabled(enabled);
525    }
526
527    @Override
528    public void notifyScreenNailChanged() {
529        mScreenNailItem.setScreenNail(mAppBridge.attachScreenNail());
530        mScreenNailSet.notifyChange();
531    }
532
533    @Override
534    protected boolean onCreateActionBar(Menu menu) {
535        GalleryActionBar actionBar = mActionBar;
536        boolean result = actionBar.createActionMenu(menu, R.menu.photo);
537        if (mPendingSharePath != null) updateShareURI(mPendingSharePath);
538        updateMenuOperations();
539        updateTitle();
540        return result;
541    }
542
543    private MenuExecutor.ProgressListener mConfirmDialogListener =
544            new MenuExecutor.ProgressListener() {
545        @Override
546        public void onProgressUpdate(int index) {}
547
548        @Override
549        public void onProgressComplete(int result) {}
550
551        @Override
552        public void onConfirmDialogShown() {
553            mHandler.removeMessages(MSG_HIDE_BARS);
554        }
555
556        @Override
557        public void onConfirmDialogDismissed(boolean confirmed) {
558            refreshHidingMessage();
559        }
560
561        @Override
562        public void onProgressStart() {}
563    };
564
565    @Override
566    protected boolean onItemSelected(MenuItem item) {
567        refreshHidingMessage();
568        MediaItem current = mModel.getMediaItem(0);
569
570        if (current == null) {
571            // item is not ready, ignore
572            return true;
573        }
574
575        int currentIndex = mModel.getCurrentIndex();
576        Path path = current.getPath();
577
578        DataManager manager = mActivity.getDataManager();
579        int action = item.getItemId();
580        String confirmMsg = null;
581        switch (action) {
582            case android.R.id.home: {
583                onUpPressed();
584                return true;
585            }
586            case R.id.action_slideshow: {
587                Bundle data = new Bundle();
588                data.putString(SlideshowPage.KEY_SET_PATH, mMediaSet.getPath().toString());
589                data.putString(SlideshowPage.KEY_ITEM_PATH, path.toString());
590                data.putInt(SlideshowPage.KEY_PHOTO_INDEX, currentIndex);
591                data.putBoolean(SlideshowPage.KEY_REPEAT, true);
592                mActivity.getStateManager().startStateForResult(
593                        SlideshowPage.class, REQUEST_SLIDESHOW, data);
594                return true;
595            }
596            case R.id.action_crop: {
597                Activity activity = (Activity) mActivity;
598                Intent intent = new Intent(CropImage.CROP_ACTION);
599                intent.setClass(activity, CropImage.class);
600                intent.setData(manager.getContentUri(path));
601                activity.startActivityForResult(intent, PicasaSource.isPicasaImage(current)
602                        ? REQUEST_CROP_PICASA
603                        : REQUEST_CROP);
604                return true;
605            }
606            case R.id.action_edit: {
607                Intent intent = new Intent(Intent.ACTION_EDIT)
608                        .setData(manager.getContentUri(path))
609                        .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
610                ((Activity) mActivity).startActivityForResult(Intent.createChooser(intent, null),
611                        REQUEST_EDIT);
612                return true;
613            }
614            case R.id.action_details: {
615                if (mShowDetails) {
616                    hideDetails();
617                } else {
618                    showDetails();
619                }
620                return true;
621            }
622            case R.id.action_delete:
623                confirmMsg = mActivity.getResources().getQuantityString(
624                        R.plurals.delete_selection, 1);
625            case R.id.action_setas:
626            case R.id.action_rotate_ccw:
627            case R.id.action_rotate_cw:
628            case R.id.action_show_on_map:
629                mSelectionManager.deSelectAll();
630                mSelectionManager.toggle(path);
631                mMenuExecutor.onMenuClicked(item, confirmMsg, mConfirmDialogListener);
632                return true;
633            case R.id.action_import:
634                mSelectionManager.deSelectAll();
635                mSelectionManager.toggle(path);
636                mMenuExecutor.onMenuClicked(item, confirmMsg,
637                        new ImportCompleteListener(mActivity));
638                return true;
639            case R.id.action_share:
640                Activity activity = (Activity) mActivity;
641                Intent intent = createShareIntent(mCurrentPhoto.getPath());
642                activity.startActivity(Intent.createChooser(intent,
643                        activity.getString(R.string.share)));
644                return true;
645            default :
646                return false;
647        }
648    }
649
650    private void hideDetails() {
651        mShowDetails = false;
652        mDetailsHelper.hide();
653    }
654
655    private void showDetails() {
656        mShowDetails = true;
657        if (mDetailsHelper == null) {
658            mDetailsHelper = new DetailsHelper(mActivity, mRootPane, new MyDetailsSource());
659            mDetailsHelper.setCloseListener(new CloseListener() {
660                @Override
661                public void onClose() {
662                    hideDetails();
663                }
664            });
665        }
666        mDetailsHelper.show();
667    }
668
669    ////////////////////////////////////////////////////////////////////////////
670    //  Callbacks from PhotoView
671    ////////////////////////////////////////////////////////////////////////////
672    @Override
673    public void onSingleTapUp(int x, int y) {
674        if (mAppBridge != null) {
675            if (mAppBridge.onSingleTapUp(x, y)) return;
676        }
677
678        MediaItem item = mModel.getMediaItem(0);
679        if (item == null || item == mScreenNailItem) {
680            // item is not ready or it is camera preview, ignore
681            return;
682        }
683
684        boolean playVideo =
685                (item.getSupportedOperations() & MediaItem.SUPPORT_PLAY) != 0;
686
687        if (playVideo) {
688            // determine if the point is at center (1/6) of the photo view.
689            // (The position of the "play" icon is at center (1/6) of the photo)
690            int w = mPhotoView.getWidth();
691            int h = mPhotoView.getHeight();
692            playVideo = (Math.abs(x - w / 2) * 12 <= w)
693                && (Math.abs(y - h / 2) * 12 <= h);
694        }
695
696        if (playVideo) {
697            playVideo((Activity) mActivity, item.getPlayUri(), item.getName());
698        } else {
699            toggleBars();
700        }
701    }
702
703    @Override
704    public void lockOrientation() {
705        mHandler.sendEmptyMessage(MSG_LOCK_ORIENTATION);
706    }
707
708    @Override
709    public void unlockOrientation() {
710        mHandler.sendEmptyMessage(MSG_UNLOCK_ORIENTATION);
711    }
712
713    @Override
714    public void onActionBarAllowed(boolean allowed) {
715        mActionBarAllowed = allowed;
716        mHandler.sendEmptyMessage(MSG_UPDATE_ACTION_BAR);
717    }
718
719    @Override
720    public void onActionBarWanted() {
721        mHandler.sendEmptyMessage(MSG_WANT_BARS);
722    }
723
724    @Override
725    public void onFullScreenChanged(boolean full) {
726        Message m = mHandler.obtainMessage(
727                MSG_ON_FULL_SCREEN_CHANGED, full ? 1 : 0, 0);
728        m.sendToTarget();
729    }
730
731    // How we do delete/undo:
732    //
733    // When the user choose to delete a media item, we just tell the
734    // FilterDeleteSet to hide that item. If the user choose to undo it, we
735    // again tell FilterDeleteSet not to hide it. If the user choose to commit
736    // the deletion, we then actually delete the media item.
737    @Override
738    public void onDeleteImage(Path path, int offset) {
739        onCommitDeleteImage();  // commit the previous deletion
740        mDeletePath = path;
741        mDeleteIsFocus = (offset == 0);
742        mMediaSet.addDeletion(path, mCurrentIndex + offset);
743    }
744
745    @Override
746    public void onUndoDeleteImage() {
747        if (mDeletePath == null) return;
748        // If the deletion was done on the focused item, we want the model to
749        // focus on it when it is undeleted.
750        if (mDeleteIsFocus) mModel.setFocusHintPath(mDeletePath);
751        mMediaSet.removeDeletion(mDeletePath);
752        mDeletePath = null;
753    }
754
755    @Override
756    public void onCommitDeleteImage() {
757        if (mDeletePath == null) return;
758        mSelectionManager.deSelectAll();
759        mSelectionManager.toggle(mDeletePath);
760        mMenuExecutor.onMenuClicked(R.id.action_delete, null, true, false);
761        mDeletePath = null;
762    }
763
764    public static void playVideo(Activity activity, Uri uri, String title) {
765        try {
766            Intent intent = new Intent(Intent.ACTION_VIEW)
767                    .setDataAndType(uri, "video/*")
768                    .putExtra(Intent.EXTRA_TITLE, title)
769                    .putExtra(MovieActivity.KEY_TREAT_UP_AS_BACK, true);
770            activity.startActivityForResult(intent, REQUEST_PLAY_VIDEO);
771        } catch (ActivityNotFoundException e) {
772            Toast.makeText(activity, activity.getString(R.string.video_err),
773                    Toast.LENGTH_SHORT).show();
774        }
775    }
776
777    private void setCurrentPhotoByIntent(Intent intent) {
778        if (intent == null) return;
779        Path path = mApplication.getDataManager()
780                .findPathByUri(intent.getData(), intent.getType());
781        if (path != null) {
782            mModel.setCurrentPhoto(path, mCurrentIndex);
783        }
784    }
785
786    @Override
787    protected void onStateResult(int requestCode, int resultCode, Intent data) {
788        mHasActivityResult = true;
789        switch (requestCode) {
790            case REQUEST_EDIT:
791                setCurrentPhotoByIntent(data);
792                break;
793            case REQUEST_CROP:
794                if (resultCode == Activity.RESULT_OK) {
795                    setCurrentPhotoByIntent(data);
796                }
797                break;
798            case REQUEST_CROP_PICASA: {
799                if (resultCode == Activity.RESULT_OK) {
800                    Context context = mActivity.getAndroidContext();
801                    String message = context.getString(R.string.crop_saved,
802                            context.getString(R.string.folder_download));
803                    Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
804                }
805                break;
806            }
807            case REQUEST_SLIDESHOW: {
808                if (data == null) break;
809                String path = data.getStringExtra(SlideshowPage.KEY_ITEM_PATH);
810                int index = data.getIntExtra(SlideshowPage.KEY_PHOTO_INDEX, 0);
811                if (path != null) {
812                    mModel.setCurrentPhoto(Path.fromString(path), index);
813                }
814            }
815        }
816    }
817
818    private class PreparePhotoFallback implements OnGLIdleListener {
819        private PhotoFallbackEffect mPhotoFallback = new PhotoFallbackEffect();
820        private boolean mResultReady = false;
821
822        public synchronized PhotoFallbackEffect get() {
823            while (!mResultReady) {
824                Utils.waitWithoutInterrupt(this);
825            }
826            return mPhotoFallback;
827        }
828
829        @Override
830        public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) {
831            mPhotoFallback = mPhotoView.buildFallbackEffect(mRootPane, canvas);
832            synchronized (this) {
833                mResultReady = true;
834                notifyAll();
835            }
836            return false;
837        }
838    }
839
840    private void preparePhotoFallbackView() {
841        GLRoot root = mActivity.getGLRoot();
842        PreparePhotoFallback task = new PreparePhotoFallback();
843        root.unlockRenderThread();
844        PhotoFallbackEffect anim;
845        try {
846            root.addOnGLIdleListener(task);
847            anim = task.get();
848        } finally {
849            root.lockRenderThread();
850        }
851        mActivity.getTransitionStore().put(
852                AlbumPage.KEY_RESUME_ANIMATION, anim);
853    }
854
855    @Override
856    public void onPause() {
857        super.onPause();
858        mIsActive = false;
859
860        mActivity.getGLRoot().unfreeze();
861        mHandler.removeMessages(MSG_UNFREEZE_GLROOT);
862        if (isFinishing()) preparePhotoFallbackView();
863
864        DetailsHelper.pause();
865        mPhotoView.pause();
866        mModel.pause();
867        mHandler.removeMessages(MSG_HIDE_BARS);
868        mActionBar.removeOnMenuVisibilityListener(mMenuVisibilityListener);
869
870        onCommitDeleteImage();
871        mMenuExecutor.pause();
872        if (mMediaSet != null) mMediaSet.clearDeletion();
873    }
874
875    @Override
876    public void onCurrentImageUpdated() {
877        mActivity.getGLRoot().unfreeze();
878    }
879
880    @Override
881    protected void onResume() {
882        super.onResume();
883        mActivity.getGLRoot().freeze();
884        mIsActive = true;
885        setContentPane(mRootPane);
886
887        mModel.resume();
888        mPhotoView.resume();
889        mActionBar.setDisplayOptions(mSetPathString != null, true);
890        mActionBar.addOnMenuVisibilityListener(mMenuVisibilityListener);
891
892        if (mAppBridge != null && !mHasActivityResult) {
893            mPhotoView.resetToFirstPicture();
894        }
895        mHasActivityResult = false;
896        mHandler.sendEmptyMessageDelayed(MSG_UNFREEZE_GLROOT, UNFREEZE_GLROOT_TIMEOUT);
897    }
898
899    @Override
900    protected void onDestroy() {
901        if (mAppBridge != null) {
902            mAppBridge.setServer(null);
903            mScreenNailItem.setScreenNail(null);
904            mAppBridge.detachScreenNail();
905            mAppBridge = null;
906            mScreenNailSet = null;
907            mScreenNailItem = null;
908        }
909        mOrientationManager.removeListener(this);
910        mActivity.getGLRoot().setOrientationSource(null);
911
912        // Remove all pending messages.
913        mHandler.removeCallbacksAndMessages(null);
914        super.onDestroy();
915    }
916
917    private class MyDetailsSource implements DetailsSource {
918
919        @Override
920        public MediaDetails getDetails() {
921            return mModel.getMediaItem(0).getDetails();
922        }
923
924        @Override
925        public int size() {
926            return mMediaSet != null ? mMediaSet.getMediaItemCount() : 1;
927        }
928
929        @Override
930        public int setIndex() {
931            return mModel.getCurrentIndex();
932        }
933    }
934}
935