/*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.fastergallery.ui;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import android.graphics.Bitmap;
import android.os.Message;
import android.text.TextUtils;
import com.android.fastergallery.app.AbstractGalleryActivity;
import com.android.fastergallery.app.AlbumSetDataLoader;
import com.android.fastergallery.app.FilterUtils;
import com.android.fastergallery.app.AlbumSetDataLoader.AlbumSetCoverItem;
import com.android.fastergallery.common.Utils;
import com.android.fastergallery.data.DataManager;
import com.android.fastergallery.data.DataSourceType;
import com.android.fastergallery.data.LocalAlbum;
import com.android.fastergallery.data.LocalImage;
import com.android.fastergallery.data.MediaDetails;
import com.android.fastergallery.data.MediaItem;
import com.android.fastergallery.data.MediaObject;
import com.android.fastergallery.data.MediaSet;
import com.android.fastergallery.data.Path;
import com.android.fastergallery.glrenderer.BitmapTexture;
import com.android.fastergallery.glrenderer.Texture;
import com.android.fastergallery.glrenderer.TextureUploader;
import com.android.fastergallery.glrenderer.TiledTexture;
import com.android.fastergallery.util.Future;
import com.android.fastergallery.util.FutureListener;
import com.android.fastergallery.util.ThreadPool;
import com.android.fastergallery.R;
public class AlbumSetTypeSlidingWindow 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 AlbumTypeLabelMaker 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 AlbumSetCoverItem 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 AlbumSetTypeSlidingWindow(AbstractGalleryActivity activity,
AlbumSetDataLoader source,
AlbumSetTypeSlotRenderer.LabelSpec labelSpec, int cacheSize) {
source.setModelListener(this);
mSource = source;
mData = new AlbumSetEntry[cacheSize];
mSize = source.size();
mThreadPool = activity.getThreadPool();
mLabelMaker = new AlbumTypeLabelMaker(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 static long getCoverDataVersion(AlbumSetCoverItem object) {
return object == null ? MediaSet.INVALID_DATA_VERSION : object.getItem()
.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 String getLocalFilePath(MediaSet album) {
if (album == null) {
return "";
}
String path = "";
int viewType = AlbumSetTypeManager.get().getCurrentType();
if (FilterUtils.CLUSTER_BY_LIST != viewType) {
return path;
}
MediaItem item = album.getCoverMediaItem();
if (item != null && item instanceof LocalImage) {
path = new File(((LocalImage)item).filePath).getParent();
}
return path;
}
private String formatTime(long time) {
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date curDate = new Date(time);
return formatter.format(curDate);
}
private void updateAlbumSetEntry(AlbumSetEntry entry, int slotIndex) {
MediaSet album = mSource.getMediaSet(slotIndex);
AlbumSetCoverItem cover = mSource.getCoverItems(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) {
String albumPath = getLocalFilePath(album);
String albumDate = "";
if (!TextUtils.isEmpty(albumPath)) {
albumDate = formatTime(new File(albumPath).lastModified());
}
entry.labelLoader = new AlbumLabelLoader(slotIndex, title,
totalCount, sourceType, albumPath, albumDate);
}
}
entry.coverItem = cover;
if (getCoverDataVersion(cover) != entry.coverDataVersion) {
entry.coverDataVersion = getCoverDataVersion(cover);
entry.rotation = (cover == null) ? 0 : cover.getItem().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,
AlbumSetTypeManager.get().getCurrentType()).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 AlbumSetCoverItem mMediaItems;
private final int mSlotIndex;
private int mCurrentItemIndex;
public AlbumCoverLoader(int slotIndex, AlbumSetCoverItem items) {
mSlotIndex = slotIndex;
mMediaItems = items;
mCurrentItemIndex = 0;
}
@Override
protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l) {
return mThreadPool.submit(
mMediaItems.getItem().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;
++mCurrentItemIndex;
if (isActiveSlot(mSlotIndex)) {
mContentUploader.addTexture(texture);
--mActiveRequestCount;
if (mActiveRequestCount == 0)
requestNonactiveImages();
if (mListener != null)
mListener.onContentChanged();
} else {
mContentUploader.addTexture(texture);
}
}
}
private class AlbumTypeCoverLoader extends BitmapLoader implements EntryUpdater {
private AlbumSetCoverItem mMediaItems;
private final int mSlotIndex;
private int mCurrentItemIndex;
public AlbumTypeCoverLoader(int slotIndex, AlbumSetCoverItem items) {
mSlotIndex = slotIndex;
mMediaItems = items;
mCurrentItemIndex = 0;
}
@Override
protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l) {
return mThreadPool.submit(
mMediaItems.getItem().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;
++mCurrentItemIndex;
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 String mFilePath;
private final String mFileDate;
private final int mTotalCount;
private final int mSourceType;
public AlbumLabelLoader(int slotIndex, String title, int totalCount,
int sourceType) {
this(slotIndex, title, totalCount, sourceType, "", "");
}
public AlbumLabelLoader(int slotIndex, String title, int totalCount,
int sourceType, String filePath, String fileDate) {
mSlotIndex = slotIndex;
mTitle = title;
mFilePath = filePath;
mFileDate = fileDate;
mTotalCount = totalCount;
mSourceType = sourceType;
}
@Override
protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l) {
return mThreadPool.submit(
mLabelMaker.requestLabel(mTitle,
"sdcard/test1",
String.valueOf(mTotalCount), mFilePath, mFileDate, mSourceType,
AlbumSetTypeManager.get().getCurrentType()), 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, height);
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();
}
}