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