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