| /* |
| * 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 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 ArrayList<DataListener> mDataListener = new ArrayList<>(); |
| 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.add(listener); |
| } |
| |
| public void removeModelListener(DataListener listener) { |
| mDataListener.remove(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) |
| for (DataListener l : mDataListener) { |
| l.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) { |
| for (DataListener l : mDataListener) { |
| l.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(); |
| } |
| } |
| } |
| |
| |