/* * 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 { 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 { 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 executeAndWait(Callable callable) { FutureTask task = new FutureTask(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(); } } }