/* * 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 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); } } private static class UpdateInfo { public long version; public int reloadStart; public int reloadCount; public int size; public ArrayList items; } private class GetUpdateInfo implements Callable { 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 { 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 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(); } } }