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