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.Context;
24import android.content.Intent;
25import android.net.Uri;
26import android.os.Bundle;
27import android.os.Handler;
28import android.os.Message;
29import android.view.Menu;
30import android.view.MenuInflater;
31import android.view.MenuItem;
32import android.view.View;
33import android.view.View.MeasureSpec;
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.picasasource.PicasaSource;
47import com.android.gallery3d.ui.DetailsHelper;
48import com.android.gallery3d.ui.DetailsHelper.CloseListener;
49import com.android.gallery3d.ui.DetailsHelper.DetailsSource;
50import com.android.gallery3d.ui.FilmStripView;
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.PositionRepository;
57import com.android.gallery3d.ui.PositionRepository.Position;
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, 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 final 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 final 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 (mMenu == null) return;
304        MenuItem item = mMenu.findItem(R.id.action_slideshow);
305        if (item != null) {
306            item.setVisible(canDoSlideShow());
307        }
308        if (mCurrentPhoto == null) return;
309        int supportedOperations = mCurrentPhoto.getSupportedOperations();
310        if (!GalleryUtils.isEditorAvailable((Context) mActivity, "image/*")) {
311            supportedOperations &= ~MediaObject.SUPPORT_EDIT;
312        }
313
314        MenuExecutor.updateMenuOperation(mMenu, supportedOperations);
315    }
316
317    private boolean canDoSlideShow() {
318        if (mMediaSet == null || mCurrentPhoto == null) {
319            return false;
320        }
321        if (mCurrentPhoto.getMediaType() != MediaObject.MEDIA_TYPE_IMAGE) {
322            return false;
323        }
324        if (mMediaSet instanceof MtpDevice) {
325            return false;
326        }
327        return true;
328    }
329
330    private void showBars() {
331        if (mShowBars) return;
332        mShowBars = true;
333        mActionBar.show();
334        WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes();
335        params.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE;
336        ((Activity) mActivity).getWindow().setAttributes(params);
337        if (mFilmStripView != null) {
338            mFilmStripView.show();
339        }
340    }
341
342    private void hideBars() {
343        if (!mShowBars) return;
344        mShowBars = false;
345        mActionBar.hide();
346        WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes();
347        params.systemUiVisibility = View. SYSTEM_UI_FLAG_LOW_PROFILE;
348        ((Activity) mActivity).getWindow().setAttributes(params);
349        if (mFilmStripView != null) {
350            mFilmStripView.hide();
351        }
352    }
353
354    private void refreshHidingMessage() {
355        mHandler.removeMessages(MSG_HIDE_BARS);
356        if (!mIsMenuVisible && !mIsInteracting) {
357            mHandler.sendEmptyMessageDelayed(MSG_HIDE_BARS, HIDE_BARS_TIMEOUT);
358        }
359    }
360
361    @Override
362    public void onUserInteraction() {
363        showBars();
364        refreshHidingMessage();
365    }
366
367    public void onUserInteractionTap() {
368        if (mShowBars) {
369            hideBars();
370            mHandler.removeMessages(MSG_HIDE_BARS);
371        } else {
372            showBars();
373            refreshHidingMessage();
374        }
375    }
376
377    @Override
378    public void onUserInteractionBegin() {
379        showBars();
380        mIsInteracting = true;
381        refreshHidingMessage();
382    }
383
384    @Override
385    public void onUserInteractionEnd() {
386        mIsInteracting = false;
387
388        // This function could be called from GL thread (in SlotView.render)
389        // and post to the main thread. So, it could be executed while the
390        // activity is paused.
391        if (mIsActive) refreshHidingMessage();
392    }
393
394    @Override
395    protected void onBackPressed() {
396        if (mShowDetails) {
397            hideDetails();
398        } else {
399            PositionRepository repository = PositionRepository.getInstance(mActivity);
400            repository.clear();
401            if (mCurrentPhoto != null) {
402                Position position = new Position();
403                position.x = mRootPane.getWidth() / 2;
404                position.y = mRootPane.getHeight() / 2;
405                position.z = -1000;
406                repository.putPosition(
407                        Long.valueOf(System.identityHashCode(mCurrentPhoto.getPath())),
408                        position);
409            }
410            super.onBackPressed();
411        }
412    }
413
414    @Override
415    protected boolean onCreateActionBar(Menu menu) {
416        MenuInflater inflater = ((Activity) mActivity).getMenuInflater();
417        inflater.inflate(R.menu.photo, menu);
418        mShareActionProvider = GalleryActionBar.initializeShareActionProvider(menu);
419        if (mPendingSharePath != null) updateShareURI(mPendingSharePath);
420        mMenu = menu;
421        mShowBars = true;
422        updateMenuOperations();
423        return true;
424    }
425
426    @Override
427    protected boolean onItemSelected(MenuItem item) {
428        MediaItem current = mModel.getCurrentMediaItem();
429
430        if (current == null) {
431            // item is not ready, ignore
432            return true;
433        }
434
435        int currentIndex = mModel.getCurrentIndex();
436        Path path = current.getPath();
437
438        DataManager manager = mActivity.getDataManager();
439        int action = item.getItemId();
440        switch (action) {
441            case R.id.action_slideshow: {
442                Bundle data = new Bundle();
443                data.putString(SlideshowPage.KEY_SET_PATH, mMediaSet.getPath().toString());
444                data.putString(SlideshowPage.KEY_ITEM_PATH, path.toString());
445                data.putInt(SlideshowPage.KEY_PHOTO_INDEX, currentIndex);
446                data.putBoolean(SlideshowPage.KEY_REPEAT, true);
447                mActivity.getStateManager().startStateForResult(
448                        SlideshowPage.class, REQUEST_SLIDESHOW, data);
449                return true;
450            }
451            case R.id.action_crop: {
452                Activity activity = (Activity) mActivity;
453                Intent intent = new Intent(CropImage.CROP_ACTION);
454                intent.setClass(activity, CropImage.class);
455                intent.setData(manager.getContentUri(path));
456                activity.startActivityForResult(intent, PicasaSource.isPicasaImage(current)
457                        ? REQUEST_CROP_PICASA
458                        : REQUEST_CROP);
459                return true;
460            }
461            case R.id.action_details: {
462                if (mShowDetails) {
463                    hideDetails();
464                } else {
465                    showDetails(currentIndex);
466                }
467                return true;
468            }
469            case R.id.action_setas:
470            case R.id.action_confirm_delete:
471            case R.id.action_rotate_ccw:
472            case R.id.action_rotate_cw:
473            case R.id.action_show_on_map:
474            case R.id.action_edit:
475                mSelectionManager.deSelectAll();
476                mSelectionManager.toggle(path);
477                mMenuExecutor.onMenuClicked(item, null);
478                return true;
479            case R.id.action_import:
480                mSelectionManager.deSelectAll();
481                mSelectionManager.toggle(path);
482                mMenuExecutor.onMenuClicked(item,
483                        new ImportCompleteListener(mActivity));
484                return true;
485            default :
486                return false;
487        }
488    }
489
490    private void hideDetails() {
491        mShowDetails = false;
492        mDetailsHelper.hide();
493    }
494
495    private void showDetails(int index) {
496        mShowDetails = true;
497        if (mDetailsHelper == null) {
498            mDetailsHelper = new DetailsHelper(mActivity, mRootPane, new MyDetailsSource());
499            mDetailsHelper.setCloseListener(new CloseListener() {
500                public void onClose() {
501                    hideDetails();
502                }
503            });
504        }
505        mDetailsHelper.reloadDetails(index);
506        mDetailsHelper.show();
507    }
508
509    public void onSingleTapUp(int x, int y) {
510        MediaItem item = mModel.getCurrentMediaItem();
511        if (item == null) {
512            // item is not ready, ignore
513            return;
514        }
515
516        boolean playVideo =
517                (item.getSupportedOperations() & MediaItem.SUPPORT_PLAY) != 0;
518
519        if (playVideo) {
520            // determine if the point is at center (1/6) of the photo view.
521            // (The position of the "play" icon is at center (1/6) of the photo)
522            int w = mPhotoView.getWidth();
523            int h = mPhotoView.getHeight();
524            playVideo = (Math.abs(x - w / 2) * 12 <= w)
525                && (Math.abs(y - h / 2) * 12 <= h);
526        }
527
528        if (playVideo) {
529            playVideo((Activity) mActivity, item.getPlayUri(), item.getName());
530        } else {
531            onUserInteractionTap();
532        }
533    }
534
535    public static void playVideo(Activity activity, Uri uri, String title) {
536        try {
537            Intent intent = new Intent(Intent.ACTION_VIEW)
538                    .setDataAndType(uri, "video/*");
539            intent.putExtra(Intent.EXTRA_TITLE, title);
540            activity.startActivity(intent);
541        } catch (ActivityNotFoundException e) {
542            Toast.makeText(activity, activity.getString(R.string.video_err),
543                    Toast.LENGTH_SHORT).show();
544        }
545    }
546
547    // Called by FileStripView.
548    // Returns false if it cannot jump to the specified index at this time.
549    public boolean onSlotSelected(int slotIndex) {
550        return mPhotoView.jumpTo(slotIndex);
551    }
552
553    @Override
554    protected void onStateResult(int requestCode, int resultCode, Intent data) {
555        switch (requestCode) {
556            case REQUEST_CROP:
557                if (resultCode == Activity.RESULT_OK) {
558                    if (data == null) break;
559                    Path path = mApplication
560                            .getDataManager().findPathByUri(data.getData());
561                    if (path != null) {
562                        mModel.setCurrentPhoto(path, mCurrentIndex);
563                    }
564                }
565                break;
566            case REQUEST_CROP_PICASA: {
567                int message = resultCode == Activity.RESULT_OK
568                        ? R.string.crop_saved
569                        : R.string.crop_not_saved;
570                Toast.makeText(mActivity.getAndroidContext(),
571                        message, Toast.LENGTH_SHORT).show();
572                break;
573            }
574            case REQUEST_SLIDESHOW: {
575                if (data == null) break;
576                String path = data.getStringExtra(SlideshowPage.KEY_ITEM_PATH);
577                int index = data.getIntExtra(SlideshowPage.KEY_PHOTO_INDEX, 0);
578                if (path != null) {
579                    mModel.setCurrentPhoto(Path.fromString(path), index);
580                }
581            }
582        }
583    }
584
585    @Override
586    public void onPause() {
587        super.onPause();
588        mIsActive = false;
589        if (mFilmStripView != null) {
590            mFilmStripView.pause();
591        }
592        DetailsHelper.pause();
593        mPhotoView.pause();
594        mModel.pause();
595        mHandler.removeMessages(MSG_HIDE_BARS);
596        mActionBar.removeOnMenuVisibilityListener(mMenuVisibilityListener);
597        mMenuExecutor.pause();
598    }
599
600    @Override
601    protected void onResume() {
602        super.onResume();
603        mIsActive = true;
604        setContentPane(mRootPane);
605        mModel.resume();
606        mPhotoView.resume();
607        if (mFilmStripView != null) {
608            mFilmStripView.resume();
609        }
610        if (mMenuVisibilityListener == null) {
611            mMenuVisibilityListener = new MyMenuVisibilityListener();
612        }
613        mActionBar.addOnMenuVisibilityListener(mMenuVisibilityListener);
614        onUserInteraction();
615    }
616
617    private class MyDetailsSource implements DetailsSource {
618        private int mIndex;
619
620        @Override
621        public MediaDetails getDetails() {
622            return mModel.getCurrentMediaItem().getDetails();
623        }
624
625        @Override
626        public int size() {
627            return mMediaSet != null ? mMediaSet.getMediaItemCount() : 1;
628        }
629
630        @Override
631        public int findIndex(int indexHint) {
632            mIndex = indexHint;
633            return indexHint;
634        }
635
636        @Override
637        public int getIndex() {
638            return mIndex;
639        }
640    }
641}
642