/*
* 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.app;
import android.os.Handler;
import android.os.Message;
import android.os.Process;
import com.android.gallery3d.common.Utils;
import com.android.gallery3d.data.ContentListener;
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.ui.SynchronizedHandler;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class AlbumDataLoader {
@SuppressWarnings("unused")
private static final String TAG = "AlbumDataAdapter";
private static final int DATA_CACHE_SIZE = 1000;
private static final int MSG_LOAD_START = 1;
private static final int MSG_LOAD_FINISH = 2;
private static final int MSG_RUN_OBJECT = 3;
private static final int MIN_LOAD_COUNT = 32;
private static final int MAX_LOAD_COUNT = 64;
private final MediaItem[] mData;
private final long[] mItemVersion;
private final long[] mSetVersion;
public static interface DataListener {
public void onContentChanged(int index);
public void onSizeChanged(int size);
}
private int mActiveStart = 0;
private int mActiveEnd = 0;
private int mContentStart = 0;
private int mContentEnd = 0;
private final MediaSet mSource;
private long mSourceVersion = MediaObject.INVALID_DATA_VERSION;
private final Handler mMainHandler;
private int mSize = 0;
private DataListener mDataListener;
private MySourceListener mSourceListener = new MySourceListener();
private LoadingListener mLoadingListener;
private ReloadTask mReloadTask;
// the data version on which last loading failed
private long mFailedVersion = MediaObject.INVALID_DATA_VERSION;
public AlbumDataLoader(AbstractGalleryActivity context, MediaSet mediaSet) {
mSource = mediaSet;
mData = new MediaItem[DATA_CACHE_SIZE];
mItemVersion = new long[DATA_CACHE_SIZE];
mSetVersion = new long[DATA_CACHE_SIZE];
Arrays.fill(mItemVersion, MediaObject.INVALID_DATA_VERSION);
Arrays.fill(mSetVersion, MediaObject.INVALID_DATA_VERSION);
mMainHandler = new SynchronizedHandler(context.getGLRoot()) {
@Override
public void handleMessage(Message message) {
switch (message.what) {
case MSG_RUN_OBJECT:
((Runnable) message.obj).run();
return;
case MSG_LOAD_START:
if (mLoadingListener != null) mLoadingListener.onLoadingStarted();
return;
case MSG_LOAD_FINISH:
if (mLoadingListener != null) {
boolean loadingFailed =
(mFailedVersion != MediaObject.INVALID_DATA_VERSION);
mLoadingListener.onLoadingFinished(loadingFailed);
}
return;
}
}
};
}
public void resume() {
mSource.addContentListener(mSourceListener);
mReloadTask = new ReloadTask();
mReloadTask.start();
}
public void pause() {
mReloadTask.terminate();
mReloadTask = null;
mSource.removeContentListener(mSourceListener);
}
public MediaItem get(int index) {
if (!isActive(index)) {
return mSource.getMediaItem(index, 1).get(0);
}
return mData[index % mData.length];
}
public int getActiveStart() {
return mActiveStart;
}
public boolean isActive(int index) {
return index >= mActiveStart && index < mActiveEnd;
}
public int size() {
return mSize;
}
// Returns the index of the MediaItem with the given path or
// -1 if the path is not cached
public int findItem(Path id) {
for (int i = mContentStart; i < mContentEnd; i++) {
MediaItem item = mData[i % DATA_CACHE_SIZE];
if (item != null && id == item.getPath()) {
return i;
}
}
return -1;
}
private void clearSlot(int slotIndex) {
mData[slotIndex] = null;
mItemVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION;
mSetVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION;
}
private void setContentWindow(int contentStart, int contentEnd) {
if (contentStart == mContentStart && contentEnd == mContentEnd) return;
int end = mContentEnd;
int start = mContentStart;
// We need change the content window before calling reloadData(...)
synchronized (this) {
mContentStart = contentStart;
mContentEnd = contentEnd;
}
long[] itemVersion = mItemVersion;
long[] setVersion = mSetVersion;
if (contentStart >= end || start >= contentEnd) {
for (int i = start, n = end; i < n; ++i) {
clearSlot(i % DATA_CACHE_SIZE);
}
} else {
for (int i = start; i < contentStart; ++i) {
clearSlot(i % DATA_CACHE_SIZE);
}
for (int i = contentEnd, n = end; i < n; ++i) {
clearSlot(i % DATA_CACHE_SIZE);
}
}
if (mReloadTask != null) mReloadTask.notifyDirty();
}
public void setActiveWindow(int start, int end) {
if (start == mActiveStart && end == mActiveEnd) return;
Utils.assertTrue(start <= end
&& end - start <= mData.length && end <= mSize);
int length = mData.length;
mActiveStart = start;
mActiveEnd = end;
// If no data is visible, keep the cache content
if (start == end) return;
int contentStart = Utils.clamp((start + end) / 2 - length / 2,
0, Math.max(0, mSize - length));
int contentEnd = Math.min(contentStart + length, mSize);
if (mContentStart > start || mContentEnd < end
|| Math.abs(contentStart - mContentStart) > MIN_LOAD_COUNT) {
setContentWindow(contentStart, contentEnd);
}
}
private class MySourceListener implements ContentListener {
@Override
public void onContentDirty() {
if (mReloadTask != null) mReloadTask.notifyDirty();
}
}
public void setDataListener(DataListener listener) {
mDataListener = listener;
}
public void setLoadingListener(LoadingListener listener) {
mLoadingListener = listener;
}
private <T> T executeAndWait(Callable<T> callable) {
FutureTask<T> task = new FutureTask<T>(callable);
mMainHandler.sendMessage(
mMainHandler.obtainMessage(MSG_RUN_OBJECT, task));
try {
return task.get();
} catch (InterruptedException e) {
return null;
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
private static class UpdateInfo {
public long version;
public int reloadStart;
public int reloadCount;
public int size;
public ArrayList<MediaItem> items;
}
private class GetUpdateInfo implements Callable<UpdateInfo> {
private final long mVersion;
public GetUpdateInfo(long version) {
mVersion = version;
}
@Override
public UpdateInfo call() throws Exception {
if (mFailedVersion == mVersion) {
// previous loading failed, return null to pause loading
return null;
}
UpdateInfo info = new UpdateInfo();
long version = mVersion;
info.version = mSourceVersion;
info.size = mSize;
long setVersion[] = mSetVersion;
for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
int index = i % DATA_CACHE_SIZE;
if (setVersion[index] != version) {
info.reloadStart = i;
info.reloadCount = Math.min(MAX_LOAD_COUNT, n - i);
return info;
}
}
return mSourceVersion == mVersion ? null : info;
}
}
private class UpdateContent implements Callable<Void> {
private UpdateInfo mUpdateInfo;
public UpdateContent(UpdateInfo info) {
mUpdateInfo = info;
}
@Override
public Void call() throws Exception {
UpdateInfo info = mUpdateInfo;
mSourceVersion = info.version;
if (mSize != info.size) {
mSize = info.size;
if (mDataListener != null) mDataListener.onSizeChanged(mSize);
if (mContentEnd > mSize) mContentEnd = mSize;
if (mActiveEnd > mSize) mActiveEnd = mSize;
}
ArrayList<MediaItem> items = info.items;
mFailedVersion = MediaObject.INVALID_DATA_VERSION;
if ((items == null) || items.isEmpty()) {
if (info.reloadCount > 0) {
mFailedVersion = info.version;
Log.d(TAG, "loading failed: " + mFailedVersion);
}
return null;
}
int start = Math.max(info.reloadStart, mContentStart);
int end = Math.min(info.reloadStart + items.size(), mContentEnd);
for (int i = start; i < end; ++i) {
int index = i % DATA_CACHE_SIZE;
mSetVersion[index] = info.version;
MediaItem updateItem = items.get(i - info.reloadStart);
long itemVersion = updateItem.getDataVersion();
if (mItemVersion[index] != itemVersion) {
mItemVersion[index] = itemVersion;
mData[index] = updateItem;
if (mDataListener != null && i >= mActiveStart && i < mActiveEnd) {
mDataListener.onContentChanged(i);
}
}
}
return null;
}
}
/*
* The thread model of ReloadTask
* *
* [Reload Task] [Main Thread]
* | |
* getUpdateInfo() --> | (synchronous call)
* (wait) <---- getUpdateInfo()
* | |
* Load Data |
* | |
* updateContent() --> | (synchronous call)
* (wait) updateContent()
* | |
* | |
*/
private class ReloadTask extends Thread {
private volatile boolean mActive = true;
private volatile boolean mDirty = true;
private boolean mIsLoading = false;
private void updateLoading(boolean loading) {
if (mIsLoading == loading) return;
mIsLoading = loading;
mMainHandler.sendEmptyMessage(loading ? MSG_LOAD_START : MSG_LOAD_FINISH);
}
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
boolean updateComplete = false;
while (mActive) {
synchronized (this) {
if (mActive && !mDirty && updateComplete) {
updateLoading(false);
if (mFailedVersion != MediaObject.INVALID_DATA_VERSION) {
Log.d(TAG, "reload pause");
}
Utils.waitWithoutInterrupt(this);
if (mActive && (mFailedVersion != MediaObject.INVALID_DATA_VERSION)) {
Log.d(TAG, "reload resume");
}
continue;
}
mDirty = false;
}
updateLoading(true);
long version = mSource.reload();
UpdateInfo info = executeAndWait(new GetUpdateInfo(version));
updateComplete = info == null;
if (updateComplete) continue;
if (info.version != version) {
info.size = mSource.getMediaItemCount();
info.version = version;
}
if (info.reloadCount > 0) {
info.items = mSource.getMediaItem(info.reloadStart, info.reloadCount);
}
executeAndWait(new UpdateContent(info));
}
updateLoading(false);
}
public synchronized void notifyDirty() {
mDirty = true;
notifyAll();
}
public synchronized void terminate() {
mActive = false;
notifyAll();
}
}
}