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