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