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