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