PhotoPage.java revision 67098d1a72fd04e2af06d3a5939cde28c65f70d9
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        } else {
243            // Get default media set by the URI
244            MediaItem mediaItem = (MediaItem)
245                    mActivity.getDataManager().getMediaObject(itemPath);
246            mModel = new SinglePhotoDataAdapter(mActivity, mPhotoView, mediaItem);
247            mPhotoView.setModel(mModel);
248            updateCurrentPhoto(mediaItem);
249        }
250
251        mHandler = new SynchronizedHandler(mActivity.getGLRoot()) {
252            @Override
253            public void handleMessage(Message message) {
254                switch (message.what) {
255                    case MSG_HIDE_BARS: {
256                        hideBars();
257                        break;
258                    }
259                    default: throw new AssertionError(message.what);
260                }
261            }
262        };
263
264        // start the opening animation only if it's not restored.
265        if (restoreState == null) {
266            mPhotoView.setOpenAnimationRect((Rect) data.getParcelable(KEY_OPEN_ANIMATION_RECT));
267        }
268    }
269
270    private void updateShareURI(Path path) {
271        if (mShareActionProvider != null) {
272            DataManager manager = mActivity.getDataManager();
273            int type = manager.getMediaType(path);
274            Intent intent = new Intent(Intent.ACTION_SEND);
275            intent.setType(MenuExecutor.getMimeType(type));
276            intent.putExtra(Intent.EXTRA_STREAM, manager.getContentUri(path));
277            mShareActionProvider.setShareIntent(intent);
278            if (mNfcAdapter != null) {
279                mNfcAdapter.setBeamPushUris(new Uri[]{manager.getContentUri(path)},
280                        (Activity)mActivity);
281            }
282            mPendingSharePath = null;
283        } else {
284            // This happens when ActionBar is not created yet.
285            mPendingSharePath = path;
286        }
287    }
288
289    private void updateCurrentPhoto(MediaItem photo) {
290        if (mCurrentPhoto == photo) return;
291        mCurrentPhoto = photo;
292        if (mCurrentPhoto == null) return;
293        updateMenuOperations();
294        updateTitle();
295        if (mShowDetails) {
296            mDetailsHelper.reloadDetails(mModel.getCurrentIndex());
297        }
298        mPhotoView.showVideoPlayIcon(
299                photo.getMediaType() == MediaObject.MEDIA_TYPE_VIDEO);
300
301        if ((photo.getSupportedOperations() & MediaItem.SUPPORT_SHARE) != 0) {
302            updateShareURI(photo.getPath());
303        }
304    }
305
306    private void updateTitle() {
307        if (mCurrentPhoto == null) return;
308        boolean showTitle = mActivity.getAndroidContext().getResources().getBoolean(
309                R.bool.show_action_bar_title);
310        if (showTitle && mCurrentPhoto.getName() != null)
311            mActionBar.setTitle(mCurrentPhoto.getName());
312        else
313            mActionBar.setTitle("");
314    }
315
316    private void updateMenuOperations() {
317        if (mMenu == null) return;
318        MenuItem item = mMenu.findItem(R.id.action_slideshow);
319        if (item != null) {
320            item.setVisible(canDoSlideShow());
321        }
322        if (mCurrentPhoto == null) return;
323        int supportedOperations = mCurrentPhoto.getSupportedOperations();
324        if (!GalleryUtils.isEditorAvailable((Context) mActivity, "image/*")) {
325            supportedOperations &= ~MediaObject.SUPPORT_EDIT;
326        }
327
328        MenuExecutor.updateMenuOperation(mMenu, supportedOperations);
329    }
330
331    private boolean canDoSlideShow() {
332        if (mMediaSet == null || mCurrentPhoto == null) {
333            return false;
334        }
335        if (mCurrentPhoto.getMediaType() != MediaObject.MEDIA_TYPE_IMAGE) {
336            return false;
337        }
338        if (mMediaSet instanceof MtpDevice) {
339            return false;
340        }
341        return true;
342    }
343
344    private void showBars() {
345        if (mShowBars) return;
346        mShowBars = true;
347        mActionBar.show();
348        WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes();
349        params.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE;
350        ((Activity) mActivity).getWindow().setAttributes(params);
351    }
352
353    private void hideBars() {
354        if (!mShowBars) return;
355        mShowBars = false;
356        mActionBar.hide();
357        WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes();
358        params.systemUiVisibility = View. SYSTEM_UI_FLAG_LOW_PROFILE;
359        ((Activity) mActivity).getWindow().setAttributes(params);
360    }
361
362    private void refreshHidingMessage() {
363        mHandler.removeMessages(MSG_HIDE_BARS);
364        if (!mIsMenuVisible && !mIsInteracting) {
365            mHandler.sendEmptyMessageDelayed(MSG_HIDE_BARS, HIDE_BARS_TIMEOUT);
366        }
367    }
368
369    @Override
370    public void onUserInteraction() {
371        showBars();
372        refreshHidingMessage();
373    }
374
375    public void onUserInteractionTap() {
376        if (mShowBars) {
377            hideBars();
378            mHandler.removeMessages(MSG_HIDE_BARS);
379        } else {
380            showBars();
381            refreshHidingMessage();
382        }
383    }
384
385    @Override
386    public void onUserInteractionBegin() {
387        showBars();
388        mIsInteracting = true;
389        refreshHidingMessage();
390    }
391
392    @Override
393    public void onUserInteractionEnd() {
394        mIsInteracting = false;
395
396        // This function could be called from GL thread (in SlotView.render)
397        // and post to the main thread. So, it could be executed while the
398        // activity is paused.
399        if (mIsActive) refreshHidingMessage();
400    }
401
402    @Override
403    protected void onBackPressed() {
404        if (mShowDetails) {
405            hideDetails();
406        } else {
407            super.onBackPressed();
408        }
409    }
410
411    @Override
412    protected boolean onCreateActionBar(Menu menu) {
413        MenuInflater inflater = ((Activity) mActivity).getMenuInflater();
414        inflater.inflate(R.menu.photo, menu);
415        mShareActionProvider = GalleryActionBar.initializeShareActionProvider(menu);
416        if (mPendingSharePath != null) updateShareURI(mPendingSharePath);
417        mMenu = menu;
418        mShowBars = true;
419        updateMenuOperations();
420        updateTitle();
421        return true;
422    }
423
424    @Override
425    protected boolean onItemSelected(MenuItem item) {
426        MediaItem current = mModel.getCurrentMediaItem();
427
428        if (current == null) {
429            // item is not ready, ignore
430            return true;
431        }
432
433        int currentIndex = mModel.getCurrentIndex();
434        Path path = current.getPath();
435
436        DataManager manager = mActivity.getDataManager();
437        int action = item.getItemId();
438        boolean needsConfirm = false;
439        switch (action) {
440            case android.R.id.home: {
441                if (mSetPathString != null) {
442                    if (mActivity.getStateManager().getStateCount() > 1) {
443                        onBackPressed();
444                    } else {
445                        // We're in view mode so set up the stacks on our own.
446                        Bundle data = new Bundle(getData());
447                        data.putString(AlbumPage.KEY_MEDIA_PATH, mSetPathString);
448                        data.putString(AlbumPage.KEY_PARENT_MEDIA_PATH,
449                                mActivity.getDataManager().getTopSetPath(
450                                        DataManager.INCLUDE_ALL));
451                        mActivity.getStateManager().switchState(this, AlbumPage.class, data);
452                    }
453                }
454                return true;
455            }
456            case R.id.action_slideshow: {
457                Bundle data = new Bundle();
458                data.putString(SlideshowPage.KEY_SET_PATH, mMediaSet.getPath().toString());
459                data.putString(SlideshowPage.KEY_ITEM_PATH, path.toString());
460                data.putInt(SlideshowPage.KEY_PHOTO_INDEX, currentIndex);
461                data.putBoolean(SlideshowPage.KEY_REPEAT, true);
462                mActivity.getStateManager().startStateForResult(
463                        SlideshowPage.class, REQUEST_SLIDESHOW, data);
464                return true;
465            }
466            case R.id.action_crop: {
467                Activity activity = (Activity) mActivity;
468                Intent intent = new Intent(CropImage.CROP_ACTION);
469                intent.setClass(activity, CropImage.class);
470                intent.setData(manager.getContentUri(path));
471                activity.startActivityForResult(intent, PicasaSource.isPicasaImage(current)
472                        ? REQUEST_CROP_PICASA
473                        : REQUEST_CROP);
474                return true;
475            }
476            case R.id.action_details: {
477                if (mShowDetails) {
478                    hideDetails();
479                } else {
480                    showDetails(currentIndex);
481                }
482                return true;
483            }
484            case R.id.action_delete:
485                needsConfirm = true;
486            case R.id.action_setas:
487            case R.id.action_rotate_ccw:
488            case R.id.action_rotate_cw:
489            case R.id.action_show_on_map:
490            case R.id.action_edit:
491                mSelectionManager.deSelectAll();
492                mSelectionManager.toggle(path);
493                mMenuExecutor.onMenuClicked(item, needsConfirm, null);
494                return true;
495            case R.id.action_import:
496                mSelectionManager.deSelectAll();
497                mSelectionManager.toggle(path);
498                mMenuExecutor.onMenuClicked(item, needsConfirm,
499                        new ImportCompleteListener(mActivity));
500                return true;
501            default :
502                return false;
503        }
504    }
505
506    private void hideDetails() {
507        mShowDetails = false;
508        mDetailsHelper.hide();
509    }
510
511    private void showDetails(int index) {
512        mShowDetails = true;
513        if (mDetailsHelper == null) {
514            mDetailsHelper = new DetailsHelper(mActivity, mRootPane, new MyDetailsSource());
515            mDetailsHelper.setCloseListener(new CloseListener() {
516                public void onClose() {
517                    hideDetails();
518                }
519            });
520        }
521        mDetailsHelper.reloadDetails(index);
522        mDetailsHelper.show();
523    }
524
525    public void onSingleTapUp(int x, int y) {
526        if (mPageTapListener != null) {
527            if (mPageTapListener.onSingleTapUp(x, y)) return;
528        }
529
530        MediaItem item = mModel.getCurrentMediaItem();
531        if (item == null) {
532            // item is not ready, ignore
533            return;
534        }
535
536        boolean playVideo =
537                (item.getSupportedOperations() & MediaItem.SUPPORT_PLAY) != 0;
538
539        if (playVideo) {
540            // determine if the point is at center (1/6) of the photo view.
541            // (The position of the "play" icon is at center (1/6) of the photo)
542            int w = mPhotoView.getWidth();
543            int h = mPhotoView.getHeight();
544            playVideo = (Math.abs(x - w / 2) * 12 <= w)
545                && (Math.abs(y - h / 2) * 12 <= h);
546        }
547
548        if (playVideo) {
549            playVideo((Activity) mActivity, item.getPlayUri(), item.getName());
550        } else {
551            onUserInteractionTap();
552        }
553    }
554
555    public static void playVideo(Activity activity, Uri uri, String title) {
556        try {
557            Intent intent = new Intent(Intent.ACTION_VIEW)
558                    .setDataAndType(uri, "video/*");
559            intent.putExtra(Intent.EXTRA_TITLE, title);
560            activity.startActivity(intent);
561        } catch (ActivityNotFoundException e) {
562            Toast.makeText(activity, activity.getString(R.string.video_err),
563                    Toast.LENGTH_SHORT).show();
564        }
565    }
566
567    @Override
568    protected void onStateResult(int requestCode, int resultCode, Intent data) {
569        switch (requestCode) {
570            case REQUEST_CROP:
571                if (resultCode == Activity.RESULT_OK) {
572                    if (data == null) break;
573                    Path path = mApplication
574                            .getDataManager().findPathByUri(data.getData());
575                    if (path != null) {
576                        mModel.setCurrentPhoto(path, mCurrentIndex);
577                    }
578                }
579                break;
580            case REQUEST_CROP_PICASA: {
581                int message = resultCode == Activity.RESULT_OK
582                        ? R.string.crop_saved
583                        : R.string.crop_not_saved;
584                Toast.makeText(mActivity.getAndroidContext(),
585                        message, Toast.LENGTH_SHORT).show();
586                break;
587            }
588            case REQUEST_SLIDESHOW: {
589                if (data == null) break;
590                String path = data.getStringExtra(SlideshowPage.KEY_ITEM_PATH);
591                int index = data.getIntExtra(SlideshowPage.KEY_PHOTO_INDEX, 0);
592                if (path != null) {
593                    mModel.setCurrentPhoto(Path.fromString(path), index);
594                }
595            }
596        }
597    }
598
599    @Override
600    public void onPause() {
601        super.onPause();
602        mIsActive = false;
603        DetailsHelper.pause();
604        mPhotoView.pause();
605        mModel.pause();
606        mHandler.removeMessages(MSG_HIDE_BARS);
607        mActionBar.removeOnMenuVisibilityListener(mMenuVisibilityListener);
608
609        mMenuExecutor.pause();
610    }
611
612    @Override
613    protected void onResume() {
614        super.onResume();
615        mIsActive = true;
616        setContentPane(mRootPane);
617
618        mModel.resume();
619        mPhotoView.resume();
620        if (mMenuVisibilityListener == null) {
621            mMenuVisibilityListener = new MyMenuVisibilityListener();
622        }
623        mActionBar.setDisplayOptions(mSetPathString != null, true);
624        mActionBar.addOnMenuVisibilityListener(mMenuVisibilityListener);
625
626        onUserInteraction();
627    }
628
629    @Override
630    protected void onDestroy() {
631        if (mScreenNailHolder != null) {
632            // Unregister the ScreenNail and notify mScreenNailHolder.
633            SnailSource.unregisterScreenNail(mScreenNail);
634            mScreenNailHolder.detach();
635            mScreenNailHolder = null;
636            mScreenNail = null;
637        }
638        super.onDestroy();
639    }
640
641    private class MyDetailsSource implements DetailsSource {
642        private int mIndex;
643
644        @Override
645        public MediaDetails getDetails() {
646            return mModel.getCurrentMediaItem().getDetails();
647        }
648
649        @Override
650        public int size() {
651            return mMediaSet != null ? mMediaSet.getMediaItemCount() : 1;
652        }
653
654        @Override
655        public int findIndex(int indexHint) {
656            mIndex = indexHint;
657            return indexHint;
658        }
659
660        @Override
661        public int getIndex() {
662            return mIndex;
663        }
664    }
665
666}
667