AlbumSetSlidingWindow.java revision 8ef6c55bdad9a3e835ce56bdc98681434b4ac5b3
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        }
351    }
352
353    @Override
354    public void onContentChanged(int index) {
355        if (!mIsActive) {
356            // paused, ignore slot changed event
357            return;
358        }
359
360        // If the updated content is not cached, ignore it
361        if (index < mContentStart || index >= mContentEnd) {
362            Log.w(TAG, String.format(
363                    "invalid update: %s is outside (%s, %s)",
364                    index, mContentStart, mContentEnd) );
365            return;
366        }
367
368        AlbumSetEntry entry = mData[index % mData.length];
369        updateAlbumSetEntry(entry, index);
370        updateAllImageRequests();
371        updateTextureUploadQueue();
372        if (mListener != null && isActiveSlot(index)) {
373            mListener.onContentChanged();
374        }
375    }
376
377    public BitmapTexture getLoadingTexture() {
378        if (mLoadingLabel == null) {
379            Bitmap bitmap = mLabelMaker.requestLabel(
380                    mLoadingText, "", DataSourceType.TYPE_NOT_CATEGORIZED)
381                    .run(ThreadPool.JOB_CONTEXT_STUB);
382            mLoadingLabel = new BitmapTexture(bitmap);
383            mLoadingLabel.setOpaque(false);
384        }
385        return mLoadingLabel;
386    }
387
388    public void pause() {
389        mIsActive = false;
390        for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
391            freeSlotContent(i);
392        }
393        mLabelMaker.clearRecycledLabels();
394    }
395
396    public void resume() {
397        mIsActive = true;
398        for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
399            prepareSlotContent(i);
400        }
401        updateAllImageRequests();
402    }
403
404    private static interface EntryUpdater {
405        public void updateEntry();
406    }
407
408    private class AlbumCoverLoader extends BitmapLoader implements EntryUpdater {
409        private MediaItem mMediaItem;
410        private final int mSlotIndex;
411
412        public AlbumCoverLoader(int slotIndex, MediaItem item) {
413            mSlotIndex = slotIndex;
414            mMediaItem = item;
415        }
416
417        @Override
418        protected void recycleBitmap(Bitmap bitmap) {
419            MediaItem.getMicroThumbPool().recycle(bitmap);
420        }
421
422        @Override
423        protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l) {
424            return mThreadPool.submit(mMediaItem.requestImage(
425                    MediaItem.TYPE_MICROTHUMBNAIL), l);
426        }
427
428        @Override
429        protected void onLoadComplete(Bitmap bitmap) {
430            mHandler.obtainMessage(MSG_UPDATE_ALBUM_ENTRY, this).sendToTarget();
431        }
432
433        @Override
434        public void updateEntry() {
435            Bitmap bitmap = getBitmap();
436            if (bitmap == null) return; // error or recycled
437
438            AlbumSetEntry entry = mData[mSlotIndex % mData.length];
439            BitmapTexture texture = new BitmapTexture(bitmap);
440            entry.content = texture;
441
442            if (isActiveSlot(mSlotIndex)) {
443                mTextureUploader.addFgTexture(texture);
444                --mActiveRequestCount;
445                if (mActiveRequestCount == 0) requestNonactiveImages();
446                if (mListener != null) mListener.onContentChanged();
447            } else {
448                mTextureUploader.addBgTexture(texture);
449            }
450        }
451    }
452
453    private static int identifyCacheFlag(MediaSet set) {
454        if (set == null || (set.getSupportedOperations()
455                & MediaSet.SUPPORT_CACHE) == 0) {
456            return MediaSet.CACHE_FLAG_NO;
457        }
458
459        return set.getCacheFlag();
460    }
461
462    private static int identifyCacheStatus(MediaSet set) {
463        if (set == null || (set.getSupportedOperations()
464                & MediaSet.SUPPORT_CACHE) == 0) {
465            return MediaSet.CACHE_STATUS_NOT_CACHED;
466        }
467
468        return set.getCacheStatus();
469    }
470
471    private class AlbumLabelLoader extends BitmapLoader implements EntryUpdater {
472        private final int mSlotIndex;
473        private final String mTitle;
474        private final int mTotalCount;
475        private final int mSourceType;
476
477        public AlbumLabelLoader(
478                int slotIndex, String title, int totalCount, int sourceType) {
479            mSlotIndex = slotIndex;
480            mTitle = title;
481            mTotalCount = totalCount;
482            mSourceType = sourceType;
483        }
484
485        @Override
486        protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l) {
487            return mThreadPool.submit(mLabelMaker.requestLabel(
488                    mTitle, String.valueOf(mTotalCount), mSourceType), l);
489        }
490
491        @Override
492        protected void recycleBitmap(Bitmap bitmap) {
493            mLabelMaker.reycleLabel(bitmap);
494        }
495
496        @Override
497        protected void onLoadComplete(Bitmap bitmap) {
498            mHandler.obtainMessage(MSG_UPDATE_ALBUM_ENTRY, this).sendToTarget();
499        }
500
501        @Override
502        public void updateEntry() {
503            Bitmap bitmap = getBitmap();
504            if (bitmap == null) return; // Error or recycled
505
506            AlbumSetEntry entry = mData[mSlotIndex % mData.length];
507            BitmapTexture texture = new BitmapTexture(bitmap);
508            texture.setOpaque(false);
509            entry.label = texture;
510
511            if (isActiveSlot(mSlotIndex)) {
512                mTextureUploader.addFgTexture(texture);
513                --mActiveRequestCount;
514                if (mActiveRequestCount == 0) requestNonactiveImages();
515                if (mListener != null) mListener.onContentChanged();
516            } else {
517                mTextureUploader.addBgTexture(texture);
518            }
519        }
520    }
521
522    public void onSlotSizeChanged(int width, int height) {
523        if (mSlotWidth == width) return;
524
525        mSlotWidth = width;
526        mLoadingLabel = null;
527        mLabelMaker.setLabelWidth(mSlotWidth);
528
529        if (!mIsActive) return;
530
531        for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
532            AlbumSetEntry entry = mData[i % mData.length];
533            if (entry.labelLoader != null) {
534                entry.labelLoader.recycle();
535                entry.labelLoader = null;
536                entry.label = null;
537            }
538            if (entry.album != null) {
539                entry.labelLoader = new AlbumLabelLoader(i,
540                        entry.title, entry.totalCount, entry.sourceType);
541            }
542        }
543        updateAllImageRequests();
544        updateTextureUploadQueue();
545    }
546}
547