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