PhotoPage.java revision 21a412c93c9b0401b039f4eb86d4950752e5bb84
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.app.ActionBar.OnMenuVisibilityListener;
20import android.app.Activity;
21import android.content.ActivityNotFoundException;
22import android.content.Context;
23import android.content.Intent;
24import android.graphics.Rect;
25import android.net.Uri;
26import android.nfc.NfcAdapter;
27import android.os.Bundle;
28import android.os.Handler;
29import android.os.Message;
30import android.view.Menu;
31import android.view.MenuInflater;
32import android.view.MenuItem;
33import android.view.View;
34import android.view.WindowManager;
35import android.widget.ShareActionProvider;
36import android.widget.Toast;
37
38import com.android.gallery3d.R;
39import com.android.gallery3d.data.DataManager;
40import com.android.gallery3d.data.MediaDetails;
41import com.android.gallery3d.data.MediaItem;
42import com.android.gallery3d.data.MediaObject;
43import com.android.gallery3d.data.MediaSet;
44import com.android.gallery3d.data.MtpDevice;
45import com.android.gallery3d.data.Path;
46import com.android.gallery3d.data.SnailSource;
47import com.android.gallery3d.picasasource.PicasaSource;
48import com.android.gallery3d.ui.DetailsHelper;
49import com.android.gallery3d.ui.DetailsHelper.CloseListener;
50import com.android.gallery3d.ui.DetailsHelper.DetailsSource;
51import com.android.gallery3d.ui.GLCanvas;
52import com.android.gallery3d.ui.GLView;
53import com.android.gallery3d.ui.ImportCompleteListener;
54import com.android.gallery3d.ui.MenuExecutor;
55import com.android.gallery3d.ui.PhotoView;
56import com.android.gallery3d.ui.ScreenNail;
57import com.android.gallery3d.ui.ScreenNailHolder;
58import com.android.gallery3d.ui.SelectionManager;
59import com.android.gallery3d.ui.SynchronizedHandler;
60import com.android.gallery3d.ui.UserInteractionListener;
61import com.android.gallery3d.util.GalleryUtils;
62
63public class PhotoPage extends ActivityState
64        implements PhotoView.PhotoTapListener, UserInteractionListener {
65    private static final String TAG = "PhotoPage";
66
67    private static final int MSG_HIDE_BARS = 1;
68
69    private static final int HIDE_BARS_TIMEOUT = 3500;
70
71    private static final int REQUEST_SLIDESHOW = 1;
72    private static final int REQUEST_CROP = 2;
73    private static final int REQUEST_CROP_PICASA = 3;
74
75    public static final String KEY_MEDIA_SET_PATH = "media-set-path";
76    public static final String KEY_MEDIA_ITEM_PATH = "media-item-path";
77    public static final String KEY_INDEX_HINT = "index-hint";
78    public static final String KEY_OPEN_ANIMATION_RECT = "open-animation-rect";
79    public static final String KEY_SCREENNAIL_HOLDER = "screennail-holder";
80
81    private GalleryApp mApplication;
82    private SelectionManager mSelectionManager;
83
84    private PhotoView mPhotoView;
85    private PhotoPage.Model mModel;
86    private DetailsHelper mDetailsHelper;
87    private boolean mShowDetails;
88    private Path mPendingSharePath;
89
90    // mMediaSet could be null if there is no KEY_MEDIA_SET_PATH supplied.
91    // E.g., viewing a photo in gmail attachment
92    private MediaSet mMediaSet;
93    private Menu mMenu;
94
95    private final Intent mResultIntent = new Intent();
96    private int mCurrentIndex = 0;
97    private Handler mHandler;
98    private boolean mShowBars = true;
99    private GalleryActionBar mActionBar;
100    private MyMenuVisibilityListener mMenuVisibilityListener;
101    private PageTapListener mPageTapListener;
102    private boolean mIsMenuVisible;
103    private boolean mIsInteracting;
104    private MediaItem mCurrentPhoto = null;
105    private MenuExecutor mMenuExecutor;
106    private boolean mIsActive;
107    private ShareActionProvider mShareActionProvider;
108    private String mSetPathString;
109    private ScreenNailHolder mScreenNailHolder;
110    private ScreenNail mScreenNail;
111
112    private NfcAdapter mNfcAdapter;
113
114    public static interface Model extends PhotoView.Model {
115        public void resume();
116        public void pause();
117        public boolean isEmpty();
118        public MediaItem getCurrentMediaItem();
119        public int getCurrentIndex();
120        public void setCurrentPhoto(Path path, int indexHint);
121    }
122
123    private class MyMenuVisibilityListener implements OnMenuVisibilityListener {
124        public void onMenuVisibilityChanged(boolean isVisible) {
125            mIsMenuVisible = isVisible;
126            refreshHidingMessage();
127        }
128    }
129
130    public interface PageTapListener {
131        // Return true if the tap is consumed.
132        public boolean onSingleTapUp(int x, int y);
133    }
134
135    public void setPageTapListener(PageTapListener listener) {
136        mPageTapListener = listener;
137    }
138
139    private final GLView mRootPane = new GLView() {
140
141        @Override
142        protected void renderBackground(GLCanvas view) {
143            view.clearBuffer();
144        }
145
146        @Override
147        protected void onLayout(
148                boolean changed, int left, int top, int right, int bottom) {
149            mPhotoView.layout(0, 0, right - left, bottom - top);
150            if (mShowDetails) {
151                mDetailsHelper.layout(left, mActionBar.getHeight(), right, bottom);
152            }
153        }
154    };
155
156    @Override
157    public void onCreate(Bundle data, Bundle restoreState) {
158        mActionBar = mActivity.getGalleryActionBar();
159        mSelectionManager = new SelectionManager(mActivity, false);
160        mMenuExecutor = new MenuExecutor(mActivity, mSelectionManager);
161
162        mPhotoView = new PhotoView(mActivity);
163        mPhotoView.setPhotoTapListener(this);
164        mRootPane.addComponent(mPhotoView);
165        mApplication = (GalleryApp)((Activity) mActivity).getApplication();
166
167        mSetPathString = data.getString(KEY_MEDIA_SET_PATH);
168        mNfcAdapter = NfcAdapter.getDefaultAdapter(mActivity.getAndroidContext());
169        Path itemPath = Path.fromString(data.getString(KEY_MEDIA_ITEM_PATH));
170
171        if (mSetPathString != null) {
172            mScreenNailHolder =
173                (ScreenNailHolder) data.getParcelable(KEY_SCREENNAIL_HOLDER);
174            if (mScreenNailHolder != null) {
175                mScreenNail = mScreenNailHolder.attach();
176
177                // Get the ScreenNail from ScreenNailHolder and register it.
178                int id = SnailSource.registerScreenNail(mScreenNail);
179                Path screenNailSetPath = SnailSource.getSetPath(id);
180                Path screenNailItemPath = SnailSource.getItemPath(id);
181
182                // Combine the original MediaSet with the one for CameraScreenNail.
183                mSetPathString = "/combo/item/{" + screenNailSetPath +
184                        "," + mSetPathString + "}";
185
186                // Start from the screen nail.
187                itemPath = screenNailItemPath;
188            }
189
190            mMediaSet = mActivity.getDataManager().getMediaSet(mSetPathString);
191            mCurrentIndex = data.getInt(KEY_INDEX_HINT, 0);
192            mMediaSet = (MediaSet)
193                    mActivity.getDataManager().getMediaObject(mSetPathString);
194            if (mMediaSet == null) {
195                Log.w(TAG, "failed to restore " + mSetPathString);
196            }
197            PhotoDataAdapter pda = new PhotoDataAdapter(
198                    mActivity, mPhotoView, mMediaSet, itemPath, mCurrentIndex);
199            mModel = pda;
200            mPhotoView.setModel(mModel);
201
202            mResultIntent.putExtra(KEY_INDEX_HINT, mCurrentIndex);
203            setStateResult(Activity.RESULT_OK, mResultIntent);
204
205            pda.setDataListener(new PhotoDataAdapter.DataListener() {
206
207                @Override
208                public void onPhotoChanged(int index, Path item) {
209                    mCurrentIndex = index;
210                    mResultIntent.putExtra(KEY_INDEX_HINT, index);
211                    if (item != null) {
212                        mResultIntent.putExtra(KEY_MEDIA_ITEM_PATH, item.toString());
213                        MediaItem photo = mModel.getCurrentMediaItem();
214                        if (photo != null) updateCurrentPhoto(photo);
215                    } else {
216                        mResultIntent.removeExtra(KEY_MEDIA_ITEM_PATH);
217                    }
218                    setStateResult(Activity.RESULT_OK, mResultIntent);
219                }
220
221                @Override
222                public void onLoadingFinished() {
223                    GalleryUtils.setSpinnerVisibility((Activity) mActivity, false);
224                    if (!mModel.isEmpty()) {
225                        MediaItem photo = mModel.getCurrentMediaItem();
226                        if (photo != null) updateCurrentPhoto(photo);
227                    } else if (mIsActive) {
228                        mActivity.getStateManager().finishState(PhotoPage.this);
229                    }
230                }
231
232                @Override
233                public void onLoadingStarted() {
234                    GalleryUtils.setSpinnerVisibility((Activity) mActivity, true);
235                }
236            });
237        } else {
238            // Get default media set by the URI
239            MediaItem mediaItem = (MediaItem)
240                    mActivity.getDataManager().getMediaObject(itemPath);
241            mModel = new SinglePhotoDataAdapter(mActivity, mPhotoView, mediaItem);
242            mPhotoView.setModel(mModel);
243            updateCurrentPhoto(mediaItem);
244        }
245
246        mHandler = new SynchronizedHandler(mActivity.getGLRoot()) {
247            @Override
248            public void handleMessage(Message message) {
249                switch (message.what) {
250                    case MSG_HIDE_BARS: {
251                        hideBars();
252                        break;
253                    }
254                    default: throw new AssertionError(message.what);
255                }
256            }
257        };
258
259        // start the opening animation only if it's not restored.
260        if (restoreState == null) {
261            mPhotoView.setOpenAnimationRect((Rect) data.getParcelable(KEY_OPEN_ANIMATION_RECT));
262        }
263    }
264
265    private void updateShareURI(Path path) {
266        if (mShareActionProvider != null) {
267            DataManager manager = mActivity.getDataManager();
268            int type = manager.getMediaType(path);
269            Intent intent = new Intent(Intent.ACTION_SEND);
270            intent.setType(MenuExecutor.getMimeType(type));
271            intent.putExtra(Intent.EXTRA_STREAM, manager.getContentUri(path));
272            mShareActionProvider.setShareIntent(intent);
273            if (mNfcAdapter != null) {
274                mNfcAdapter.setBeamPushUris(new Uri[]{manager.getContentUri(path)},
275                        (Activity)mActivity);
276            }
277            mPendingSharePath = null;
278        } else {
279            // This happens when ActionBar is not created yet.
280            mPendingSharePath = path;
281        }
282    }
283
284    private void updateCurrentPhoto(MediaItem photo) {
285        if (mCurrentPhoto == photo) return;
286        mCurrentPhoto = photo;
287        if (mCurrentPhoto == null) return;
288        updateMenuOperations();
289        updateTitle();
290        if (mShowDetails) {
291            mDetailsHelper.reloadDetails(mModel.getCurrentIndex());
292        }
293        mPhotoView.showVideoPlayIcon(
294                photo.getMediaType() == MediaObject.MEDIA_TYPE_VIDEO);
295
296        if ((photo.getSupportedOperations() & MediaItem.SUPPORT_SHARE) != 0) {
297            updateShareURI(photo.getPath());
298        }
299    }
300
301    private void updateTitle() {
302        if (mCurrentPhoto == null) return;
303        boolean showTitle = mActivity.getAndroidContext().getResources().getBoolean(
304                R.bool.show_action_bar_title);
305        if (showTitle && mCurrentPhoto.getName() != null)
306            mActionBar.setTitle(mCurrentPhoto.getName());
307        else
308            mActionBar.setTitle("");
309    }
310
311    private void updateMenuOperations() {
312        if (mMenu == null) return;
313        MenuItem item = mMenu.findItem(R.id.action_slideshow);
314        if (item != null) {
315            item.setVisible(canDoSlideShow());
316        }
317        if (mCurrentPhoto == null) return;
318        int supportedOperations = mCurrentPhoto.getSupportedOperations();
319        if (!GalleryUtils.isEditorAvailable((Context) mActivity, "image/*")) {
320            supportedOperations &= ~MediaObject.SUPPORT_EDIT;
321        }
322
323        MenuExecutor.updateMenuOperation(mMenu, supportedOperations);
324    }
325
326    private boolean canDoSlideShow() {
327        if (mMediaSet == null || mCurrentPhoto == null) {
328            return false;
329        }
330        if (mCurrentPhoto.getMediaType() != MediaObject.MEDIA_TYPE_IMAGE) {
331            return false;
332        }
333        if (mMediaSet instanceof MtpDevice) {
334            return false;
335        }
336        return true;
337    }
338
339    private void showBars() {
340        if (mShowBars) return;
341        mShowBars = true;
342        mActionBar.show();
343        WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes();
344        params.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE;
345        ((Activity) mActivity).getWindow().setAttributes(params);
346    }
347
348    private void hideBars() {
349        if (!mShowBars) return;
350        mShowBars = false;
351        mActionBar.hide();
352        WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes();
353        params.systemUiVisibility = View. SYSTEM_UI_FLAG_LOW_PROFILE;
354        ((Activity) mActivity).getWindow().setAttributes(params);
355    }
356
357    private void refreshHidingMessage() {
358        mHandler.removeMessages(MSG_HIDE_BARS);
359        if (!mIsMenuVisible && !mIsInteracting) {
360            mHandler.sendEmptyMessageDelayed(MSG_HIDE_BARS, HIDE_BARS_TIMEOUT);
361        }
362    }
363
364    @Override
365    public void onUserInteraction() {
366        showBars();
367        refreshHidingMessage();
368    }
369
370    public void onUserInteractionTap() {
371        if (mShowBars) {
372            hideBars();
373            mHandler.removeMessages(MSG_HIDE_BARS);
374        } else {
375            showBars();
376            refreshHidingMessage();
377        }
378    }
379
380    @Override
381    public void onUserInteractionBegin() {
382        showBars();
383        mIsInteracting = true;
384        refreshHidingMessage();
385    }
386
387    @Override
388    public void onUserInteractionEnd() {
389        mIsInteracting = false;
390
391        // This function could be called from GL thread (in SlotView.render)
392        // and post to the main thread. So, it could be executed while the
393        // activity is paused.
394        if (mIsActive) refreshHidingMessage();
395    }
396
397    @Override
398    protected void onBackPressed() {
399        if (mShowDetails) {
400            hideDetails();
401        } else if (mScreenNail == null
402                || !switchWithCaptureAnimation(-1)) {
403            super.onBackPressed();
404        }
405    }
406
407    // Switch to the previous or next picture using the capture animation.
408    // The offset is -1 to switch to the previous picture, 1 to switch to
409    // the next picture.
410    public boolean switchWithCaptureAnimation(int offset) {
411        return mPhotoView.switchWithCaptureAnimation(offset);
412    }
413
414    @Override
415    protected boolean onCreateActionBar(Menu menu) {
416        MenuInflater inflater = ((Activity) mActivity).getMenuInflater();
417        inflater.inflate(R.menu.photo, menu);
418        mShareActionProvider = GalleryActionBar.initializeShareActionProvider(menu);
419        if (mPendingSharePath != null) updateShareURI(mPendingSharePath);
420        mMenu = menu;
421        mShowBars = true;
422        updateMenuOperations();
423        updateTitle();
424        return true;
425    }
426
427    @Override
428    protected boolean onItemSelected(MenuItem item) {
429        MediaItem current = mModel.getCurrentMediaItem();
430
431        if (current == null) {
432            // item is not ready, ignore
433            return true;
434        }
435
436        int currentIndex = mModel.getCurrentIndex();
437        Path path = current.getPath();
438
439        DataManager manager = mActivity.getDataManager();
440        int action = item.getItemId();
441        boolean needsConfirm = false;
442        switch (action) {
443            case android.R.id.home: {
444                if (mSetPathString != null) {
445                    if (mActivity.getStateManager().getStateCount() > 1) {
446                        onBackPressed();
447                    } else {
448                        // We're in view mode so set up the stacks on our own.
449                        Bundle data = new Bundle(getData());
450                        data.putString(AlbumPage.KEY_MEDIA_PATH, mSetPathString);
451                        data.putString(AlbumPage.KEY_PARENT_MEDIA_PATH,
452                                mActivity.getDataManager().getTopSetPath(
453                                        DataManager.INCLUDE_ALL));
454                        mActivity.getStateManager().switchState(this, AlbumPage.class, data);
455                    }
456                }
457                return true;
458            }
459            case R.id.action_slideshow: {
460                Bundle data = new Bundle();
461                data.putString(SlideshowPage.KEY_SET_PATH, mMediaSet.getPath().toString());
462                data.putString(SlideshowPage.KEY_ITEM_PATH, path.toString());
463                data.putInt(SlideshowPage.KEY_PHOTO_INDEX, currentIndex);
464                data.putBoolean(SlideshowPage.KEY_REPEAT, true);
465                mActivity.getStateManager().startStateForResult(
466                        SlideshowPage.class, REQUEST_SLIDESHOW, data);
467                return true;
468            }
469            case R.id.action_crop: {
470                Activity activity = (Activity) mActivity;
471                Intent intent = new Intent(CropImage.CROP_ACTION);
472                intent.setClass(activity, CropImage.class);
473                intent.setData(manager.getContentUri(path));
474                activity.startActivityForResult(intent, PicasaSource.isPicasaImage(current)
475                        ? REQUEST_CROP_PICASA
476                        : REQUEST_CROP);
477                return true;
478            }
479            case R.id.action_details: {
480                if (mShowDetails) {
481                    hideDetails();
482                } else {
483                    showDetails(currentIndex);
484                }
485                return true;
486            }
487            case R.id.action_delete:
488                needsConfirm = true;
489            case R.id.action_setas:
490            case R.id.action_rotate_ccw:
491            case R.id.action_rotate_cw:
492            case R.id.action_show_on_map:
493            case R.id.action_edit:
494                mSelectionManager.deSelectAll();
495                mSelectionManager.toggle(path);
496                mMenuExecutor.onMenuClicked(item, needsConfirm, null);
497                return true;
498            case R.id.action_import:
499                mSelectionManager.deSelectAll();
500                mSelectionManager.toggle(path);
501                mMenuExecutor.onMenuClicked(item, needsConfirm,
502                        new ImportCompleteListener(mActivity));
503                return true;
504            default :
505                return false;
506        }
507    }
508
509    private void hideDetails() {
510        mShowDetails = false;
511        mDetailsHelper.hide();
512    }
513
514    private void showDetails(int index) {
515        mShowDetails = true;
516        if (mDetailsHelper == null) {
517            mDetailsHelper = new DetailsHelper(mActivity, mRootPane, new MyDetailsSource());
518            mDetailsHelper.setCloseListener(new CloseListener() {
519                public void onClose() {
520                    hideDetails();
521                }
522            });
523        }
524        mDetailsHelper.reloadDetails(index);
525        mDetailsHelper.show();
526    }
527
528    public void onSingleTapUp(int x, int y) {
529        if (mPageTapListener != null) {
530            if (mPageTapListener.onSingleTapUp(x, y)) return;
531        }
532
533        MediaItem item = mModel.getCurrentMediaItem();
534        if (item == null) {
535            // item is not ready, ignore
536            return;
537        }
538
539        boolean playVideo =
540                (item.getSupportedOperations() & MediaItem.SUPPORT_PLAY) != 0;
541
542        if (playVideo) {
543            // determine if the point is at center (1/6) of the photo view.
544            // (The position of the "play" icon is at center (1/6) of the photo)
545            int w = mPhotoView.getWidth();
546            int h = mPhotoView.getHeight();
547            playVideo = (Math.abs(x - w / 2) * 12 <= w)
548                && (Math.abs(y - h / 2) * 12 <= h);
549        }
550
551        if (playVideo) {
552            playVideo((Activity) mActivity, item.getPlayUri(), item.getName());
553        } else {
554            onUserInteractionTap();
555        }
556    }
557
558    public static void playVideo(Activity activity, Uri uri, String title) {
559        try {
560            Intent intent = new Intent(Intent.ACTION_VIEW)
561                    .setDataAndType(uri, "video/*");
562            intent.putExtra(Intent.EXTRA_TITLE, title);
563            activity.startActivity(intent);
564        } catch (ActivityNotFoundException e) {
565            Toast.makeText(activity, activity.getString(R.string.video_err),
566                    Toast.LENGTH_SHORT).show();
567        }
568    }
569
570    @Override
571    protected void onStateResult(int requestCode, int resultCode, Intent data) {
572        switch (requestCode) {
573            case REQUEST_CROP:
574                if (resultCode == Activity.RESULT_OK) {
575                    if (data == null) break;
576                    Path path = mApplication.getDataManager()
577                            .findPathByUri(data.getData(), data.getType());
578                    if (path != null) {
579                        mModel.setCurrentPhoto(path, mCurrentIndex);
580                    }
581                }
582                break;
583            case REQUEST_CROP_PICASA: {
584                int message = resultCode == Activity.RESULT_OK
585                        ? R.string.crop_saved
586                        : R.string.crop_not_saved;
587                Toast.makeText(mActivity.getAndroidContext(),
588                        message, Toast.LENGTH_SHORT).show();
589                break;
590            }
591            case REQUEST_SLIDESHOW: {
592                if (data == null) break;
593                String path = data.getStringExtra(SlideshowPage.KEY_ITEM_PATH);
594                int index = data.getIntExtra(SlideshowPage.KEY_PHOTO_INDEX, 0);
595                if (path != null) {
596                    mModel.setCurrentPhoto(Path.fromString(path), index);
597                }
598            }
599        }
600    }
601
602    @Override
603    public void onPause() {
604        super.onPause();
605        mIsActive = false;
606        DetailsHelper.pause();
607        mPhotoView.pause();
608        mModel.pause();
609        mHandler.removeMessages(MSG_HIDE_BARS);
610        mActionBar.removeOnMenuVisibilityListener(mMenuVisibilityListener);
611
612        mMenuExecutor.pause();
613    }
614
615    @Override
616    protected void onResume() {
617        super.onResume();
618        mIsActive = true;
619        setContentPane(mRootPane);
620
621        mModel.resume();
622        mPhotoView.resume();
623        if (mMenuVisibilityListener == null) {
624            mMenuVisibilityListener = new MyMenuVisibilityListener();
625        }
626        mActionBar.setDisplayOptions(mSetPathString != null, true);
627        mActionBar.addOnMenuVisibilityListener(mMenuVisibilityListener);
628
629        onUserInteraction();
630    }
631
632    @Override
633    protected void onDestroy() {
634        if (mScreenNailHolder != null) {
635            // Unregister the ScreenNail and notify mScreenNailHolder.
636            SnailSource.unregisterScreenNail(mScreenNail);
637            mScreenNailHolder.detach();
638            mScreenNailHolder = null;
639            mScreenNail = null;
640        }
641        super.onDestroy();
642    }
643
644    private class MyDetailsSource implements DetailsSource {
645        private int mIndex;
646
647        @Override
648        public MediaDetails getDetails() {
649            return mModel.getCurrentMediaItem().getDetails();
650        }
651
652        @Override
653        public int size() {
654            return mMediaSet != null ? mMediaSet.getMediaItemCount() : 1;
655        }
656
657        @Override
658        public int findIndex(int indexHint) {
659            mIndex = indexHint;
660            return indexHint;
661        }
662
663        @Override
664        public int getIndex() {
665            return mIndex;
666        }
667    }
668
669}
670