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 
17 package com.android.gallery3d.ui;
18 
19 import android.graphics.Bitmap;
20 import android.os.Message;
21 
22 import com.android.gallery3d.R;
23 import com.android.gallery3d.app.AbstractGalleryActivity;
24 import com.android.gallery3d.app.AlbumSetDataLoader;
25 import com.android.gallery3d.common.Utils;
26 import com.android.gallery3d.data.DataSourceType;
27 import com.android.gallery3d.data.MediaItem;
28 import com.android.gallery3d.data.MediaObject;
29 import com.android.gallery3d.data.MediaSet;
30 import com.android.gallery3d.data.Path;
31 import com.android.gallery3d.glrenderer.BitmapTexture;
32 import com.android.gallery3d.glrenderer.Texture;
33 import com.android.gallery3d.glrenderer.TextureUploader;
34 import com.android.gallery3d.glrenderer.TiledTexture;
35 import com.android.gallery3d.util.Future;
36 import com.android.gallery3d.util.FutureListener;
37 import com.android.gallery3d.util.ThreadPool;
38 
39 public 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 {
onSizeChanged(int size)44         public void onSizeChanged(int size);
onContentChanged()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 
AlbumSetSlidingWindow(AbstractGalleryActivity activity, AlbumSetDataLoader source, AlbumSetSlotRenderer.LabelSpec labelSpec, int cacheSize)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 
setListener(Listener listener)116     public void setListener(Listener listener) {
117         mListener = listener;
118     }
119 
get(int slotIndex)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 
size()128     public int size() {
129         return mSize;
130     }
131 
isActiveSlot(int slotIndex)132     public boolean isActiveSlot(int slotIndex) {
133         return slotIndex >= mActiveStart && slotIndex < mActiveEnd;
134     }
135 
setContentWindow(int contentStart, int contentEnd)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 
setActiveWindow(int start, int end)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 ----------->|
requestNonactiveImages()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 
cancelNonactiveImages()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 
requestImagesInSlot(int slotIndex)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 
cancelImagesInSlot(int slotIndex)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 
getDataVersion(MediaObject object)224     private static long getDataVersion(MediaObject object) {
225         return object == null
226                 ? MediaSet.INVALID_DATA_VERSION
227                 : object.getDataVersion();
228     }
229 
freeSlotContent(int slotIndex)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 
isLabelChanged( AlbumSetEntry entry, String title, int totalCount, int sourceType)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 
updateAlbumSetEntry(AlbumSetEntry entry, int slotIndex)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 
prepareSlotContent(int slotIndex)290     private void prepareSlotContent(int slotIndex) {
291         AlbumSetEntry entry = new AlbumSetEntry();
292         updateAlbumSetEntry(entry, slotIndex);
293         mData[slotIndex % mData.length] = entry;
294     }
295 
startLoadBitmap(BitmapLoader loader)296     private static boolean startLoadBitmap(BitmapLoader loader) {
297         if (loader == null) return false;
298         loader.startLoad();
299         return loader.isRequestInProgress();
300     }
301 
uploadBackgroundTextureInSlot(int index)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 
updateTextureUploadQueue()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 
updateAllImageRequests()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
onSizeChanged(int size)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
onContentChanged(int index)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 
getLoadingTexture()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 
pause()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 
resume()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 {
updateEntry()417         public void updateEntry();
418     }
419 
420     private class AlbumCoverLoader extends BitmapLoader implements EntryUpdater {
421         private MediaItem mMediaItem;
422         private final int mSlotIndex;
423 
AlbumCoverLoader(int slotIndex, MediaItem item)424         public AlbumCoverLoader(int slotIndex, MediaItem item) {
425             mSlotIndex = slotIndex;
426             mMediaItem = item;
427         }
428 
429         @Override
submitBitmapTask(FutureListener<Bitmap> l)430         protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l) {
431             return mThreadPool.submit(mMediaItem.requestImage(
432                     MediaItem.TYPE_MICROTHUMBNAIL), l);
433         }
434 
435         @Override
onLoadComplete(Bitmap bitmap)436         protected void onLoadComplete(Bitmap bitmap) {
437             mHandler.obtainMessage(MSG_UPDATE_ALBUM_ENTRY, this).sendToTarget();
438         }
439 
440         @Override
updateEntry()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 
identifyCacheFlag(MediaSet set)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 
identifyCacheStatus(MediaSet set)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 
AlbumLabelLoader( int slotIndex, String title, int totalCount, int sourceType)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
submitBitmapTask(FutureListener<Bitmap> l)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
onLoadComplete(Bitmap bitmap)500         protected void onLoadComplete(Bitmap bitmap) {
501             mHandler.obtainMessage(MSG_UPDATE_ALBUM_ENTRY, this).sendToTarget();
502         }
503 
504         @Override
updateEntry()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 
onSlotSizeChanged(int width, int height)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