| /*T |
| * Copyright (C) 2010 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.gallery3d.ui; |
| |
| import android.graphics.Bitmap; |
| import android.os.Message; |
| |
| import com.android.gallery3d.R; |
| import com.android.gallery3d.app.AbstractGalleryActivity; |
| import com.android.gallery3d.app.AlbumSetDataLoader; |
| import com.android.gallery3d.common.Utils; |
| import com.android.gallery3d.data.DataSourceType; |
| import com.android.gallery3d.data.MediaItem; |
| import com.android.gallery3d.data.MediaObject; |
| import com.android.gallery3d.data.MediaSet; |
| import com.android.gallery3d.data.Path; |
| import com.android.gallery3d.glrenderer.BitmapTexture; |
| import com.android.gallery3d.glrenderer.Texture; |
| import com.android.gallery3d.glrenderer.TextureUploader; |
| import com.android.gallery3d.glrenderer.TiledTexture; |
| import com.android.gallery3d.util.Future; |
| import com.android.gallery3d.util.FutureListener; |
| import com.android.gallery3d.util.ThreadPool; |
| |
| public class AlbumSetSlidingWindow implements AlbumSetDataLoader.DataListener { |
| private static final String TAG = "AlbumSetSlidingWindow"; |
| private static final int MSG_UPDATE_ALBUM_ENTRY = 1; |
| |
| public static interface Listener { |
| public void onSizeChanged(int size); |
| public void onContentChanged(); |
| } |
| |
| private final AlbumSetDataLoader mSource; |
| private int mSize; |
| |
| private int mContentStart = 0; |
| private int mContentEnd = 0; |
| |
| private int mActiveStart = 0; |
| private int mActiveEnd = 0; |
| |
| private Listener mListener; |
| |
| private final AlbumSetEntry mData[]; |
| private final SynchronizedHandler mHandler; |
| private final ThreadPool mThreadPool; |
| private final AlbumLabelMaker mLabelMaker; |
| private final String mLoadingText; |
| |
| private final TiledTexture.Uploader mContentUploader; |
| private final TextureUploader mLabelUploader; |
| |
| private int mActiveRequestCount = 0; |
| private boolean mIsActive = false; |
| private BitmapTexture mLoadingLabel; |
| |
| private int mSlotWidth; |
| |
| public static class AlbumSetEntry { |
| public MediaSet album; |
| public MediaItem coverItem; |
| public Texture content; |
| public BitmapTexture labelTexture; |
| public TiledTexture bitmapTexture; |
| public Path setPath; |
| public String title; |
| public int totalCount; |
| public int sourceType; |
| public int cacheFlag; |
| public int cacheStatus; |
| public int rotation; |
| public boolean isWaitLoadingDisplayed; |
| public long setDataVersion; |
| public long coverDataVersion; |
| private BitmapLoader labelLoader; |
| private BitmapLoader coverLoader; |
| } |
| |
| public AlbumSetSlidingWindow(AbstractGalleryActivity activity, |
| AlbumSetDataLoader source, AlbumSetSlotRenderer.LabelSpec labelSpec, int cacheSize) { |
| source.setModelListener(this); |
| mSource = source; |
| mData = new AlbumSetEntry[cacheSize]; |
| mSize = source.size(); |
| mThreadPool = activity.getThreadPool(); |
| |
| mLabelMaker = new AlbumLabelMaker(activity.getAndroidContext(), labelSpec); |
| mLoadingText = activity.getAndroidContext().getString(R.string.loading); |
| mContentUploader = new TiledTexture.Uploader(activity.getGLRoot()); |
| mLabelUploader = new TextureUploader(activity.getGLRoot()); |
| |
| mHandler = new SynchronizedHandler(activity.getGLRoot()) { |
| @Override |
| public void handleMessage(Message message) { |
| Utils.assertTrue(message.what == MSG_UPDATE_ALBUM_ENTRY); |
| ((EntryUpdater) message.obj).updateEntry(); |
| } |
| }; |
| } |
| |
| public void setListener(Listener listener) { |
| mListener = listener; |
| } |
| |
| public AlbumSetEntry get(int slotIndex) { |
| if (!isActiveSlot(slotIndex)) { |
| Utils.fail("invalid slot: %s outsides (%s, %s)", |
| slotIndex, mActiveStart, mActiveEnd); |
| } |
| return mData[slotIndex % mData.length]; |
| } |
| |
| public int size() { |
| return mSize; |
| } |
| |
| public boolean isActiveSlot(int slotIndex) { |
| return slotIndex >= mActiveStart && slotIndex < mActiveEnd; |
| } |
| |
| private void setContentWindow(int contentStart, int contentEnd) { |
| if (contentStart == mContentStart && contentEnd == mContentEnd) return; |
| |
| if (contentStart >= mContentEnd || mContentStart >= contentEnd) { |
| for (int i = mContentStart, n = mContentEnd; i < n; ++i) { |
| freeSlotContent(i); |
| } |
| mSource.setActiveWindow(contentStart, contentEnd); |
| for (int i = contentStart; i < contentEnd; ++i) { |
| prepareSlotContent(i); |
| } |
| } else { |
| for (int i = mContentStart; i < contentStart; ++i) { |
| freeSlotContent(i); |
| } |
| for (int i = contentEnd, n = mContentEnd; i < n; ++i) { |
| freeSlotContent(i); |
| } |
| mSource.setActiveWindow(contentStart, contentEnd); |
| for (int i = contentStart, n = mContentStart; i < n; ++i) { |
| prepareSlotContent(i); |
| } |
| for (int i = mContentEnd; i < contentEnd; ++i) { |
| prepareSlotContent(i); |
| } |
| } |
| |
| mContentStart = contentStart; |
| mContentEnd = contentEnd; |
| } |
| |
| public void setActiveWindow(int start, int end) { |
| if (!(start <= end && end - start <= mData.length && end <= mSize)) { |
| Utils.fail("start = %s, end = %s, length = %s, size = %s", |
| start, end, mData.length, mSize); |
| } |
| |
| AlbumSetEntry data[] = mData; |
| mActiveStart = start; |
| mActiveEnd = end; |
| int contentStart = Utils.clamp((start + end) / 2 - data.length / 2, |
| 0, Math.max(0, mSize - data.length)); |
| int contentEnd = Math.min(contentStart + data.length, mSize); |
| setContentWindow(contentStart, contentEnd); |
| |
| if (mIsActive) { |
| updateTextureUploadQueue(); |
| updateAllImageRequests(); |
| } |
| } |
| |
| // We would like to request non active slots in the following order: |
| // Order: 8 6 4 2 1 3 5 7 |
| // |---------|---------------|---------| |
| // |<- active ->| |
| // |<-------- cached range ----------->| |
| private void requestNonactiveImages() { |
| int range = Math.max( |
| mContentEnd - mActiveEnd, mActiveStart - mContentStart); |
| for (int i = 0 ;i < range; ++i) { |
| requestImagesInSlot(mActiveEnd + i); |
| requestImagesInSlot(mActiveStart - 1 - i); |
| } |
| } |
| |
| private void cancelNonactiveImages() { |
| int range = Math.max( |
| mContentEnd - mActiveEnd, mActiveStart - mContentStart); |
| for (int i = 0 ;i < range; ++i) { |
| cancelImagesInSlot(mActiveEnd + i); |
| cancelImagesInSlot(mActiveStart - 1 - i); |
| } |
| } |
| |
| private void requestImagesInSlot(int slotIndex) { |
| if (slotIndex < mContentStart || slotIndex >= mContentEnd) return; |
| AlbumSetEntry entry = mData[slotIndex % mData.length]; |
| if (entry.coverLoader != null) entry.coverLoader.startLoad(); |
| if (entry.labelLoader != null) entry.labelLoader.startLoad(); |
| } |
| |
| private void cancelImagesInSlot(int slotIndex) { |
| if (slotIndex < mContentStart || slotIndex >= mContentEnd) return; |
| AlbumSetEntry entry = mData[slotIndex % mData.length]; |
| if (entry.coverLoader != null) entry.coverLoader.cancelLoad(); |
| if (entry.labelLoader != null) entry.labelLoader.cancelLoad(); |
| } |
| |
| private static long getDataVersion(MediaObject object) { |
| return object == null |
| ? MediaSet.INVALID_DATA_VERSION |
| : object.getDataVersion(); |
| } |
| |
| private void freeSlotContent(int slotIndex) { |
| AlbumSetEntry entry = mData[slotIndex % mData.length]; |
| if (entry.coverLoader != null) entry.coverLoader.recycle(); |
| if (entry.labelLoader != null) entry.labelLoader.recycle(); |
| if (entry.labelTexture != null) entry.labelTexture.recycle(); |
| if (entry.bitmapTexture != null) entry.bitmapTexture.recycle(); |
| mData[slotIndex % mData.length] = null; |
| } |
| |
| private boolean isLabelChanged( |
| AlbumSetEntry entry, String title, int totalCount, int sourceType) { |
| return !Utils.equals(entry.title, title) |
| || entry.totalCount != totalCount |
| || entry.sourceType != sourceType; |
| } |
| |
| private void updateAlbumSetEntry(AlbumSetEntry entry, int slotIndex) { |
| MediaSet album = mSource.getMediaSet(slotIndex); |
| MediaItem cover = mSource.getCoverItem(slotIndex); |
| int totalCount = mSource.getTotalCount(slotIndex); |
| |
| entry.album = album; |
| entry.setDataVersion = getDataVersion(album); |
| entry.cacheFlag = identifyCacheFlag(album); |
| entry.cacheStatus = identifyCacheStatus(album); |
| entry.setPath = (album == null) ? null : album.getPath(); |
| |
| String title = (album == null) ? "" : Utils.ensureNotNull(album.getName()); |
| int sourceType = DataSourceType.identifySourceType(album); |
| if (isLabelChanged(entry, title, totalCount, sourceType)) { |
| entry.title = title; |
| entry.totalCount = totalCount; |
| entry.sourceType = sourceType; |
| if (entry.labelLoader != null) { |
| entry.labelLoader.recycle(); |
| entry.labelLoader = null; |
| entry.labelTexture = null; |
| } |
| if (album != null) { |
| entry.labelLoader = new AlbumLabelLoader( |
| slotIndex, title, totalCount, sourceType); |
| } |
| } |
| |
| entry.coverItem = cover; |
| if (getDataVersion(cover) != entry.coverDataVersion) { |
| entry.coverDataVersion = getDataVersion(cover); |
| entry.rotation = (cover == null) ? 0 : cover.getRotation(); |
| if (entry.coverLoader != null) { |
| entry.coverLoader.recycle(); |
| entry.coverLoader = null; |
| entry.bitmapTexture = null; |
| entry.content = null; |
| } |
| if (cover != null) { |
| entry.coverLoader = new AlbumCoverLoader(slotIndex, cover); |
| } |
| } |
| } |
| |
| private void prepareSlotContent(int slotIndex) { |
| AlbumSetEntry entry = new AlbumSetEntry(); |
| updateAlbumSetEntry(entry, slotIndex); |
| mData[slotIndex % mData.length] = entry; |
| } |
| |
| private static boolean startLoadBitmap(BitmapLoader loader) { |
| if (loader == null) return false; |
| loader.startLoad(); |
| return loader.isRequestInProgress(); |
| } |
| |
| private void uploadBackgroundTextureInSlot(int index) { |
| if (index < mContentStart || index >= mContentEnd) return; |
| AlbumSetEntry entry = mData[index % mData.length]; |
| if (entry.bitmapTexture != null) { |
| mContentUploader.addTexture(entry.bitmapTexture); |
| } |
| if (entry.labelTexture != null) { |
| mLabelUploader.addBgTexture(entry.labelTexture); |
| } |
| } |
| |
| private void updateTextureUploadQueue() { |
| if (!mIsActive) return; |
| mContentUploader.clear(); |
| mLabelUploader.clear(); |
| |
| // Upload foreground texture |
| for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) { |
| AlbumSetEntry entry = mData[i % mData.length]; |
| if (entry.bitmapTexture != null) { |
| mContentUploader.addTexture(entry.bitmapTexture); |
| } |
| if (entry.labelTexture != null) { |
| mLabelUploader.addFgTexture(entry.labelTexture); |
| } |
| } |
| |
| // add background textures |
| int range = Math.max( |
| (mContentEnd - mActiveEnd), (mActiveStart - mContentStart)); |
| for (int i = 0; i < range; ++i) { |
| uploadBackgroundTextureInSlot(mActiveEnd + i); |
| uploadBackgroundTextureInSlot(mActiveStart - i - 1); |
| } |
| } |
| |
| private void updateAllImageRequests() { |
| mActiveRequestCount = 0; |
| for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) { |
| AlbumSetEntry entry = mData[i % mData.length]; |
| if (startLoadBitmap(entry.coverLoader)) ++mActiveRequestCount; |
| if (startLoadBitmap(entry.labelLoader)) ++mActiveRequestCount; |
| } |
| if (mActiveRequestCount == 0) { |
| requestNonactiveImages(); |
| } else { |
| cancelNonactiveImages(); |
| } |
| } |
| |
| @Override |
| public void onSizeChanged(int size) { |
| if (mIsActive && mSize != size) { |
| mSize = size; |
| if (mListener != null) mListener.onSizeChanged(mSize); |
| if (mContentEnd > mSize) mContentEnd = mSize; |
| if (mActiveEnd > mSize) mActiveEnd = mSize; |
| } |
| } |
| |
| @Override |
| public void onContentChanged(int index) { |
| if (!mIsActive) { |
| // paused, ignore slot changed event |
| return; |
| } |
| |
| // If the updated content is not cached, ignore it |
| if (index < mContentStart || index >= mContentEnd) { |
| Log.w(TAG, String.format( |
| "invalid update: %s is outside (%s, %s)", |
| index, mContentStart, mContentEnd) ); |
| return; |
| } |
| |
| AlbumSetEntry entry = mData[index % mData.length]; |
| updateAlbumSetEntry(entry, index); |
| updateAllImageRequests(); |
| updateTextureUploadQueue(); |
| if (mListener != null && isActiveSlot(index)) { |
| mListener.onContentChanged(); |
| } |
| } |
| |
| public BitmapTexture getLoadingTexture() { |
| if (mLoadingLabel == null) { |
| Bitmap bitmap = mLabelMaker.requestLabel( |
| mLoadingText, "", DataSourceType.TYPE_NOT_CATEGORIZED) |
| .run(ThreadPool.JOB_CONTEXT_STUB); |
| mLoadingLabel = new BitmapTexture(bitmap); |
| mLoadingLabel.setOpaque(false); |
| } |
| return mLoadingLabel; |
| } |
| |
| public void pause() { |
| mIsActive = false; |
| mLabelUploader.clear(); |
| mContentUploader.clear(); |
| TiledTexture.freeResources(); |
| for (int i = mContentStart, n = mContentEnd; i < n; ++i) { |
| freeSlotContent(i); |
| } |
| } |
| |
| public void resume() { |
| mIsActive = true; |
| TiledTexture.prepareResources(); |
| for (int i = mContentStart, n = mContentEnd; i < n; ++i) { |
| prepareSlotContent(i); |
| } |
| updateAllImageRequests(); |
| } |
| |
| private static interface EntryUpdater { |
| public void updateEntry(); |
| } |
| |
| private class AlbumCoverLoader extends BitmapLoader implements EntryUpdater { |
| private MediaItem mMediaItem; |
| private final int mSlotIndex; |
| |
| public AlbumCoverLoader(int slotIndex, MediaItem item) { |
| mSlotIndex = slotIndex; |
| mMediaItem = item; |
| } |
| |
| @Override |
| protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l) { |
| return mThreadPool.submit(mMediaItem.requestImage( |
| MediaItem.TYPE_MICROTHUMBNAIL), l); |
| } |
| |
| @Override |
| protected void onLoadComplete(Bitmap bitmap) { |
| mHandler.obtainMessage(MSG_UPDATE_ALBUM_ENTRY, this).sendToTarget(); |
| } |
| |
| @Override |
| public void updateEntry() { |
| Bitmap bitmap = getBitmap(); |
| if (bitmap == null) return; // error or recycled |
| |
| AlbumSetEntry entry = mData[mSlotIndex % mData.length]; |
| TiledTexture texture = new TiledTexture(bitmap); |
| entry.bitmapTexture = texture; |
| entry.content = texture; |
| |
| if (isActiveSlot(mSlotIndex)) { |
| mContentUploader.addTexture(texture); |
| --mActiveRequestCount; |
| if (mActiveRequestCount == 0) requestNonactiveImages(); |
| if (mListener != null) mListener.onContentChanged(); |
| } else { |
| mContentUploader.addTexture(texture); |
| } |
| } |
| } |
| |
| private static int identifyCacheFlag(MediaSet set) { |
| if (set == null || (set.getSupportedOperations() |
| & MediaSet.SUPPORT_CACHE) == 0) { |
| return MediaSet.CACHE_FLAG_NO; |
| } |
| |
| return set.getCacheFlag(); |
| } |
| |
| private static int identifyCacheStatus(MediaSet set) { |
| if (set == null || (set.getSupportedOperations() |
| & MediaSet.SUPPORT_CACHE) == 0) { |
| return MediaSet.CACHE_STATUS_NOT_CACHED; |
| } |
| |
| return set.getCacheStatus(); |
| } |
| |
| private class AlbumLabelLoader extends BitmapLoader implements EntryUpdater { |
| private final int mSlotIndex; |
| private final String mTitle; |
| private final int mTotalCount; |
| private final int mSourceType; |
| |
| public AlbumLabelLoader( |
| int slotIndex, String title, int totalCount, int sourceType) { |
| mSlotIndex = slotIndex; |
| mTitle = title; |
| mTotalCount = totalCount; |
| mSourceType = sourceType; |
| } |
| |
| @Override |
| protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l) { |
| return mThreadPool.submit(mLabelMaker.requestLabel( |
| mTitle, String.valueOf(mTotalCount), mSourceType), l); |
| } |
| |
| @Override |
| protected void onLoadComplete(Bitmap bitmap) { |
| mHandler.obtainMessage(MSG_UPDATE_ALBUM_ENTRY, this).sendToTarget(); |
| } |
| |
| @Override |
| public void updateEntry() { |
| Bitmap bitmap = getBitmap(); |
| if (bitmap == null) return; // Error or recycled |
| |
| AlbumSetEntry entry = mData[mSlotIndex % mData.length]; |
| BitmapTexture texture = new BitmapTexture(bitmap); |
| texture.setOpaque(false); |
| entry.labelTexture = texture; |
| |
| if (isActiveSlot(mSlotIndex)) { |
| mLabelUploader.addFgTexture(texture); |
| --mActiveRequestCount; |
| if (mActiveRequestCount == 0) requestNonactiveImages(); |
| if (mListener != null) mListener.onContentChanged(); |
| } else { |
| mLabelUploader.addBgTexture(texture); |
| } |
| } |
| } |
| |
| public void onSlotSizeChanged(int width, int height) { |
| if (mSlotWidth == width) return; |
| |
| mSlotWidth = width; |
| mLoadingLabel = null; |
| mLabelMaker.setLabelWidth(mSlotWidth); |
| |
| if (!mIsActive) return; |
| |
| for (int i = mContentStart, n = mContentEnd; i < n; ++i) { |
| AlbumSetEntry entry = mData[i % mData.length]; |
| if (entry.labelLoader != null) { |
| entry.labelLoader.recycle(); |
| entry.labelLoader = null; |
| entry.labelTexture = null; |
| } |
| if (entry.album != null) { |
| entry.labelLoader = new AlbumLabelLoader(i, |
| entry.title, entry.totalCount, entry.sourceType); |
| } |
| } |
| updateAllImageRequests(); |
| updateTextureUploadQueue(); |
| } |
| } |