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.Activity;
20import android.content.Intent;
21import android.graphics.Bitmap;
22import android.os.Bundle;
23import android.os.Handler;
24import android.os.Message;
25import android.view.MotionEvent;
26
27import com.android.gallery3d.R;
28import com.android.gallery3d.common.Utils;
29import com.android.gallery3d.data.ContentListener;
30import com.android.gallery3d.data.MediaItem;
31import com.android.gallery3d.data.MediaObject;
32import com.android.gallery3d.data.MediaSet;
33import com.android.gallery3d.data.Path;
34import com.android.gallery3d.glrenderer.GLCanvas;
35import com.android.gallery3d.ui.GLView;
36import com.android.gallery3d.ui.SlideshowView;
37import com.android.gallery3d.ui.SynchronizedHandler;
38import com.android.gallery3d.util.Future;
39import com.android.gallery3d.util.FutureListener;
40
41import java.util.ArrayList;
42import java.util.Random;
43
44public class SlideshowPage extends ActivityState {
45    private static final String TAG = "SlideshowPage";
46
47    public static final String KEY_SET_PATH = "media-set-path";
48    public static final String KEY_ITEM_PATH = "media-item-path";
49    public static final String KEY_PHOTO_INDEX = "photo-index";
50    public static final String KEY_RANDOM_ORDER = "random-order";
51    public static final String KEY_REPEAT = "repeat";
52    public static final String KEY_DREAM = "dream";
53
54    private static final long SLIDESHOW_DELAY = 3000; // 3 seconds
55
56    private static final int MSG_LOAD_NEXT_BITMAP = 1;
57    private static final int MSG_SHOW_PENDING_BITMAP = 2;
58
59    public static interface Model {
60        public void pause();
61
62        public void resume();
63
64        public Future<Slide> nextSlide(FutureListener<Slide> listener);
65    }
66
67    public static class Slide {
68        public Bitmap bitmap;
69        public MediaItem item;
70        public int index;
71
72        public Slide(MediaItem item, int index, Bitmap bitmap) {
73            this.bitmap = bitmap;
74            this.item = item;
75            this.index = index;
76        }
77    }
78
79    private Handler mHandler;
80    private Model mModel;
81    private SlideshowView mSlideshowView;
82
83    private Slide mPendingSlide = null;
84    private boolean mIsActive = false;
85    private final Intent mResultIntent = new Intent();
86
87    @Override
88    protected int getBackgroundColorId() {
89        return R.color.slideshow_background;
90    }
91
92    private final GLView mRootPane = new GLView() {
93        @Override
94        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
95            mSlideshowView.layout(0, 0, right - left, bottom - top);
96        }
97
98        @Override
99        protected boolean onTouch(MotionEvent event) {
100            if (event.getAction() == MotionEvent.ACTION_UP) {
101                onBackPressed();
102            }
103            return true;
104        }
105
106        @Override
107        protected void renderBackground(GLCanvas canvas) {
108            canvas.clearBuffer(getBackgroundColor());
109        }
110    };
111
112    @Override
113    public void onCreate(Bundle data, Bundle restoreState) {
114        super.onCreate(data, restoreState);
115        mFlags |= (FLAG_HIDE_ACTION_BAR | FLAG_HIDE_STATUS_BAR);
116        if (data.getBoolean(KEY_DREAM)) {
117            // Dream screensaver only keeps screen on for plugged devices.
118            mFlags |= FLAG_SCREEN_ON_WHEN_PLUGGED | FLAG_SHOW_WHEN_LOCKED;
119        } else {
120            // User-initiated slideshow would always keep screen on.
121            mFlags |= FLAG_SCREEN_ON_ALWAYS;
122        }
123
124        mHandler = new SynchronizedHandler(mActivity.getGLRoot()) {
125            @Override
126            public void handleMessage(Message message) {
127                switch (message.what) {
128                    case MSG_SHOW_PENDING_BITMAP:
129                        showPendingBitmap();
130                        break;
131                    case MSG_LOAD_NEXT_BITMAP:
132                        loadNextBitmap();
133                        break;
134                    default: throw new AssertionError();
135                }
136            }
137        };
138        initializeViews();
139        initializeData(data);
140    }
141
142    private void loadNextBitmap() {
143        mModel.nextSlide(new FutureListener<Slide>() {
144            @Override
145            public void onFutureDone(Future<Slide> future) {
146                mPendingSlide = future.get();
147                mHandler.sendEmptyMessage(MSG_SHOW_PENDING_BITMAP);
148            }
149        });
150    }
151
152    private void showPendingBitmap() {
153        // mPendingBitmap could be null, if
154        // 1.) there is no more items
155        // 2.) mModel is paused
156        Slide slide = mPendingSlide;
157        if (slide == null) {
158            if (mIsActive) {
159                mActivity.getStateManager().finishState(SlideshowPage.this);
160            }
161            return;
162        }
163
164        mSlideshowView.next(slide.bitmap, slide.item.getRotation());
165
166        setStateResult(Activity.RESULT_OK, mResultIntent
167                .putExtra(KEY_ITEM_PATH, slide.item.getPath().toString())
168                .putExtra(KEY_PHOTO_INDEX, slide.index));
169        mHandler.sendEmptyMessageDelayed(MSG_LOAD_NEXT_BITMAP, SLIDESHOW_DELAY);
170    }
171
172    @Override
173    public void onPause() {
174        super.onPause();
175        mIsActive = false;
176        mModel.pause();
177        mSlideshowView.release();
178
179        mHandler.removeMessages(MSG_LOAD_NEXT_BITMAP);
180        mHandler.removeMessages(MSG_SHOW_PENDING_BITMAP);
181    }
182
183    @Override
184    public void onResume() {
185        super.onResume();
186        mIsActive = true;
187        mModel.resume();
188
189        if (mPendingSlide != null) {
190            showPendingBitmap();
191        } else {
192            loadNextBitmap();
193        }
194    }
195
196    private void initializeData(Bundle data) {
197        boolean random = data.getBoolean(KEY_RANDOM_ORDER, false);
198
199        // We only want to show slideshow for images only, not videos.
200        String mediaPath = data.getString(KEY_SET_PATH);
201        mediaPath = FilterUtils.newFilterPath(mediaPath, FilterUtils.FILTER_IMAGE_ONLY);
202        MediaSet mediaSet = mActivity.getDataManager().getMediaSet(mediaPath);
203
204        if (random) {
205            boolean repeat = data.getBoolean(KEY_REPEAT);
206            mModel = new SlideshowDataAdapter(mActivity,
207                    new ShuffleSource(mediaSet, repeat), 0, null);
208            setStateResult(Activity.RESULT_OK, mResultIntent.putExtra(KEY_PHOTO_INDEX, 0));
209        } else {
210            int index = data.getInt(KEY_PHOTO_INDEX);
211            String itemPath = data.getString(KEY_ITEM_PATH);
212            Path path = itemPath != null ? Path.fromString(itemPath) : null;
213            boolean repeat = data.getBoolean(KEY_REPEAT);
214            mModel = new SlideshowDataAdapter(mActivity, new SequentialSource(mediaSet, repeat),
215                    index, path);
216            setStateResult(Activity.RESULT_OK, mResultIntent.putExtra(KEY_PHOTO_INDEX, index));
217        }
218    }
219
220    private void initializeViews() {
221        mSlideshowView = new SlideshowView();
222        mRootPane.addComponent(mSlideshowView);
223        setContentPane(mRootPane);
224    }
225
226    private static MediaItem findMediaItem(MediaSet mediaSet, int index) {
227        for (int i = 0, n = mediaSet.getSubMediaSetCount(); i < n; ++i) {
228            MediaSet subset = mediaSet.getSubMediaSet(i);
229            int count = subset.getTotalMediaItemCount();
230            if (index < count) {
231                return findMediaItem(subset, index);
232            }
233            index -= count;
234        }
235        ArrayList<MediaItem> list = mediaSet.getMediaItem(index, 1);
236        return list.isEmpty() ? null : list.get(0);
237    }
238
239    private static class ShuffleSource implements SlideshowDataAdapter.SlideshowSource {
240        private static final int RETRY_COUNT = 5;
241        private final MediaSet mMediaSet;
242        private final Random mRandom = new Random();
243        private int mOrder[] = new int[0];
244        private final boolean mRepeat;
245        private long mSourceVersion = MediaSet.INVALID_DATA_VERSION;
246        private int mLastIndex = -1;
247
248        public ShuffleSource(MediaSet mediaSet, boolean repeat) {
249            mMediaSet = Utils.checkNotNull(mediaSet);
250            mRepeat = repeat;
251        }
252
253        @Override
254        public int findItemIndex(Path path, int hint) {
255            return hint;
256        }
257
258        @Override
259        public MediaItem getMediaItem(int index) {
260            if (!mRepeat && index >= mOrder.length) return null;
261            if (mOrder.length == 0) return null;
262            mLastIndex = mOrder[index % mOrder.length];
263            MediaItem item = findMediaItem(mMediaSet, mLastIndex);
264            for (int i = 0; i < RETRY_COUNT && item == null; ++i) {
265                Log.w(TAG, "fail to find image: " + mLastIndex);
266                mLastIndex = mRandom.nextInt(mOrder.length);
267                item = findMediaItem(mMediaSet, mLastIndex);
268            }
269            return item;
270        }
271
272        @Override
273        public long reload() {
274            long version = mMediaSet.reload();
275            if (version != mSourceVersion) {
276                mSourceVersion = version;
277                int count = mMediaSet.getTotalMediaItemCount();
278                if (count != mOrder.length) generateOrderArray(count);
279            }
280            return version;
281        }
282
283        private void generateOrderArray(int totalCount) {
284            if (mOrder.length != totalCount) {
285                mOrder = new int[totalCount];
286                for (int i = 0; i < totalCount; ++i) {
287                    mOrder[i] = i;
288                }
289            }
290            for (int i = totalCount - 1; i > 0; --i) {
291                Utils.swap(mOrder, i, mRandom.nextInt(i + 1));
292            }
293            if (mOrder[0] == mLastIndex && totalCount > 1) {
294                Utils.swap(mOrder, 0, mRandom.nextInt(totalCount - 1) + 1);
295            }
296        }
297
298        @Override
299        public void addContentListener(ContentListener listener) {
300            mMediaSet.addContentListener(listener);
301        }
302
303        @Override
304        public void removeContentListener(ContentListener listener) {
305            mMediaSet.removeContentListener(listener);
306        }
307    }
308
309    private static class SequentialSource implements SlideshowDataAdapter.SlideshowSource {
310        private static final int DATA_SIZE = 32;
311
312        private ArrayList<MediaItem> mData = new ArrayList<MediaItem>();
313        private int mDataStart = 0;
314        private long mDataVersion = MediaObject.INVALID_DATA_VERSION;
315        private final MediaSet mMediaSet;
316        private final boolean mRepeat;
317
318        public SequentialSource(MediaSet mediaSet, boolean repeat) {
319            mMediaSet = mediaSet;
320            mRepeat = repeat;
321        }
322
323        @Override
324        public int findItemIndex(Path path, int hint) {
325            return mMediaSet.getIndexOfItem(path, hint);
326        }
327
328        @Override
329        public MediaItem getMediaItem(int index) {
330            int dataEnd = mDataStart + mData.size();
331
332            if (mRepeat) {
333                int count = mMediaSet.getMediaItemCount();
334                if (count == 0) return null;
335                index = index % count;
336            }
337            if (index < mDataStart || index >= dataEnd) {
338                mData = mMediaSet.getMediaItem(index, DATA_SIZE);
339                mDataStart = index;
340                dataEnd = index + mData.size();
341            }
342
343            return (index < mDataStart || index >= dataEnd) ? null : mData.get(index - mDataStart);
344        }
345
346        @Override
347        public long reload() {
348            long version = mMediaSet.reload();
349            if (version != mDataVersion) {
350                mDataVersion = version;
351                mData.clear();
352            }
353            return mDataVersion;
354        }
355
356        @Override
357        public void addContentListener(ContentListener listener) {
358            mMediaSet.addContentListener(listener);
359        }
360
361        @Override
362        public void removeContentListener(ContentListener listener) {
363            mMediaSet.removeContentListener(listener);
364        }
365    }
366}
367