diff options
Diffstat (limited to 'src/com/android/gallery3d/app/AlbumSetDataLoader.java')
-rw-r--r-- | src/com/android/gallery3d/app/AlbumSetDataLoader.java | 393 |
1 files changed, 393 insertions, 0 deletions
diff --git a/src/com/android/gallery3d/app/AlbumSetDataLoader.java b/src/com/android/gallery3d/app/AlbumSetDataLoader.java new file mode 100644 index 000000000..cf380f812 --- /dev/null +++ b/src/com/android/gallery3d/app/AlbumSetDataLoader.java @@ -0,0 +1,393 @@ +/* + * 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.Arrays; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; + +public class AlbumSetDataLoader { + @SuppressWarnings("unused") + private static final String TAG = "AlbumSetDataAdapter"; + + private static final int INDEX_NONE = -1; + + private static final int MIN_LOAD_COUNT = 4; + + private static final int MSG_LOAD_START = 1; + private static final int MSG_LOAD_FINISH = 2; + private static final int MSG_RUN_OBJECT = 3; + + public static interface DataListener { + public void onContentChanged(int index); + public void onSizeChanged(int size); + } + + private final MediaSet[] mData; + private final MediaItem[] mCoverItem; + private final int[] mTotalCount; + private final long[] mItemVersion; + private final long[] mSetVersion; + + 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 int mSize; + + private DataListener mDataListener; + private LoadingListener mLoadingListener; + private ReloadTask mReloadTask; + + private final Handler mMainHandler; + + private final MySourceListener mSourceListener = new MySourceListener(); + + public AlbumSetDataLoader(AbstractGalleryActivity activity, MediaSet albumSet, int cacheSize) { + mSource = Utils.checkNotNull(albumSet); + mCoverItem = new MediaItem[cacheSize]; + mData = new MediaSet[cacheSize]; + mTotalCount = new int[cacheSize]; + mItemVersion = new long[cacheSize]; + mSetVersion = new long[cacheSize]; + Arrays.fill(mItemVersion, MediaObject.INVALID_DATA_VERSION); + Arrays.fill(mSetVersion, MediaObject.INVALID_DATA_VERSION); + + mMainHandler = new SynchronizedHandler(activity.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) mLoadingListener.onLoadingFinished(false); + return; + } + } + }; + } + + public void pause() { + mReloadTask.terminate(); + mReloadTask = null; + mSource.removeContentListener(mSourceListener); + } + + public void resume() { + mSource.addContentListener(mSourceListener); + mReloadTask = new ReloadTask(); + mReloadTask.start(); + } + + private void assertIsActive(int index) { + if (index < mActiveStart && index >= mActiveEnd) { + throw new IllegalArgumentException(String.format( + "%s not in (%s, %s)", index, mActiveStart, mActiveEnd)); + } + } + + public MediaSet getMediaSet(int index) { + assertIsActive(index); + return mData[index % mData.length]; + } + + public MediaItem getCoverItem(int index) { + assertIsActive(index); + return mCoverItem[index % mCoverItem.length]; + } + + public int getTotalCount(int index) { + assertIsActive(index); + return mTotalCount[index % mTotalCount.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 MediaSet with the given path or + // -1 if the path is not cached + public int findSet(Path id) { + int length = mData.length; + for (int i = mContentStart; i < mContentEnd; i++) { + MediaSet set = mData[i % length]; + if (set != null && id == set.getPath()) { + return i; + } + } + return -1; + } + + private void clearSlot(int slotIndex) { + mData[slotIndex] = null; + mCoverItem[slotIndex] = null; + mTotalCount[slotIndex] = 0; + 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 length = mCoverItem.length; + + int start = this.mContentStart; + int end = this.mContentEnd; + + mContentStart = contentStart; + mContentEnd = contentEnd; + + if (contentStart >= end || start >= contentEnd) { + for (int i = start, n = end; i < n; ++i) { + clearSlot(i % length); + } + } else { + for (int i = start; i < contentStart; ++i) { + clearSlot(i % length); + } + for (int i = contentEnd, n = end; i < n; ++i) { + clearSlot(i % length); + } + } + mReloadTask.notifyDirty(); + } + + public void setActiveWindow(int start, int end) { + if (start == mActiveStart && end == mActiveEnd) return; + + Utils.assertTrue(start <= end + && end - start <= mCoverItem.length && end <= mSize); + + mActiveStart = start; + mActiveEnd = end; + + int length = mCoverItem.length; + // 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() { + mReloadTask.notifyDirty(); + } + } + + public void setModelListener(DataListener listener) { + mDataListener = listener; + } + + public void setLoadingListener(LoadingListener listener) { + mLoadingListener = listener; + } + + private static class UpdateInfo { + public long version; + public int index; + + public int size; + public MediaSet item; + public MediaItem cover; + public int totalCount; + } + + private class GetUpdateInfo implements Callable<UpdateInfo> { + + private final long mVersion; + + public GetUpdateInfo(long version) { + mVersion = version; + } + + private int getInvalidIndex(long version) { + long setVersion[] = mSetVersion; + int length = setVersion.length; + for (int i = mContentStart, n = mContentEnd; i < n; ++i) { + int index = i % length; + if (setVersion[i % length] != version) return i; + } + return INDEX_NONE; + } + + @Override + public UpdateInfo call() throws Exception { + int index = getInvalidIndex(mVersion); + if (index == INDEX_NONE && mSourceVersion == mVersion) return null; + UpdateInfo info = new UpdateInfo(); + info.version = mSourceVersion; + info.index = index; + info.size = mSize; + return info; + } + } + + private class UpdateContent implements Callable<Void> { + private final UpdateInfo mUpdateInfo; + + public UpdateContent(UpdateInfo info) { + mUpdateInfo = info; + } + + @Override + public Void call() { + // Avoid notifying listeners of status change after pause + // Otherwise gallery will be in inconsistent state after resume. + if (mReloadTask == null) return null; + 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; + } + // Note: info.index could be INDEX_NONE, i.e., -1 + if (info.index >= mContentStart && info.index < mContentEnd) { + int pos = info.index % mCoverItem.length; + mSetVersion[pos] = info.version; + long itemVersion = info.item.getDataVersion(); + if (mItemVersion[pos] == itemVersion) return null; + mItemVersion[pos] = itemVersion; + mData[pos] = info.item; + mCoverItem[pos] = info.cover; + mTotalCount[pos] = info.totalCount; + if (mDataListener != null + && info.index >= mActiveStart && info.index < mActiveEnd) { + mDataListener.onContentChanged(info.index); + } + } + return null; + } + } + + 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); + } + } + + // TODO: load active range first + private class ReloadTask extends Thread { + private volatile boolean mActive = true; + private volatile boolean mDirty = true; + private volatile 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) { + if (!mSource.isLoading()) updateLoading(false); + Utils.waitWithoutInterrupt(this); + 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.version = version; + info.size = mSource.getSubMediaSetCount(); + + // If the size becomes smaller after reload(), we may + // receive from GetUpdateInfo an index which is too + // big. Because the main thread is not aware of the size + // change until we call UpdateContent. + if (info.index >= info.size) { + info.index = INDEX_NONE; + } + } + if (info.index != INDEX_NONE) { + info.item = mSource.getSubMediaSet(info.index); + if (info.item == null) continue; + info.cover = info.item.getCoverMediaItem(); + info.totalCount = info.item.getTotalMediaItemCount(); + } + executeAndWait(new UpdateContent(info)); + } + updateLoading(false); + } + + public synchronized void notifyDirty() { + mDirty = true; + notifyAll(); + } + + public synchronized void terminate() { + mActive = false; + notifyAll(); + } + } +} + + |