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.ui.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                | FLAG_ALLOW_LOCK_WHILE_SCREEN_ON | FLAG_SHOW_WHEN_LOCKED);
117        if (data.getBoolean(KEY_DREAM)) {
118            // Dream screensaver only keeps screen on for plugged devices.
119            mFlags |= FLAG_SCREEN_ON_WHEN_PLUGGED;
120        } else {
121            // User-initiated slideshow would always keep screen on.
122            mFlags |= FLAG_SCREEN_ON_ALWAYS;
123        }
124
125        mHandler = new SynchronizedHandler(mActivity.getGLRoot()) {
126            @Override
127            public void handleMessage(Message message) {
128                switch (message.what) {
129                    case MSG_SHOW_PENDING_BITMAP:
130                        showPendingBitmap();
131                        break;
132                    case MSG_LOAD_NEXT_BITMAP:
133                        loadNextBitmap();
134                        break;
135                    default: throw new AssertionError();
136                }
137            }
138        };
139        initializeViews();
140        initializeData(data);
141    }
142
143    private void loadNextBitmap() {
144        mModel.nextSlide(new FutureListener<Slide>() {
145            @Override
146            public void onFutureDone(Future<Slide> future) {
147                mPendingSlide = future.get();
148                mHandler.sendEmptyMessage(MSG_SHOW_PENDING_BITMAP);
149            }
150        });
151    }
152
153    private void showPendingBitmap() {
154        // mPendingBitmap could be null, if
155        // 1.) there is no more items
156        // 2.) mModel is paused
157        Slide slide = mPendingSlide;
158        if (slide == null) {
159            if (mIsActive) {
160                mActivity.getStateManager().finishState(SlideshowPage.this);
161            }
162            return;
163        }
164
165        mSlideshowView.next(slide.bitmap, slide.item.getRotation());
166
167        setStateResult(Activity.RESULT_OK, mResultIntent
168                .putExtra(KEY_ITEM_PATH, slide.item.getPath().toString())
169                .putExtra(KEY_PHOTO_INDEX, slide.index));
170        mHandler.sendEmptyMessageDelayed(MSG_LOAD_NEXT_BITMAP, SLIDESHOW_DELAY);
171    }
172
173    @Override
174    public void onPause() {
175        super.onPause();
176        mIsActive = false;
177        mModel.pause();
178        mSlideshowView.release();
179
180        mHandler.removeMessages(MSG_LOAD_NEXT_BITMAP);
181        mHandler.removeMessages(MSG_SHOW_PENDING_BITMAP);
182    }
183
184    @Override
185    public void onResume() {
186        super.onResume();
187        mIsActive = true;
188        mModel.resume();
189
190        if (mPendingSlide != null) {
191            showPendingBitmap();
192        } else {
193            loadNextBitmap();
194        }
195    }
196
197    private void initializeData(Bundle data) {
198        boolean random = data.getBoolean(KEY_RANDOM_ORDER, false);
199
200        // We only want to show slideshow for images only, not videos.
201        String mediaPath = data.getString(KEY_SET_PATH);
202        mediaPath = FilterUtils.newFilterPath(mediaPath, FilterUtils.FILTER_IMAGE_ONLY);
203        MediaSet mediaSet = mActivity.getDataManager().getMediaSet(mediaPath);
204
205        if (random) {
206            boolean repeat = data.getBoolean(KEY_REPEAT);
207            mModel = new SlideshowDataAdapter(mActivity,
208                    new ShuffleSource(mediaSet, repeat), 0, null);
209            setStateResult(Activity.RESULT_OK, mResultIntent.putExtra(KEY_PHOTO_INDEX, 0));
210        } else {
211            int index = data.getInt(KEY_PHOTO_INDEX);
212            String itemPath = data.getString(KEY_ITEM_PATH);
213            Path path = itemPath != null ? Path.fromString(itemPath) : null;
214            boolean repeat = data.getBoolean(KEY_REPEAT);
215            mModel = new SlideshowDataAdapter(mActivity, new SequentialSource(mediaSet, repeat),
216                    index, path);
217            setStateResult(Activity.RESULT_OK, mResultIntent.putExtra(KEY_PHOTO_INDEX, index));
218        }
219    }
220
221    private void initializeViews() {
222        mSlideshowView = new SlideshowView();
223        mRootPane.addComponent(mSlideshowView);
224        setContentPane(mRootPane);
225    }
226
227    private static MediaItem findMediaItem(MediaSet mediaSet, int index) {
228        for (int i = 0, n = mediaSet.getSubMediaSetCount(); i < n; ++i) {
229            MediaSet subset = mediaSet.getSubMediaSet(i);
230            int count = subset.getTotalMediaItemCount();
231            if (index < count) {
232                return findMediaItem(subset, index);
233            }
234            index -= count;
235        }
236        ArrayList<MediaItem> list = mediaSet.getMediaItem(index, 1);
237        return list.isEmpty() ? null : list.get(0);
238    }
239
240    private static class ShuffleSource implements SlideshowDataAdapter.SlideshowSource {
241        private static final int RETRY_COUNT = 5;
242        private final MediaSet mMediaSet;
243        private final Random mRandom = new Random();
244        private int mOrder[] = new int[0];
245        private final boolean mRepeat;
246        private long mSourceVersion = MediaSet.INVALID_DATA_VERSION;
247        private int mLastIndex = -1;
248
249        public ShuffleSource(MediaSet mediaSet, boolean repeat) {
250            mMediaSet = Utils.checkNotNull(mediaSet);
251            mRepeat = repeat;
252        }
253
254        @Override
255        public int findItemIndex(Path path, int hint) {
256            return hint;
257        }
258
259        @Override
260        public MediaItem getMediaItem(int index) {
261            if (!mRepeat && index >= mOrder.length) return null;
262            if (mOrder.length == 0) return null;
263            mLastIndex = mOrder[index % mOrder.length];
264            MediaItem item = findMediaItem(mMediaSet, mLastIndex);
265            for (int i = 0; i < RETRY_COUNT && item == null; ++i) {
266                Log.w(TAG, "fail to find image: " + mLastIndex);
267                mLastIndex = mRandom.nextInt(mOrder.length);
268                item = findMediaItem(mMediaSet, mLastIndex);
269            }
270            return item;
271        }
272
273        @Override
274        public long reload() {
275            long version = mMediaSet.reload();
276            if (version != mSourceVersion) {
277                mSourceVersion = version;
278                int count = mMediaSet.getTotalMediaItemCount();
279                if (count != mOrder.length) generateOrderArray(count);
280            }
281            return version;
282        }
283
284        private void generateOrderArray(int totalCount) {
285            if (mOrder.length != totalCount) {
286                mOrder = new int[totalCount];
287                for (int i = 0; i < totalCount; ++i) {
288                    mOrder[i] = i;
289                }
290            }
291            for (int i = totalCount - 1; i > 0; --i) {
292                Utils.swap(mOrder, i, mRandom.nextInt(i + 1));
293            }
294            if (mOrder[0] == mLastIndex && totalCount > 1) {
295                Utils.swap(mOrder, 0, mRandom.nextInt(totalCount - 1) + 1);
296            }
297        }
298
299        @Override
300        public void addContentListener(ContentListener listener) {
301            mMediaSet.addContentListener(listener);
302        }
303
304        @Override
305        public void removeContentListener(ContentListener listener) {
306            mMediaSet.removeContentListener(listener);
307        }
308    }
309
310    private static class SequentialSource implements SlideshowDataAdapter.SlideshowSource {
311        private static final int DATA_SIZE = 32;
312
313        private ArrayList<MediaItem> mData = new ArrayList<MediaItem>();
314        private int mDataStart = 0;
315        private long mDataVersion = MediaObject.INVALID_DATA_VERSION;
316        private final MediaSet mMediaSet;
317        private final boolean mRepeat;
318
319        public SequentialSource(MediaSet mediaSet, boolean repeat) {
320            mMediaSet = mediaSet;
321            mRepeat = repeat;
322        }
323
324        @Override
325        public int findItemIndex(Path path, int hint) {
326            return mMediaSet.getIndexOfItem(path, hint);
327        }
328
329        @Override
330        public MediaItem getMediaItem(int index) {
331            int dataEnd = mDataStart + mData.size();
332
333            if (mRepeat) {
334                int count = mMediaSet.getMediaItemCount();
335                if (count == 0) return null;
336                index = index % count;
337            }
338            if (index < mDataStart || index >= dataEnd) {
339                mData = mMediaSet.getMediaItem(index, DATA_SIZE);
340                mDataStart = index;
341                dataEnd = index + mData.size();
342            }
343
344            return (index < mDataStart || index >= dataEnd) ? null : mData.get(index - mDataStart);
345        }
346
347        @Override
348        public long reload() {
349            long version = mMediaSet.reload();
350            if (version != mDataVersion) {
351                mDataVersion = version;
352                mData.clear();
353            }
354            return mDataVersion;
355        }
356
357        @Override
358        public void addContentListener(ContentListener listener) {
359            mMediaSet.addContentListener(listener);
360        }
361
362        @Override
363        public void removeContentListener(ContentListener listener) {
364            mMediaSet.removeContentListener(listener);
365        }
366    }
367}
368