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.ui;
18
19import android.graphics.Bitmap;
20import android.os.Message;
21
22import com.android.gallery3d.app.AbstractGalleryActivity;
23import com.android.gallery3d.app.AlbumDataLoader;
24import com.android.gallery3d.common.Utils;
25import com.android.gallery3d.data.MediaItem;
26import com.android.gallery3d.data.MediaObject;
27import com.android.gallery3d.data.MediaObject.PanoramaSupportCallback;
28import com.android.gallery3d.data.Path;
29import com.android.gallery3d.glrenderer.Texture;
30import com.android.gallery3d.glrenderer.TiledTexture;
31import com.android.gallery3d.util.Future;
32import com.android.gallery3d.util.FutureListener;
33import com.android.gallery3d.util.JobLimiter;
34
35public class AlbumSlidingWindow implements AlbumDataLoader.DataListener {
36    @SuppressWarnings("unused")
37    private static final String TAG = "AlbumSlidingWindow";
38
39    private static final int MSG_UPDATE_ENTRY = 0;
40    private static final int JOB_LIMIT = 2;
41
42    public static interface Listener {
43        public void onSizeChanged(int size);
44        public void onContentChanged();
45    }
46
47    public static class AlbumEntry {
48        public MediaItem item;
49        public Path path;
50        public boolean isPanorama;
51        public int rotation;
52        public int mediaType;
53        public boolean isWaitDisplayed;
54        public TiledTexture bitmapTexture;
55        public Texture content;
56        private BitmapLoader contentLoader;
57        private PanoSupportListener mPanoSupportListener;
58    }
59
60    private final AlbumDataLoader mSource;
61    private final AlbumEntry mData[];
62    private final SynchronizedHandler mHandler;
63    private final JobLimiter mThreadPool;
64    private final TiledTexture.Uploader mTileUploader;
65
66    private int mSize;
67
68    private int mContentStart = 0;
69    private int mContentEnd = 0;
70
71    private int mActiveStart = 0;
72    private int mActiveEnd = 0;
73
74    private Listener mListener;
75
76    private int mActiveRequestCount = 0;
77    private boolean mIsActive = false;
78
79    private class PanoSupportListener implements PanoramaSupportCallback {
80        public final AlbumEntry mEntry;
81        public PanoSupportListener (AlbumEntry entry) {
82            mEntry = entry;
83        }
84        @Override
85        public void panoramaInfoAvailable(MediaObject mediaObject, boolean isPanorama,
86                boolean isPanorama360) {
87            if (mEntry != null) mEntry.isPanorama = isPanorama;
88        }
89    }
90
91    public AlbumSlidingWindow(AbstractGalleryActivity activity,
92            AlbumDataLoader source, int cacheSize) {
93        source.setDataListener(this);
94        mSource = source;
95        mData = new AlbumEntry[cacheSize];
96        mSize = source.size();
97
98        mHandler = new SynchronizedHandler(activity.getGLRoot()) {
99            @Override
100            public void handleMessage(Message message) {
101                Utils.assertTrue(message.what == MSG_UPDATE_ENTRY);
102                ((ThumbnailLoader) message.obj).updateEntry();
103            }
104        };
105
106        mThreadPool = new JobLimiter(activity.getThreadPool(), JOB_LIMIT);
107        mTileUploader = new TiledTexture.Uploader(activity.getGLRoot());
108    }
109
110    public void setListener(Listener listener) {
111        mListener = listener;
112    }
113
114    public AlbumEntry get(int slotIndex) {
115        if (!isActiveSlot(slotIndex)) {
116            Utils.fail("invalid slot: %s outsides (%s, %s)",
117                    slotIndex, mActiveStart, mActiveEnd);
118        }
119        return mData[slotIndex % mData.length];
120    }
121
122    public boolean isActiveSlot(int slotIndex) {
123        return slotIndex >= mActiveStart && slotIndex < mActiveEnd;
124    }
125
126    private void setContentWindow(int contentStart, int contentEnd) {
127        if (contentStart == mContentStart && contentEnd == mContentEnd) return;
128
129        if (!mIsActive) {
130            mContentStart = contentStart;
131            mContentEnd = contentEnd;
132            mSource.setActiveWindow(contentStart, contentEnd);
133            return;
134        }
135
136        if (contentStart >= mContentEnd || mContentStart >= contentEnd) {
137            for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
138                freeSlotContent(i);
139            }
140            mSource.setActiveWindow(contentStart, contentEnd);
141            for (int i = contentStart; i < contentEnd; ++i) {
142                prepareSlotContent(i);
143            }
144        } else {
145            for (int i = mContentStart; i < contentStart; ++i) {
146                freeSlotContent(i);
147            }
148            for (int i = contentEnd, n = mContentEnd; i < n; ++i) {
149                freeSlotContent(i);
150            }
151            mSource.setActiveWindow(contentStart, contentEnd);
152            for (int i = contentStart, n = mContentStart; i < n; ++i) {
153                prepareSlotContent(i);
154            }
155            for (int i = mContentEnd; i < contentEnd; ++i) {
156                prepareSlotContent(i);
157            }
158        }
159
160        mContentStart = contentStart;
161        mContentEnd = contentEnd;
162    }
163
164    public void setActiveWindow(int start, int end) {
165        if (!(start <= end && end - start <= mData.length && end <= mSize)) {
166            Utils.fail("%s, %s, %s, %s", start, end, mData.length, mSize);
167        }
168        AlbumEntry data[] = mData;
169
170        mActiveStart = start;
171        mActiveEnd = end;
172
173        int contentStart = Utils.clamp((start + end) / 2 - data.length / 2,
174                0, Math.max(0, mSize - data.length));
175        int contentEnd = Math.min(contentStart + data.length, mSize);
176        setContentWindow(contentStart, contentEnd);
177        updateTextureUploadQueue();
178        if (mIsActive) updateAllImageRequests();
179    }
180
181    private void uploadBgTextureInSlot(int index) {
182        if (index < mContentEnd && index >= mContentStart) {
183            AlbumEntry entry = mData[index % mData.length];
184            if (entry.bitmapTexture != null) {
185                mTileUploader.addTexture(entry.bitmapTexture);
186            }
187        }
188    }
189
190    private void updateTextureUploadQueue() {
191        if (!mIsActive) return;
192        mTileUploader.clear();
193
194        // add foreground textures
195        for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) {
196            AlbumEntry entry = mData[i % mData.length];
197            if (entry.bitmapTexture != null) {
198                mTileUploader.addTexture(entry.bitmapTexture);
199            }
200        }
201
202        // add background textures
203        int range = Math.max(
204                (mContentEnd - mActiveEnd), (mActiveStart - mContentStart));
205        for (int i = 0; i < range; ++i) {
206            uploadBgTextureInSlot(mActiveEnd + i);
207            uploadBgTextureInSlot(mActiveStart - i - 1);
208        }
209    }
210
211    // We would like to request non active slots in the following order:
212    // Order:    8 6 4 2                   1 3 5 7
213    //         |---------|---------------|---------|
214    //                   |<-  active  ->|
215    //         |<-------- cached range ----------->|
216    private void requestNonactiveImages() {
217        int range = Math.max(
218                (mContentEnd - mActiveEnd), (mActiveStart - mContentStart));
219        for (int i = 0 ;i < range; ++i) {
220            requestSlotImage(mActiveEnd + i);
221            requestSlotImage(mActiveStart - 1 - i);
222        }
223    }
224
225    // return whether the request is in progress or not
226    private boolean requestSlotImage(int slotIndex) {
227        if (slotIndex < mContentStart || slotIndex >= mContentEnd) return false;
228        AlbumEntry entry = mData[slotIndex % mData.length];
229        if (entry.content != null || entry.item == null) return false;
230
231        // Set up the panorama callback
232        entry.mPanoSupportListener = new PanoSupportListener(entry);
233        entry.item.getPanoramaSupport(entry.mPanoSupportListener);
234
235        entry.contentLoader.startLoad();
236        return entry.contentLoader.isRequestInProgress();
237    }
238
239    private void cancelNonactiveImages() {
240        int range = Math.max(
241                (mContentEnd - mActiveEnd), (mActiveStart - mContentStart));
242        for (int i = 0 ;i < range; ++i) {
243            cancelSlotImage(mActiveEnd + i);
244            cancelSlotImage(mActiveStart - 1 - i);
245        }
246    }
247
248    private void cancelSlotImage(int slotIndex) {
249        if (slotIndex < mContentStart || slotIndex >= mContentEnd) return;
250        AlbumEntry item = mData[slotIndex % mData.length];
251        if (item.contentLoader != null) item.contentLoader.cancelLoad();
252    }
253
254    private void freeSlotContent(int slotIndex) {
255        AlbumEntry data[] = mData;
256        int index = slotIndex % data.length;
257        AlbumEntry entry = data[index];
258        if (entry.contentLoader != null) entry.contentLoader.recycle();
259        if (entry.bitmapTexture != null) entry.bitmapTexture.recycle();
260        data[index] = null;
261    }
262
263    private void prepareSlotContent(int slotIndex) {
264        AlbumEntry entry = new AlbumEntry();
265        MediaItem item = mSource.get(slotIndex); // item could be null;
266        entry.item = item;
267        entry.mediaType = (item == null)
268                ? MediaItem.MEDIA_TYPE_UNKNOWN
269                : entry.item.getMediaType();
270        entry.path = (item == null) ? null : item.getPath();
271        entry.rotation = (item == null) ? 0 : item.getRotation();
272        entry.contentLoader = new ThumbnailLoader(slotIndex, entry.item);
273        mData[slotIndex % mData.length] = entry;
274    }
275
276    private void updateAllImageRequests() {
277        mActiveRequestCount = 0;
278        for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) {
279            if (requestSlotImage(i)) ++mActiveRequestCount;
280        }
281        if (mActiveRequestCount == 0) {
282            requestNonactiveImages();
283        } else {
284            cancelNonactiveImages();
285        }
286    }
287
288    private class ThumbnailLoader extends BitmapLoader  {
289        private final int mSlotIndex;
290        private final MediaItem mItem;
291
292        public ThumbnailLoader(int slotIndex, MediaItem item) {
293            mSlotIndex = slotIndex;
294            mItem = item;
295        }
296
297        @Override
298        protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l) {
299            return mThreadPool.submit(
300                    mItem.requestImage(MediaItem.TYPE_MICROTHUMBNAIL), this);
301        }
302
303        @Override
304        protected void onLoadComplete(Bitmap bitmap) {
305            mHandler.obtainMessage(MSG_UPDATE_ENTRY, this).sendToTarget();
306        }
307
308        public void updateEntry() {
309            Bitmap bitmap = getBitmap();
310            if (bitmap == null) return; // error or recycled
311            AlbumEntry entry = mData[mSlotIndex % mData.length];
312            entry.bitmapTexture = new TiledTexture(bitmap);
313            entry.content = entry.bitmapTexture;
314
315            if (isActiveSlot(mSlotIndex)) {
316                mTileUploader.addTexture(entry.bitmapTexture);
317                --mActiveRequestCount;
318                if (mActiveRequestCount == 0) requestNonactiveImages();
319                if (mListener != null) mListener.onContentChanged();
320            } else {
321                mTileUploader.addTexture(entry.bitmapTexture);
322            }
323        }
324    }
325
326    @Override
327    public void onSizeChanged(int size) {
328        if (mSize != size) {
329            mSize = size;
330            if (mListener != null) mListener.onSizeChanged(mSize);
331            if (mContentEnd > mSize) mContentEnd = mSize;
332            if (mActiveEnd > mSize) mActiveEnd = mSize;
333        }
334    }
335
336    @Override
337    public void onContentChanged(int index) {
338        if (index >= mContentStart && index < mContentEnd && mIsActive) {
339            freeSlotContent(index);
340            prepareSlotContent(index);
341            updateAllImageRequests();
342            if (mListener != null && isActiveSlot(index)) {
343                mListener.onContentChanged();
344            }
345        }
346    }
347
348    public void resume() {
349        mIsActive = true;
350        TiledTexture.prepareResources();
351        for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
352            prepareSlotContent(i);
353        }
354        updateAllImageRequests();
355    }
356
357    public void pause() {
358        mIsActive = false;
359        mTileUploader.clear();
360        TiledTexture.freeResources();
361        for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
362            freeSlotContent(i);
363        }
364    }
365}
366