AlbumSetSlidingWindow.java revision b8be1e0ad76b6abc0da7ead39f7a9811195d001e
1/*T
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.R;
23import com.android.gallery3d.app.AlbumSetDataLoader;
24import com.android.gallery3d.app.GalleryActivity;
25import com.android.gallery3d.common.Utils;
26import com.android.gallery3d.data.DataSourceType;
27import com.android.gallery3d.data.MediaItem;
28import com.android.gallery3d.data.MediaObject;
29import com.android.gallery3d.data.MediaSet;
30import com.android.gallery3d.data.Path;
31import com.android.gallery3d.util.Future;
32import com.android.gallery3d.util.FutureListener;
33import com.android.gallery3d.util.GalleryUtils;
34import com.android.gallery3d.util.ThreadPool;
35
36public class AlbumSetSlidingWindow implements AlbumSetDataLoader.DataListener {
37    private static final String TAG = "AlbumSetSlidingWindow";
38    private static final int MSG_UPDATE_ALBUM_ENTRY = 1;
39
40    public static interface Listener {
41        public void onSizeChanged(int size);
42        public void onContentChanged();
43    }
44
45    private final AlbumSetDataLoader mSource;
46    private int mSize;
47
48    private int mContentStart = 0;
49    private int mContentEnd = 0;
50
51    private int mActiveStart = 0;
52    private int mActiveEnd = 0;
53
54    private Listener mListener;
55
56    private final AlbumSetEntry mData[];
57    private final SynchronizedHandler mHandler;
58    private final ThreadPool mThreadPool;
59    private final AlbumLabelMaker mLabelMaker;
60    private final String mLoadingText;
61    private final TextureUploader mTextureUploader;
62
63    private int mActiveRequestCount = 0;
64    private boolean mIsActive = false;
65    private BitmapTexture mLoadingLabel;
66
67    private int mSlotWidth;
68
69    public static class AlbumSetEntry {
70        public MediaSet album;
71        public MediaItem coverItem;
72        public Texture content;
73        public Texture label;
74        public Path setPath;
75        public String title;
76        public int totalCount;
77        public int sourceType;
78        public int cacheFlag;
79        public int cacheStatus;
80        public int rotation;
81        public int mediaType;
82        public boolean isPanorama;
83        public boolean isWaitLoadingDisplayed;
84        public long setDataVersion;
85        public long coverDataVersion;
86        private BitmapLoader labelLoader;
87        private BitmapLoader coverLoader;
88    }
89
90    public AlbumSetSlidingWindow(GalleryActivity activity,
91            AlbumSetDataLoader source, AlbumSetSlotRenderer.LabelSpec labelSpec, int cacheSize) {
92        source.setModelListener(this);
93        mSource = source;
94        mData = new AlbumSetEntry[cacheSize];
95        mSize = source.size();
96        mThreadPool = activity.getThreadPool();
97
98        mLabelMaker = new AlbumLabelMaker(activity.getAndroidContext(), labelSpec);
99        mLoadingText = activity.getAndroidContext().getString(R.string.loading);
100        mTextureUploader = new TextureUploader(activity.getGLRoot());
101
102        mHandler = new SynchronizedHandler(activity.getGLRoot()) {
103            @Override
104            public void handleMessage(Message message) {
105                Utils.assertTrue(message.what == MSG_UPDATE_ALBUM_ENTRY);
106                ((EntryUpdater) message.obj).updateEntry();
107            }
108        };
109    }
110
111    public void setListener(Listener listener) {
112        mListener = listener;
113    }
114
115    public AlbumSetEntry get(int slotIndex) {
116        if (!isActiveSlot(slotIndex)) {
117            Utils.fail("invalid slot: %s outsides (%s, %s)",
118                    slotIndex, mActiveStart, mActiveEnd);
119        }
120        return mData[slotIndex % mData.length];
121    }
122
123    public int size() {
124        return mSize;
125    }
126
127    public boolean isActiveSlot(int slotIndex) {
128        return slotIndex >= mActiveStart && slotIndex < mActiveEnd;
129    }
130
131    private void setContentWindow(int contentStart, int contentEnd) {
132        if (contentStart == mContentStart && contentEnd == mContentEnd) return;
133
134        if (contentStart >= mContentEnd || mContentStart >= contentEnd) {
135            for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
136                freeSlotContent(i);
137            }
138            mSource.setActiveWindow(contentStart, contentEnd);
139            for (int i = contentStart; i < contentEnd; ++i) {
140                prepareSlotContent(i);
141            }
142        } else {
143            for (int i = mContentStart; i < contentStart; ++i) {
144                freeSlotContent(i);
145            }
146            for (int i = contentEnd, n = mContentEnd; i < n; ++i) {
147                freeSlotContent(i);
148            }
149            mSource.setActiveWindow(contentStart, contentEnd);
150            for (int i = contentStart, n = mContentStart; i < n; ++i) {
151                prepareSlotContent(i);
152            }
153            for (int i = mContentEnd; i < contentEnd; ++i) {
154                prepareSlotContent(i);
155            }
156        }
157
158        mContentStart = contentStart;
159        mContentEnd = contentEnd;
160    }
161
162    public void setActiveWindow(int start, int end) {
163        if (!(start <= end && end - start <= mData.length && end <= mSize)) {
164            Utils.fail("start = %s, end = %s, length = %s, size = %s",
165                    start, end, mData.length, mSize);
166        }
167
168        AlbumSetEntry data[] = mData;
169        mActiveStart = start;
170        mActiveEnd = end;
171        int contentStart = Utils.clamp((start + end) / 2 - data.length / 2,
172                0, Math.max(0, mSize - data.length));
173        int contentEnd = Math.min(contentStart + data.length, mSize);
174        setContentWindow(contentStart, contentEnd);
175
176        if (mIsActive) {
177            updateTextureUploadQueue();
178            updateAllImageRequests();
179        }
180    }
181
182    // We would like to request non active slots in the following order:
183    // Order:    8 6 4 2                   1 3 5 7
184    //         |---------|---------------|---------|
185    //                   |<-  active  ->|
186    //         |<-------- cached range ----------->|
187    private void requestNonactiveImages() {
188        int range = Math.max(
189                mContentEnd - mActiveEnd, mActiveStart - mContentStart);
190        for (int i = 0 ;i < range; ++i) {
191            requestImagesInSlot(mActiveEnd + i);
192            requestImagesInSlot(mActiveStart - 1 - i);
193        }
194    }
195
196    private void cancelNonactiveImages() {
197        int range = Math.max(
198                mContentEnd - mActiveEnd, mActiveStart - mContentStart);
199        for (int i = 0 ;i < range; ++i) {
200            cancelImagesInSlot(mActiveEnd + i);
201            cancelImagesInSlot(mActiveStart - 1 - i);
202        }
203    }
204
205    private void requestImagesInSlot(int slotIndex) {
206        if (slotIndex < mContentStart || slotIndex >= mContentEnd) return;
207        AlbumSetEntry entry = mData[slotIndex % mData.length];
208        if (entry.coverLoader != null) entry.coverLoader.startLoad();
209        if (entry.labelLoader != null) entry.labelLoader.startLoad();
210    }
211
212    private void cancelImagesInSlot(int slotIndex) {
213        if (slotIndex < mContentStart || slotIndex >= mContentEnd) return;
214        AlbumSetEntry entry = mData[slotIndex % mData.length];
215        if (entry.coverLoader != null) entry.coverLoader.cancelLoad();
216        if (entry.labelLoader != null) entry.labelLoader.cancelLoad();
217    }
218
219    private static long getDataVersion(MediaObject object) {
220        return object == null
221                ? MediaSet.INVALID_DATA_VERSION
222                : object.getDataVersion();
223    }
224
225    private void freeSlotContent(int slotIndex) {
226        AlbumSetEntry entry = mData[slotIndex % mData.length];
227        if (entry.coverLoader != null) entry.coverLoader.recycle();
228        if (entry.labelLoader != null) entry.labelLoader.recycle();
229        mData[slotIndex % mData.length] = null;
230    }
231
232    private boolean isLabelChanged(
233            AlbumSetEntry entry, String title, int totalCount, int sourceType) {
234        return !Utils.equals(entry.title, title)
235                || entry.totalCount != totalCount
236                || entry.sourceType != sourceType;
237    }
238
239    private void updateAlbumSetEntry(AlbumSetEntry entry, int slotIndex) {
240        MediaSet album = mSource.getMediaSet(slotIndex);
241        MediaItem cover = mSource.getCoverItem(slotIndex);
242        int totalCount = mSource.getTotalCount(slotIndex);
243
244        entry.album = album;
245        entry.setDataVersion = getDataVersion(album);
246        entry.cacheFlag = identifyCacheFlag(album);
247        entry.cacheStatus = identifyCacheStatus(album);
248        entry.setPath = (album == null) ? null : album.getPath();
249
250        String title = (album == null) ? "" : Utils.ensureNotNull(album.getName());
251        int sourceType = DataSourceType.identifySourceType(album);
252        if (isLabelChanged(entry, title, totalCount, sourceType)) {
253            entry.title = title;
254            entry.totalCount = totalCount;
255            entry.sourceType = sourceType;
256            if (entry.labelLoader != null) {
257                entry.labelLoader.recycle();
258                entry.labelLoader = null;
259                entry.label = null;
260            }
261            if (album != null) {
262                entry.labelLoader = new AlbumLabelLoader(
263                        slotIndex, title, totalCount, sourceType);
264            }
265        }
266
267        entry.coverItem = cover;
268        if (getDataVersion(cover) != entry.coverDataVersion) {
269            entry.coverDataVersion = getDataVersion(cover);
270            entry.isPanorama = GalleryUtils.isPanorama(cover);
271            entry.rotation = (cover == null) ? 0 : cover.getRotation();
272            entry.mediaType = (cover == null) ? 0 : cover.getMediaType();
273            if (entry.coverLoader != null) {
274                entry.coverLoader.recycle();
275                entry.coverLoader = null;
276                entry.content = null;
277            }
278            if (cover != null) {
279                entry.coverLoader = new AlbumCoverLoader(slotIndex, cover);
280            }
281        }
282    }
283
284    private void prepareSlotContent(int slotIndex) {
285        AlbumSetEntry entry = new AlbumSetEntry();
286        updateAlbumSetEntry(entry, slotIndex);
287        mData[slotIndex % mData.length] = entry;
288    }
289
290    private static boolean startLoadBitmap(BitmapLoader loader) {
291        if (loader == null) return false;
292        loader.startLoad();
293        return loader.isRequestInProgress();
294    }
295
296    private void uploadBackgroundTextureInSlot(int index) {
297        if (index < mContentStart || index >= mContentEnd) return;
298        AlbumSetEntry entry = mData[index % mData.length];
299        if (entry.content instanceof BitmapTexture) {
300            mTextureUploader.addBgTexture((BitmapTexture) entry.content);
301        }
302        if (entry.label instanceof BitmapTexture) {
303            mTextureUploader.addBgTexture((BitmapTexture) entry.label);
304        }
305    }
306
307    private void updateTextureUploadQueue() {
308        if (!mIsActive) return;
309        mTextureUploader.clear();
310
311        // Upload foreground texture
312        for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) {
313            AlbumSetEntry entry = mData[i % mData.length];
314            if (entry.content instanceof BitmapTexture) {
315                mTextureUploader.addFgTexture((BitmapTexture) entry.content);
316            }
317            if (entry.label instanceof BitmapTexture) {
318                mTextureUploader.addFgTexture((BitmapTexture) entry.label);
319            }
320        }
321
322        // add background textures
323        int range = Math.max(
324                (mContentEnd - mActiveEnd), (mActiveStart - mContentStart));
325        for (int i = 0; i < range; ++i) {
326            uploadBackgroundTextureInSlot(mActiveEnd + i);
327            uploadBackgroundTextureInSlot(mActiveStart - i - 1);
328        }
329    }
330
331    private void updateAllImageRequests() {
332        mActiveRequestCount = 0;
333        for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) {
334            AlbumSetEntry entry = mData[i % mData.length];
335            if (startLoadBitmap(entry.coverLoader)) ++mActiveRequestCount;
336            if (startLoadBitmap(entry.labelLoader)) ++mActiveRequestCount;
337        }
338        if (mActiveRequestCount == 0) {
339            requestNonactiveImages();
340        } else {
341            cancelNonactiveImages();
342        }
343    }
344
345    @Override
346    public void onSizeChanged(int size) {
347        if (mIsActive && mSize != size) {
348            mSize = size;
349            if (mListener != null) mListener.onSizeChanged(mSize);
350            if (mContentEnd > mSize) mContentEnd = mSize;
351            if (mActiveEnd > mSize) mActiveEnd = mSize;
352        }
353    }
354
355    @Override
356    public void onContentChanged(int index) {
357        if (!mIsActive) {
358            // paused, ignore slot changed event
359            return;
360        }
361
362        // If the updated content is not cached, ignore it
363        if (index < mContentStart || index >= mContentEnd) {
364            Log.w(TAG, String.format(
365                    "invalid update: %s is outside (%s, %s)",
366                    index, mContentStart, mContentEnd) );
367            return;
368        }
369
370        AlbumSetEntry entry = mData[index % mData.length];
371        updateAlbumSetEntry(entry, index);
372        updateAllImageRequests();
373        updateTextureUploadQueue();
374        if (mListener != null && isActiveSlot(index)) {
375            mListener.onContentChanged();
376        }
377    }
378
379    public BitmapTexture getLoadingTexture() {
380        if (mLoadingLabel == null) {
381            Bitmap bitmap = mLabelMaker.requestLabel(
382                    mLoadingText, "", DataSourceType.TYPE_NOT_CATEGORIZED)
383                    .run(ThreadPool.JOB_CONTEXT_STUB);
384            mLoadingLabel = new BitmapTexture(bitmap);
385            mLoadingLabel.setOpaque(false);
386        }
387        return mLoadingLabel;
388    }
389
390    public void pause() {
391        mIsActive = false;
392        for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
393            freeSlotContent(i);
394        }
395        mLabelMaker.clearRecycledLabels();
396    }
397
398    public void resume() {
399        mIsActive = true;
400        for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
401            prepareSlotContent(i);
402        }
403        updateAllImageRequests();
404    }
405
406    private static interface EntryUpdater {
407        public void updateEntry();
408    }
409
410    private class AlbumCoverLoader extends BitmapLoader implements EntryUpdater {
411        private MediaItem mMediaItem;
412        private final int mSlotIndex;
413
414        public AlbumCoverLoader(int slotIndex, MediaItem item) {
415            mSlotIndex = slotIndex;
416            mMediaItem = item;
417        }
418
419        @Override
420        protected void recycleBitmap(Bitmap bitmap) {
421            MediaItem.getMicroThumbPool().recycle(bitmap);
422        }
423
424        @Override
425        protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l) {
426            return mThreadPool.submit(mMediaItem.requestImage(
427                    MediaItem.TYPE_MICROTHUMBNAIL), l);
428        }
429
430        @Override
431        protected void onLoadComplete(Bitmap bitmap) {
432            mHandler.obtainMessage(MSG_UPDATE_ALBUM_ENTRY, this).sendToTarget();
433        }
434
435        @Override
436        public void updateEntry() {
437            Bitmap bitmap = getBitmap();
438            if (bitmap == null) return; // error or recycled
439
440            AlbumSetEntry entry = mData[mSlotIndex % mData.length];
441            BitmapTexture texture = new BitmapTexture(bitmap);
442            entry.content = texture;
443
444            if (isActiveSlot(mSlotIndex)) {
445                mTextureUploader.addFgTexture(texture);
446                --mActiveRequestCount;
447                if (mActiveRequestCount == 0) requestNonactiveImages();
448                if (mListener != null) mListener.onContentChanged();
449            } else {
450                mTextureUploader.addBgTexture(texture);
451            }
452        }
453    }
454
455    private static int identifyCacheFlag(MediaSet set) {
456        if (set == null || (set.getSupportedOperations()
457                & MediaSet.SUPPORT_CACHE) == 0) {
458            return MediaSet.CACHE_FLAG_NO;
459        }
460
461        return set.getCacheFlag();
462    }
463
464    private static int identifyCacheStatus(MediaSet set) {
465        if (set == null || (set.getSupportedOperations()
466                & MediaSet.SUPPORT_CACHE) == 0) {
467            return MediaSet.CACHE_STATUS_NOT_CACHED;
468        }
469
470        return set.getCacheStatus();
471    }
472
473    private class AlbumLabelLoader extends BitmapLoader implements EntryUpdater {
474        private final int mSlotIndex;
475        private final String mTitle;
476        private final int mTotalCount;
477        private final int mSourceType;
478
479        public AlbumLabelLoader(
480                int slotIndex, String title, int totalCount, int sourceType) {
481            mSlotIndex = slotIndex;
482            mTitle = title;
483            mTotalCount = totalCount;
484            mSourceType = sourceType;
485        }
486
487        @Override
488        protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l) {
489            return mThreadPool.submit(mLabelMaker.requestLabel(
490                    mTitle, String.valueOf(mTotalCount), mSourceType), l);
491        }
492
493        @Override
494        protected void recycleBitmap(Bitmap bitmap) {
495            mLabelMaker.recycleLabel(bitmap);
496        }
497
498        @Override
499        protected void onLoadComplete(Bitmap bitmap) {
500            mHandler.obtainMessage(MSG_UPDATE_ALBUM_ENTRY, this).sendToTarget();
501        }
502
503        @Override
504        public void updateEntry() {
505            Bitmap bitmap = getBitmap();
506            if (bitmap == null) return; // Error or recycled
507
508            AlbumSetEntry entry = mData[mSlotIndex % mData.length];
509            BitmapTexture texture = new BitmapTexture(bitmap);
510            texture.setOpaque(false);
511            entry.label = texture;
512
513            if (isActiveSlot(mSlotIndex)) {
514                mTextureUploader.addFgTexture(texture);
515                --mActiveRequestCount;
516                if (mActiveRequestCount == 0) requestNonactiveImages();
517                if (mListener != null) mListener.onContentChanged();
518            } else {
519                mTextureUploader.addBgTexture(texture);
520            }
521        }
522    }
523
524    public void onSlotSizeChanged(int width, int height) {
525        if (mSlotWidth == width) return;
526
527        mSlotWidth = width;
528        mLoadingLabel = null;
529        mLabelMaker.setLabelWidth(mSlotWidth);
530
531        if (!mIsActive) return;
532
533        for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
534            AlbumSetEntry entry = mData[i % mData.length];
535            if (entry.labelLoader != null) {
536                entry.labelLoader.recycle();
537                entry.labelLoader = null;
538                entry.label = null;
539            }
540            if (entry.album != null) {
541                entry.labelLoader = new AlbumLabelLoader(i,
542                        entry.title, entry.totalCount, entry.sourceType);
543            }
544        }
545        updateAllImageRequests();
546        updateTextureUploadQueue();
547    }
548}
549