PhotoPage.java revision 655f63f04638dfe31045eab1b6b4c39cc2db10db
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;
20import android.app.ActionBar.OnMenuVisibilityListener;
21import android.app.Activity;
22import android.content.ActivityNotFoundException;
23import android.content.ContentResolver;
24import android.content.Context;
25import android.content.Intent;
26import android.net.Uri;
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.View.MeasureSpec;
35import android.view.WindowManager;
36import android.widget.ShareActionProvider;
37import android.widget.Toast;
38
39import com.android.gallery3d.R;
40import com.android.gallery3d.data.DataManager;
41import com.android.gallery3d.data.MediaDetails;
42import com.android.gallery3d.data.MediaItem;
43import com.android.gallery3d.data.MediaObject;
44import com.android.gallery3d.data.MediaSet;
45import com.android.gallery3d.data.MtpDevice;
46import com.android.gallery3d.data.Path;
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.FilmStripView;
52import com.android.gallery3d.ui.GLCanvas;
53import com.android.gallery3d.ui.GLView;
54import com.android.gallery3d.ui.ImportCompleteListener;
55import com.android.gallery3d.ui.MenuExecutor;
56import com.android.gallery3d.ui.PhotoView;
57import com.android.gallery3d.ui.PositionRepository;
58import com.android.gallery3d.ui.PositionRepository.Position;
59import com.android.gallery3d.ui.SelectionManager;
60import com.android.gallery3d.ui.SynchronizedHandler;
61import com.android.gallery3d.ui.UserInteractionListener;
62import com.android.gallery3d.util.GalleryUtils;
63
64public class PhotoPage extends ActivityState
65        implements PhotoView.PhotoTapListener, FilmStripView.Listener,
66        UserInteractionListener {
67    private static final String TAG = "PhotoPage";
68
69    private static final int MSG_HIDE_BARS = 1;
70
71    private static final int HIDE_BARS_TIMEOUT = 3500;
72
73    private static final int REQUEST_SLIDESHOW = 1;
74    private static final int REQUEST_CROP = 2;
75    private static final int REQUEST_CROP_PICASA = 3;
76
77    public static final String KEY_MEDIA_SET_PATH = "media-set-path";
78    public static final String KEY_MEDIA_ITEM_PATH = "media-item-path";
79    public static final String KEY_INDEX_HINT = "index-hint";
80
81    private GalleryApp mApplication;
82    private SelectionManager mSelectionManager;
83
84    private PhotoView mPhotoView;
85    private PhotoPage.Model mModel;
86    private FilmStripView mFilmStripView;
87    private DetailsHelper mDetailsHelper;
88    private boolean mShowDetails;
89    private Path mPendingSharePath;
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 ActionBar mActionBar;
101    private MyMenuVisibilityListener mMenuVisibilityListener;
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
110    public static interface Model extends PhotoView.Model {
111        public void resume();
112        public void pause();
113        public boolean isEmpty();
114        public MediaItem getCurrentMediaItem();
115        public int getCurrentIndex();
116        public void setCurrentPhoto(Path path, int indexHint);
117    }
118
119    private class MyMenuVisibilityListener implements OnMenuVisibilityListener {
120        public void onMenuVisibilityChanged(boolean isVisible) {
121            mIsMenuVisible = isVisible;
122            refreshHidingMessage();
123        }
124    }
125
126    private final GLView mRootPane = new GLView() {
127
128        @Override
129        protected void renderBackground(GLCanvas view) {
130            view.clearBuffer();
131        }
132
133        @Override
134        protected void onLayout(
135                boolean changed, int left, int top, int right, int bottom) {
136            mPhotoView.layout(0, 0, right - left, bottom - top);
137            PositionRepository.getInstance(mActivity).setOffset(0, 0);
138            int filmStripHeight = 0;
139            if (mFilmStripView != null) {
140                mFilmStripView.measure(
141                        MeasureSpec.makeMeasureSpec(right - left, MeasureSpec.EXACTLY),
142                        MeasureSpec.UNSPECIFIED);
143                filmStripHeight = mFilmStripView.getMeasuredHeight();
144                mFilmStripView.layout(0, bottom - top - filmStripHeight,
145                        right - left, bottom - top);
146            }
147            if (mShowDetails) {
148                mDetailsHelper.layout(left, GalleryActionBar.getHeight((Activity) mActivity),
149                        right, bottom);
150            }
151        }
152    };
153
154    private void initFilmStripView() {
155        Config.PhotoPage config = Config.PhotoPage.get((Context) mActivity);
156        mFilmStripView = new FilmStripView(mActivity, mMediaSet,
157                config.filmstripTopMargin, config.filmstripMidMargin, config.filmstripBottomMargin,
158                config.filmstripContentSize, config.filmstripThumbSize, config.filmstripBarSize,
159                config.filmstripGripSize, config.filmstripGripWidth);
160        mRootPane.addComponent(mFilmStripView);
161        mFilmStripView.setListener(this);
162        mFilmStripView.setUserInteractionListener(this);
163        mFilmStripView.setFocusIndex(mCurrentIndex);
164        mFilmStripView.setStartIndex(mCurrentIndex);
165        mRootPane.requestLayout();
166        if (mIsActive) mFilmStripView.resume();
167        if (!mShowBars) mFilmStripView.setVisibility(GLView.INVISIBLE);
168    }
169
170    @Override
171    public void onCreate(Bundle data, Bundle restoreState) {
172        mActionBar = ((Activity) mActivity).getActionBar();
173        mSelectionManager = new SelectionManager(mActivity, false);
174        mMenuExecutor = new MenuExecutor(mActivity, mSelectionManager);
175
176        mPhotoView = new PhotoView(mActivity);
177        mPhotoView.setPhotoTapListener(this);
178        mRootPane.addComponent(mPhotoView);
179        mApplication = (GalleryApp)((Activity) mActivity).getApplication();
180
181        mSetPathString = data.getString(KEY_MEDIA_SET_PATH);
182        Path itemPath = Path.fromString(data.getString(KEY_MEDIA_ITEM_PATH));
183
184        if (mSetPathString != null) {
185            mMediaSet = mActivity.getDataManager().getMediaSet(mSetPathString);
186            mCurrentIndex = data.getInt(KEY_INDEX_HINT, 0);
187            mMediaSet = (MediaSet)
188                    mActivity.getDataManager().getMediaObject(mSetPathString);
189            if (mMediaSet == null) {
190                Log.w(TAG, "failed to restore " + mSetPathString);
191            }
192            PhotoDataAdapter pda = new PhotoDataAdapter(
193                    mActivity, mPhotoView, mMediaSet, itemPath, mCurrentIndex);
194            mModel = pda;
195            mPhotoView.setModel(mModel);
196
197            mResultIntent.putExtra(KEY_INDEX_HINT, mCurrentIndex);
198            setStateResult(Activity.RESULT_OK, mResultIntent);
199
200            pda.setDataListener(new PhotoDataAdapter.DataListener() {
201
202                @Override
203                public void onPhotoChanged(int index, Path item) {
204                    if (mFilmStripView != null) mFilmStripView.setFocusIndex(index);
205                    mCurrentIndex = index;
206                    mResultIntent.putExtra(KEY_INDEX_HINT, index);
207                    if (item != null) {
208                        mResultIntent.putExtra(KEY_MEDIA_ITEM_PATH, item.toString());
209                        MediaItem photo = mModel.getCurrentMediaItem();
210                        if (photo != null) updateCurrentPhoto(photo);
211                    } else {
212                        mResultIntent.removeExtra(KEY_MEDIA_ITEM_PATH);
213                    }
214                    setStateResult(Activity.RESULT_OK, mResultIntent);
215                }
216
217                @Override
218                public void onLoadingFinished() {
219                    GalleryUtils.setSpinnerVisibility((Activity) mActivity, false);
220                    if (!mModel.isEmpty()) {
221                        MediaItem photo = mModel.getCurrentMediaItem();
222                        if (photo != null) updateCurrentPhoto(photo);
223                    } else if (mIsActive) {
224                        mActivity.getStateManager().finishState(PhotoPage.this);
225                    }
226                }
227
228                @Override
229                public void onLoadingStarted() {
230                    GalleryUtils.setSpinnerVisibility((Activity) mActivity, true);
231                }
232
233                @Override
234                public void onPhotoAvailable(long version, boolean fullImage) {
235                    if (mFilmStripView == null) initFilmStripView();
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
261        mPhotoView.setOpenedItem(itemPath);
262    }
263
264    private void updateShareURI(Path path) {
265        if (mShareActionProvider != null) {
266            DataManager manager = mActivity.getDataManager();
267            int type = manager.getMediaType(path);
268            Intent intent = new Intent(Intent.ACTION_SEND);
269            intent.setType(MenuExecutor.getMimeType(type));
270            intent.putExtra(Intent.EXTRA_STREAM, manager.getContentUri(path));
271            mShareActionProvider.setShareIntent(intent);
272            mPendingSharePath = null;
273        } else {
274            // This happens when ActionBar is not created yet.
275            mPendingSharePath = path;
276        }
277    }
278
279    private void setTitle(String title) {
280        if (title == null) return;
281        boolean showTitle = mActivity.getAndroidContext().getResources().getBoolean(
282                R.bool.show_action_bar_title);
283        if (showTitle)
284            mActionBar.setTitle(title);
285        else
286            mActionBar.setTitle("");
287    }
288
289    private void updateCurrentPhoto(MediaItem photo) {
290        if (mCurrentPhoto == photo) return;
291        mCurrentPhoto = photo;
292        if (mCurrentPhoto == null) return;
293        updateMenuOperations();
294        if (mShowDetails) {
295            mDetailsHelper.reloadDetails(mModel.getCurrentIndex());
296        }
297        setTitle(photo.getName());
298        mPhotoView.showVideoPlayIcon(
299                photo.getMediaType() == MediaObject.MEDIA_TYPE_VIDEO);
300
301        updateShareURI(photo.getPath());
302    }
303
304    private void updateMenuOperations() {
305        if (mMenu == null) return;
306        MenuItem item = mMenu.findItem(R.id.action_slideshow);
307        if (item != null) {
308            item.setVisible(canDoSlideShow());
309        }
310        if (mCurrentPhoto == null) return;
311        int supportedOperations = mCurrentPhoto.getSupportedOperations();
312        if (!GalleryUtils.isEditorAvailable((Context) mActivity, "image/*")) {
313            supportedOperations &= ~MediaObject.SUPPORT_EDIT;
314        }
315
316        MenuExecutor.updateMenuOperation(mMenu, supportedOperations);
317    }
318
319    private boolean canDoSlideShow() {
320        if (mMediaSet == null || mCurrentPhoto == null) {
321            return false;
322        }
323        if (mCurrentPhoto.getMediaType() != MediaObject.MEDIA_TYPE_IMAGE) {
324            return false;
325        }
326        if (mMediaSet instanceof MtpDevice) {
327            return false;
328        }
329        return true;
330    }
331
332    private void showBars() {
333        if (mShowBars) return;
334        mShowBars = true;
335        mActionBar.show();
336        WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes();
337        params.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE;
338        ((Activity) mActivity).getWindow().setAttributes(params);
339        if (mFilmStripView != null) {
340            mFilmStripView.show();
341        }
342    }
343
344    private void hideBars() {
345        if (!mShowBars) return;
346        mShowBars = false;
347        mActionBar.hide();
348        WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes();
349        params.systemUiVisibility = View. SYSTEM_UI_FLAG_LOW_PROFILE;
350        ((Activity) mActivity).getWindow().setAttributes(params);
351        if (mFilmStripView != null) {
352            mFilmStripView.hide();
353        }
354    }
355
356    private void refreshHidingMessage() {
357        mHandler.removeMessages(MSG_HIDE_BARS);
358        if (!mIsMenuVisible && !mIsInteracting) {
359            mHandler.sendEmptyMessageDelayed(MSG_HIDE_BARS, HIDE_BARS_TIMEOUT);
360        }
361    }
362
363    @Override
364    public void onUserInteraction() {
365        showBars();
366        refreshHidingMessage();
367    }
368
369    public void onUserInteractionTap() {
370        if (mShowBars) {
371            hideBars();
372            mHandler.removeMessages(MSG_HIDE_BARS);
373        } else {
374            showBars();
375            refreshHidingMessage();
376        }
377    }
378
379    @Override
380    public void onUserInteractionBegin() {
381        showBars();
382        mIsInteracting = true;
383        refreshHidingMessage();
384    }
385
386    @Override
387    public void onUserInteractionEnd() {
388        mIsInteracting = false;
389
390        // This function could be called from GL thread (in SlotView.render)
391        // and post to the main thread. So, it could be executed while the
392        // activity is paused.
393        if (mIsActive) refreshHidingMessage();
394    }
395
396    @Override
397    protected void onBackPressed() {
398        if (mShowDetails) {
399            hideDetails();
400        } else {
401            PositionRepository repository = PositionRepository.getInstance(mActivity);
402            repository.clear();
403            if (mCurrentPhoto != null) {
404                Position position = new Position();
405                position.x = mRootPane.getWidth() / 2;
406                position.y = mRootPane.getHeight() / 2;
407                position.z = -1000;
408                repository.putPosition(
409                        System.identityHashCode(mCurrentPhoto.getPath()),
410                        position);
411            }
412            super.onBackPressed();
413        }
414    }
415
416    @Override
417    protected boolean onCreateActionBar(Menu menu) {
418        MenuInflater inflater = ((Activity) mActivity).getMenuInflater();
419        inflater.inflate(R.menu.photo, menu);
420        mShareActionProvider = GalleryActionBar.initializeShareActionProvider(menu);
421        if (mPendingSharePath != null) updateShareURI(mPendingSharePath);
422        mMenu = menu;
423        mShowBars = true;
424        updateMenuOperations();
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                        Activity a = (Activity) mActivity;
449                        Uri uri = mActivity.getDataManager().getContentUri(
450                                Path.fromString(mSetPathString));
451                        Intent intent = new Intent(Intent.ACTION_VIEW)
452                                .setClass(a, Gallery.class)
453                                .setDataAndType(uri, ContentResolver.CURSOR_DIR_BASE_TYPE)
454                                .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP |
455                                        Intent.FLAG_ACTIVITY_NEW_TASK);
456                        a.startActivity(intent);
457                    }
458                }
459                return true;
460            }
461            case R.id.action_slideshow: {
462                Bundle data = new Bundle();
463                data.putString(SlideshowPage.KEY_SET_PATH, mMediaSet.getPath().toString());
464                data.putString(SlideshowPage.KEY_ITEM_PATH, path.toString());
465                data.putInt(SlideshowPage.KEY_PHOTO_INDEX, currentIndex);
466                data.putBoolean(SlideshowPage.KEY_REPEAT, true);
467                mActivity.getStateManager().startStateForResult(
468                        SlideshowPage.class, REQUEST_SLIDESHOW, data);
469                return true;
470            }
471            case R.id.action_crop: {
472                Activity activity = (Activity) mActivity;
473                Intent intent = new Intent(CropImage.CROP_ACTION);
474                intent.setClass(activity, CropImage.class);
475                intent.setData(manager.getContentUri(path));
476                activity.startActivityForResult(intent, PicasaSource.isPicasaImage(current)
477                        ? REQUEST_CROP_PICASA
478                        : REQUEST_CROP);
479                return true;
480            }
481            case R.id.action_details: {
482                if (mShowDetails) {
483                    hideDetails();
484                } else {
485                    showDetails(currentIndex);
486                }
487                return true;
488            }
489            case R.id.action_setas:
490            case R.id.action_confirm_delete:
491            case R.id.action_rotate_ccw:
492            case R.id.action_rotate_cw:
493            case R.id.action_show_on_map:
494            case R.id.action_edit:
495                mSelectionManager.deSelectAll();
496                mSelectionManager.toggle(path);
497                mMenuExecutor.onMenuClicked(item, null);
498                return true;
499            case R.id.action_import:
500                mSelectionManager.deSelectAll();
501                mSelectionManager.toggle(path);
502                mMenuExecutor.onMenuClicked(item,
503                        new ImportCompleteListener(mActivity));
504                return true;
505            default :
506                return false;
507        }
508    }
509
510    private void hideDetails() {
511        mShowDetails = false;
512        mDetailsHelper.hide();
513    }
514
515    private void showDetails(int index) {
516        mShowDetails = true;
517        if (mDetailsHelper == null) {
518            mDetailsHelper = new DetailsHelper(mActivity, mRootPane, new MyDetailsSource());
519            mDetailsHelper.setCloseListener(new CloseListener() {
520                public void onClose() {
521                    hideDetails();
522                }
523            });
524        }
525        mDetailsHelper.reloadDetails(index);
526        mDetailsHelper.show();
527    }
528
529    public void onSingleTapUp(int x, int y) {
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    // Called by FileStripView.
568    // Returns false if it cannot jump to the specified index at this time.
569    public boolean onSlotSelected(int slotIndex) {
570        return mPhotoView.jumpTo(slotIndex);
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
580                            .getDataManager().findPathByUri(data.getData());
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        if (mFilmStripView != null) {
610            mFilmStripView.pause();
611        }
612        DetailsHelper.pause();
613        mPhotoView.pause();
614        mModel.pause();
615        mHandler.removeMessages(MSG_HIDE_BARS);
616        mActionBar.removeOnMenuVisibilityListener(mMenuVisibilityListener);
617        mMenuExecutor.pause();
618    }
619
620    @Override
621    protected void onResume() {
622        super.onResume();
623        mIsActive = true;
624        setContentPane(mRootPane);
625        mModel.resume();
626        mPhotoView.resume();
627        if (mFilmStripView != null) {
628            mFilmStripView.resume();
629        }
630        if (mMenuVisibilityListener == null) {
631            mMenuVisibilityListener = new MyMenuVisibilityListener();
632        }
633        mActivity.getGalleryActionBar().setDisplayOptions(mSetPathString != null, true);
634
635        mActionBar.addOnMenuVisibilityListener(mMenuVisibilityListener);
636        onUserInteraction();
637    }
638
639    private class MyDetailsSource implements DetailsSource {
640        private int mIndex;
641
642        @Override
643        public MediaDetails getDetails() {
644            return mModel.getCurrentMediaItem().getDetails();
645        }
646
647        @Override
648        public int size() {
649            return mMediaSet != null ? mMediaSet.getMediaItemCount() : 1;
650        }
651
652        @Override
653        public int findIndex(int indexHint) {
654            mIndex = indexHint;
655            return indexHint;
656        }
657
658        @Override
659        public int getIndex() {
660            return mIndex;
661        }
662    }
663}
664