AlbumSetSlidingWindow.java revision da071d27a1435cce080b5c609d0d833555e5a175
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 com.android.gallery3d.R;
20import com.android.gallery3d.app.GalleryActivity;
21import com.android.gallery3d.common.Utils;
22import com.android.gallery3d.data.MediaItem;
23import com.android.gallery3d.data.MediaSet;
24import com.android.gallery3d.data.Path;
25import com.android.gallery3d.ui.AlbumSetView.AlbumSetItem;
26import com.android.gallery3d.util.Future;
27import com.android.gallery3d.util.FutureListener;
28import com.android.gallery3d.util.GalleryUtils;
29import com.android.gallery3d.util.MediaSetUtils;
30import com.android.gallery3d.util.ThreadPool;
31
32import android.graphics.Bitmap;
33import android.graphics.Color;
34import android.os.Message;
35
36public class AlbumSetSlidingWindow implements AlbumSetView.ModelListener {
37    private static final String TAG = "GallerySlidingWindow";
38    private static final int MSG_LOAD_BITMAP_DONE = 0;
39    private static final int PLACEHOLDER_COLOR = 0xFF222222;
40
41    public static interface Listener {
42        public void onSizeChanged(int size);
43        public void onContentInvalidated();
44        public void onWindowContentChanged(
45                int slot, AlbumSetItem old, AlbumSetItem update);
46    }
47
48    private final AlbumSetView.Model mSource;
49    private int mSize;
50    private AlbumSetView.LabelSpec mLabelSpec;
51
52    private int mContentStart = 0;
53    private int mContentEnd = 0;
54
55    private int mActiveStart = 0;
56    private int mActiveEnd = 0;
57
58    private Listener mListener;
59
60    private final MyAlbumSetItem mData[];
61    private SelectionDrawer mSelectionDrawer;
62    private final ColorTexture mWaitLoadingTexture;
63
64    private SynchronizedHandler mHandler;
65    private ThreadPool mThreadPool;
66
67    private int mActiveRequestCount = 0;
68    private String mLoadingLabel;
69    private boolean mIsActive = false;
70
71    private static class MyAlbumSetItem extends AlbumSetItem {
72        public Path setPath;
73        public int sourceType;
74        public int cacheFlag;
75        public int cacheStatus;
76    }
77
78    public AlbumSetSlidingWindow(GalleryActivity activity,
79            AlbumSetView.LabelSpec labelSpec, SelectionDrawer drawer,
80            AlbumSetView.Model source, int cacheSize) {
81        source.setModelListener(this);
82        mLabelSpec = labelSpec;
83        mLoadingLabel = activity.getAndroidContext().getString(R.string.loading);
84        mSource = source;
85        mSelectionDrawer = drawer;
86        mData = new MyAlbumSetItem[cacheSize];
87        mSize = source.size();
88
89        mWaitLoadingTexture = new ColorTexture(PLACEHOLDER_COLOR);
90        mWaitLoadingTexture.setSize(1, 1);
91
92        mHandler = new SynchronizedHandler(activity.getGLRoot()) {
93            @Override
94            public void handleMessage(Message message) {
95                Utils.assertTrue(message.what == MSG_LOAD_BITMAP_DONE);
96                ((GalleryDisplayItem) message.obj).onLoadBitmapDone();
97            }
98        };
99
100        mThreadPool = activity.getThreadPool();
101    }
102
103    public void setSelectionDrawer(SelectionDrawer drawer) {
104        mSelectionDrawer = drawer;
105    }
106
107    public void setListener(Listener listener) {
108        mListener = listener;
109    }
110
111    public AlbumSetItem get(int slotIndex) {
112        Utils.assertTrue(isActiveSlot(slotIndex),
113                "invalid slot: %s outsides (%s, %s)",
114                slotIndex, mActiveStart, mActiveEnd);
115        return mData[slotIndex % mData.length];
116    }
117
118    public int size() {
119        return mSize;
120    }
121
122    public boolean isActiveSlot(int slotIndex) {
123        return slotIndex >= mActiveStart && slotIndex < mActiveEnd;
124    }
125
126    private void setContentWindow(int contentStart, int contentEnd) {
127        if (contentStart == mContentStart && contentEnd == mContentEnd) return;
128
129        if (contentStart >= mContentEnd || mContentStart >= contentEnd) {
130            for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
131                freeSlotContent(i);
132            }
133            mSource.setActiveWindow(contentStart, contentEnd);
134            for (int i = contentStart; i < contentEnd; ++i) {
135                prepareSlotContent(i);
136            }
137        } else {
138            for (int i = mContentStart; i < contentStart; ++i) {
139                freeSlotContent(i);
140            }
141            for (int i = contentEnd, n = mContentEnd; i < n; ++i) {
142                freeSlotContent(i);
143            }
144            mSource.setActiveWindow(contentStart, contentEnd);
145            for (int i = contentStart, n = mContentStart; i < n; ++i) {
146                prepareSlotContent(i);
147            }
148            for (int i = mContentEnd; i < contentEnd; ++i) {
149                prepareSlotContent(i);
150            }
151        }
152
153        mContentStart = contentStart;
154        mContentEnd = contentEnd;
155    }
156
157    public void setActiveWindow(int start, int end) {
158        Utils.assertTrue(
159                start <= end && end - start <= mData.length && end <= mSize,
160                "start = %s, end = %s, length = %s, size = %s",
161                start, end, mData.length, mSize);
162
163        AlbumSetItem data[] = mData;
164
165        mActiveStart = start;
166        mActiveEnd = end;
167
168        // If no data is visible, keep the cache content
169        if (start == end) return;
170
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        if (mIsActive) updateAllImageRequests();
176    }
177
178    // We would like to request non active slots in the following order:
179    // Order:    8 6 4 2                   1 3 5 7
180    //         |---------|---------------|---------|
181    //                   |<-  active  ->|
182    //         |<-------- cached range ----------->|
183    private void requestNonactiveImages() {
184        int range = Math.max(
185                mContentEnd - mActiveEnd, mActiveStart - mContentStart);
186        for (int i = 0 ;i < range; ++i) {
187            requestImagesInSlot(mActiveEnd + i);
188            requestImagesInSlot(mActiveStart - 1 - i);
189        }
190    }
191
192    private void cancelNonactiveImages() {
193        int range = Math.max(
194                mContentEnd - mActiveEnd, mActiveStart - mContentStart);
195        for (int i = 0 ;i < range; ++i) {
196            cancelImagesInSlot(mActiveEnd + i);
197            cancelImagesInSlot(mActiveStart - 1 - i);
198        }
199    }
200
201    private void requestImagesInSlot(int slotIndex) {
202        if (slotIndex < mContentStart || slotIndex >= mContentEnd) return;
203        AlbumSetItem items = mData[slotIndex % mData.length];
204        for (DisplayItem item : items.covers) {
205            ((GalleryDisplayItem) item).requestImage();
206        }
207    }
208
209    private void cancelImagesInSlot(int slotIndex) {
210        if (slotIndex < mContentStart || slotIndex >= mContentEnd) return;
211        AlbumSetItem items = mData[slotIndex % mData.length];
212        for (DisplayItem item : items.covers) {
213            ((GalleryDisplayItem) item).cancelImageRequest();
214        }
215    }
216
217    private void freeSlotContent(int slotIndex) {
218        AlbumSetItem data[] = mData;
219        int index = slotIndex % data.length;
220        AlbumSetItem original = data[index];
221        if (original != null) {
222            data[index] = null;
223            for (DisplayItem item : original.covers) {
224                ((GalleryDisplayItem) item).recycle();
225            }
226        }
227    }
228
229    private long getMediaSetDataVersion(MediaSet set) {
230        return set == null
231                ? MediaSet.INVALID_DATA_VERSION
232                : set.getDataVersion();
233    }
234
235    private void prepareSlotContent(int slotIndex) {
236        MediaSet set = mSource.getMediaSet(slotIndex);
237
238        MyAlbumSetItem item = new MyAlbumSetItem();
239        MediaItem[] coverItems = mSource.getCoverItems(slotIndex);
240        item.covers = new GalleryDisplayItem[coverItems.length];
241        item.sourceType = identifySourceType(set);
242        item.cacheFlag = identifyCacheFlag(set);
243        item.cacheStatus = identifyCacheStatus(set);
244        item.setPath = set == null ? null : set.getPath();
245
246        for (int i = 0; i < coverItems.length; ++i) {
247            item.covers[i] = new GalleryDisplayItem(slotIndex, i, coverItems[i]);
248        }
249        item.labelItem = new LabelDisplayItem(slotIndex);
250        item.setDataVersion = getMediaSetDataVersion(set);
251        mData[slotIndex % mData.length] = item;
252    }
253
254    private boolean isCoverItemsChanged(int slotIndex) {
255        AlbumSetItem original = mData[slotIndex % mData.length];
256        if (original == null) return true;
257        MediaItem[] coverItems = mSource.getCoverItems(slotIndex);
258
259        if (original.covers.length != coverItems.length) return true;
260        for (int i = 0, n = coverItems.length; i < n; ++i) {
261            GalleryDisplayItem g = (GalleryDisplayItem) original.covers[i];
262            if (g.mDataVersion != coverItems[i].getDataVersion()) return true;
263        }
264        return false;
265    }
266
267    private void updateSlotContent(final int slotIndex) {
268
269        MyAlbumSetItem data[] = mData;
270        int pos = slotIndex % data.length;
271        MyAlbumSetItem original = data[pos];
272
273        if (!isCoverItemsChanged(slotIndex)) {
274            MediaSet set = mSource.getMediaSet(slotIndex);
275            original.sourceType = identifySourceType(set);
276            original.cacheFlag = identifyCacheFlag(set);
277            original.cacheStatus = identifyCacheStatus(set);
278            original.setPath = set == null ? null : set.getPath();
279            ((LabelDisplayItem) original.labelItem).updateContent();
280            if (mListener != null) mListener.onContentInvalidated();
281            return;
282        }
283
284        prepareSlotContent(slotIndex);
285        AlbumSetItem update = data[pos];
286
287        if (mListener != null && isActiveSlot(slotIndex)) {
288            mListener.onWindowContentChanged(slotIndex, original, update);
289        }
290        if (original != null) {
291            for (DisplayItem item : original.covers) {
292                ((GalleryDisplayItem) item).recycle();
293            }
294        }
295    }
296
297    private void notifySlotChanged(int slotIndex) {
298        // If the updated content is not cached, ignore it
299        if (slotIndex < mContentStart || slotIndex >= mContentEnd) {
300            Log.w(TAG, String.format(
301                    "invalid update: %s is outside (%s, %s)",
302                    slotIndex, mContentStart, mContentEnd) );
303            return;
304        }
305        updateSlotContent(slotIndex);
306        boolean isActiveSlot = isActiveSlot(slotIndex);
307        if (mActiveRequestCount == 0 || isActiveSlot) {
308            for (DisplayItem item : mData[slotIndex % mData.length].covers) {
309                GalleryDisplayItem galleryItem = (GalleryDisplayItem) item;
310                galleryItem.requestImage();
311                if (isActiveSlot && galleryItem.isRequestInProgress()) {
312                    ++mActiveRequestCount;
313                }
314            }
315        }
316    }
317
318    private void updateAllImageRequests() {
319        mActiveRequestCount = 0;
320        for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) {
321            for (DisplayItem item : mData[i % mData.length].covers) {
322                GalleryDisplayItem coverItem = (GalleryDisplayItem) item;
323                coverItem.requestImage();
324                if (coverItem.isRequestInProgress()) ++mActiveRequestCount;
325            }
326        }
327        if (mActiveRequestCount == 0) {
328            requestNonactiveImages();
329        } else {
330            cancelNonactiveImages();
331        }
332    }
333
334    private class GalleryDisplayItem extends AbstractDisplayItem
335            implements FutureListener<Bitmap> {
336        private Future<Bitmap> mFuture;
337        private final int mSlotIndex;
338        private final int mCoverIndex;
339        private final int mMediaType;
340        private Texture mContent;
341        private final long mDataVersion;
342        private boolean mIsPanorama;
343
344        public GalleryDisplayItem(int slotIndex, int coverIndex, MediaItem item) {
345            super(item);
346            mSlotIndex = slotIndex;
347            mCoverIndex = coverIndex;
348            mMediaType = item.getMediaType();
349            mDataVersion = item.getDataVersion();
350            mIsPanorama = GalleryUtils.isPanorama(item);
351            updateContent(mWaitLoadingTexture);
352        }
353
354        @Override
355        protected void onBitmapAvailable(Bitmap bitmap) {
356            if (isActiveSlot(mSlotIndex)) {
357                --mActiveRequestCount;
358                if (mActiveRequestCount == 0) requestNonactiveImages();
359            }
360            if (bitmap != null) {
361                BitmapTexture texture = new BitmapTexture(bitmap, true);
362                texture.setThrottled(true);
363                updateContent(new FadeInTexture(PLACEHOLDER_COLOR, texture));
364                if (mListener != null) mListener.onContentInvalidated();
365            }
366        }
367
368        private void updateContent(Texture content) {
369            mContent = content;
370        }
371
372        @Override
373        public int render(GLCanvas canvas, int pass) {
374            // Fit the content into the box
375            int width = mContent.getWidth();
376            int height = mContent.getHeight();
377
378            float scalex = mBoxWidth / (float) width;
379            float scaley = mBoxHeight / (float) height;
380            float scale = Math.min(scalex, scaley);
381
382            width = (int) Math.floor(width * scale);
383            height = (int) Math.floor(height * scale);
384
385            // Now draw it
386            int sourceType = SelectionDrawer.DATASOURCE_TYPE_NOT_CATEGORIZED;
387            int cacheFlag = MediaSet.CACHE_FLAG_NO;
388            int cacheStatus = MediaSet.CACHE_STATUS_NOT_CACHED;
389            MyAlbumSetItem set = mData[mSlotIndex % mData.length];
390            Path path = set.setPath;
391            if (mCoverIndex == 0) {
392                sourceType = set.sourceType;
393                cacheFlag = set.cacheFlag;
394                cacheStatus = set.cacheStatus;
395            }
396
397            mSelectionDrawer.draw(canvas, mContent, width, height,
398                    getRotation(), path, sourceType, mMediaType,
399                    mIsPanorama, mLabelSpec.labelBackgroundHeight,
400                    cacheFlag == MediaSet.CACHE_FLAG_FULL,
401                    (cacheFlag == MediaSet.CACHE_FLAG_FULL)
402                    && (cacheStatus != MediaSet.CACHE_STATUS_CACHED_FULL));
403
404            if (mContent != mWaitLoadingTexture &&
405                    ((FadeInTexture) mContent).isAnimating()) {
406                return RENDER_MORE_FRAME;
407            } else {
408                return 0;
409            }
410        }
411
412        @Override
413        public void startLoadBitmap() {
414            mFuture = mThreadPool.submit(mMediaItem.requestImage(
415                    MediaItem.TYPE_MICROTHUMBNAIL), this);
416        }
417
418        @Override
419        public void cancelLoadBitmap() {
420            mFuture.cancel();
421        }
422
423        @Override
424        public void onFutureDone(Future<Bitmap> future) {
425            mHandler.sendMessage(mHandler.obtainMessage(MSG_LOAD_BITMAP_DONE, this));
426        }
427
428        private void onLoadBitmapDone() {
429            Future<Bitmap> future = mFuture;
430            mFuture = null;
431            updateImage(future.get(), future.isCancelled());
432        }
433
434        @Override
435        public String toString() {
436            return String.format("GalleryDisplayItem(%s, %s)", mSlotIndex, mCoverIndex);
437        }
438    }
439
440    private static int identifySourceType(MediaSet set) {
441        if (set == null) {
442            return SelectionDrawer.DATASOURCE_TYPE_NOT_CATEGORIZED;
443        }
444
445        Path path = set.getPath();
446        if (MediaSetUtils.isCameraSource(path)) {
447            return SelectionDrawer.DATASOURCE_TYPE_CAMERA;
448        }
449
450        int type = SelectionDrawer.DATASOURCE_TYPE_NOT_CATEGORIZED;
451        String prefix = path.getPrefix();
452
453        if (prefix.equals("picasa")) {
454            type = SelectionDrawer.DATASOURCE_TYPE_PICASA;
455        } else if (prefix.equals("local") || prefix.equals("merge")) {
456            type = SelectionDrawer.DATASOURCE_TYPE_LOCAL;
457        } else if (prefix.equals("mtp")) {
458            type = SelectionDrawer.DATASOURCE_TYPE_MTP;
459        }
460
461        return type;
462    }
463
464    private static int identifyCacheFlag(MediaSet set) {
465        if (set == null || (set.getSupportedOperations()
466                & MediaSet.SUPPORT_CACHE) == 0) {
467            return MediaSet.CACHE_FLAG_NO;
468        }
469
470        return set.getCacheFlag();
471    }
472
473    private static int identifyCacheStatus(MediaSet set) {
474        if (set == null || (set.getSupportedOperations()
475                & MediaSet.SUPPORT_CACHE) == 0) {
476            return MediaSet.CACHE_STATUS_NOT_CACHED;
477        }
478
479        return set.getCacheStatus();
480    }
481
482    private class LabelDisplayItem extends DisplayItem {
483        private static final int FONT_COLOR_TITLE = Color.WHITE;
484        private static final int FONT_COLOR_COUNT = 0x80FFFFFF;  // 50% white
485
486        private StringTexture mTextureTitle;
487        private StringTexture mTextureCount;
488        private String mTitle;
489        private String mCount;
490        private int mLastWidth;
491        private final int mSlotIndex;
492        private boolean mHasIcon;
493
494        public LabelDisplayItem(int slotIndex) {
495            mSlotIndex = slotIndex;
496        }
497
498        public boolean updateContent() {
499            String title = mLoadingLabel;
500            String count = "";
501            MediaSet set = mSource.getMediaSet(mSlotIndex);
502            if (set != null) {
503                title = Utils.ensureNotNull(set.getName());
504                count = "" + set.getTotalMediaItemCount();
505            }
506            if (Utils.equals(title, mTitle)
507                    && Utils.equals(count, mCount)
508                    && Utils.equals(mBoxWidth, mLastWidth)) {
509                    return false;
510            }
511            mTitle = title;
512            mCount = count;
513            mLastWidth = mBoxWidth;
514            mHasIcon = (identifySourceType(set) !=
515                    SelectionDrawer.DATASOURCE_TYPE_NOT_CATEGORIZED);
516
517            AlbumSetView.LabelSpec s = mLabelSpec;
518            mTextureTitle = StringTexture.newInstance(
519                    title, s.titleFontSize, FONT_COLOR_TITLE,
520                    mBoxWidth - s.leftMargin, false);
521            mTextureCount = StringTexture.newInstance(
522                    count, s.countFontSize, FONT_COLOR_COUNT,
523                    mBoxWidth - s.leftMargin, true);
524
525            return true;
526        }
527
528        @Override
529        public int render(GLCanvas canvas, int pass) {
530            if (mBoxWidth != mLastWidth) {
531                updateContent();
532            }
533
534            AlbumSetView.LabelSpec s = mLabelSpec;
535            int x = -mBoxWidth / 2;
536            int y = (mBoxHeight + 1) / 2 - s.labelBackgroundHeight;
537            y += s.titleOffset;
538            mTextureTitle.draw(canvas, x + s.leftMargin, y);
539            y += s.titleFontSize + s.countOffset;
540            x += mHasIcon ? s.iconSize : s.leftMargin;
541            mTextureCount.draw(canvas, x, y);
542            return 0;
543        }
544
545        @Override
546        public long getIdentity() {
547            return System.identityHashCode(this);
548        }
549    }
550
551    public void onSizeChanged(int size) {
552        if (mSize != size) {
553            mSize = size;
554            if (mListener != null && mIsActive) mListener.onSizeChanged(mSize);
555        }
556    }
557
558    public void onWindowContentChanged(int index) {
559        if (!mIsActive) {
560            // paused, ignore slot changed event
561            return;
562        }
563        notifySlotChanged(index);
564    }
565
566    public void pause() {
567        mIsActive = false;
568        for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
569            freeSlotContent(i);
570        }
571    }
572
573    public void resume() {
574        mIsActive = true;
575        for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
576            prepareSlotContent(i);
577        }
578        updateAllImageRequests();
579    }
580}
581