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