OSDN Git Service

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