blob: a4fd411afe810cbc4cd1a2a359061877b883edc8 [file] [log] [blame]
/*
* 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.graphics.Bitmap;
import android.graphics.BitmapRegionDecoder;
import android.os.Handler;
import android.os.Message;
import com.android.gallery3d.common.BitmapUtils;
import com.android.gallery3d.common.Utils;
import com.android.gallery3d.data.ContentListener;
import com.android.gallery3d.data.DataManager;
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.BitmapScreenNail;
import com.android.gallery3d.ui.PhotoView;
import com.android.gallery3d.ui.ScreenNail;
import com.android.gallery3d.ui.SynchronizedHandler;
import com.android.gallery3d.ui.TileImageViewAdapter;
import com.android.gallery3d.util.Future;
import com.android.gallery3d.util.FutureListener;
import com.android.gallery3d.util.ThreadPool;
import com.android.gallery3d.util.ThreadPool.Job;
import com.android.gallery3d.util.ThreadPool.JobContext;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class PhotoDataAdapter implements PhotoPage.Model {
@SuppressWarnings("unused")
private static final String TAG = "PhotoDataAdapter";
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 = 8;
private static final int DATA_CACHE_SIZE = 32;
private static final int IMAGE_CACHE_SIZE = 5;
private static final int BIT_SCREEN_NAIL = 1;
private static final int BIT_FULL_IMAGE = 2;
private static final long VERSION_OUT_OF_RANGE = MediaObject.nextVersionNumber();
// sImageFetchSeq is the fetching sequence for images.
// We want to fetch the current screennail first (offset = 0), the next
// screennail (offset = +1), then the previous screennail (offset = -1) etc.
// After all the screennail are fetched, we fetch the full images (only some
// of them because of we don't want to use too much memory).
private static ImageFetch[] sImageFetchSeq;
private static class ImageFetch {
int indexOffset;
int imageBit;
public ImageFetch(int offset, int bit) {
indexOffset = offset;
imageBit = bit;
}
}
static {
int k = 0;
sImageFetchSeq = new ImageFetch[1 + (IMAGE_CACHE_SIZE - 1) * 2 + 3];
sImageFetchSeq[k++] = new ImageFetch(0, BIT_SCREEN_NAIL);
for (int i = 1; i < IMAGE_CACHE_SIZE; ++i) {
sImageFetchSeq[k++] = new ImageFetch(i, BIT_SCREEN_NAIL);
sImageFetchSeq[k++] = new ImageFetch(-i, BIT_SCREEN_NAIL);
}
sImageFetchSeq[k++] = new ImageFetch(0, BIT_FULL_IMAGE);
sImageFetchSeq[k++] = new ImageFetch(1, BIT_FULL_IMAGE);
sImageFetchSeq[k++] = new ImageFetch(-1, BIT_FULL_IMAGE);
}
private final TileImageViewAdapter mTileProvider = new TileImageViewAdapter();
// PhotoDataAdapter caches MediaItems (data) and ImageEntries (image).
//
// The MediaItems are stored in the mData array, which has DATA_CACHE_SIZE
// entries. The valid index range are [mContentStart, mContentEnd). We keep
// mContentEnd - mContentStart <= DATA_CACHE_SIZE, so we can use
// (i % DATA_CACHE_SIZE) as index to the array.
//
// The valid MediaItem window size (mContentEnd - mContentStart) may be
// smaller than DATA_CACHE_SIZE because we only update the window and reload
// the MediaItems when there are significant changes to the window position
// (>= MIN_LOAD_COUNT).
private final MediaItem mData[] = new MediaItem[DATA_CACHE_SIZE];
private int mContentStart = 0;
private int mContentEnd = 0;
/*
* The ImageCache is a version-to-ImageEntry map. It only holds
* the ImageEntries in the range of [mActiveStart, mActiveEnd).
* We also keep mActiveEnd - mActiveStart <= IMAGE_CACHE_SIZE.
* Besides, the [mActiveStart, mActiveEnd) range must be contained
* within the[mContentStart, mContentEnd) range.
*/
private HashMap<Long, ImageEntry> mImageCache = new HashMap<Long, ImageEntry>();
private int mActiveStart = 0;
private int mActiveEnd = 0;
// mCurrentIndex is the "center" image the user is viewing. The change of
// mCurrentIndex triggers the data loading and image loading.
private int mCurrentIndex;
// mChanges keeps the version number (of MediaItem) about the previous,
// current, and next image. If the version number changes, we invalidate
// the model. This is used after a database reload or mCurrentIndex changes.
private final long mChanges[] = new long[3];
private final Handler mMainHandler;
private final ThreadPool mThreadPool;
private final PhotoView mPhotoView;
private final MediaSet mSource;
private ReloadTask mReloadTask;
private long mSourceVersion = MediaObject.INVALID_DATA_VERSION;
private int mSize = 0;
private Path mItemPath;
private boolean mIsActive;
public interface DataListener extends LoadingListener {
public void onPhotoAvailable(long version, boolean fullImage);
public void onPhotoChanged(int index, Path item);
}
private DataListener mDataListener;
private final SourceListener mSourceListener = new SourceListener();
// The path of the current viewing item will be stored in mItemPath.
// If mItemPath is not null, mCurrentIndex is only a hint for where we
// can find the item. If mItemPath is null, then we use the mCurrentIndex to
// find the image being viewed.
public PhotoDataAdapter(GalleryActivity activity,
PhotoView view, MediaSet mediaSet, Path itemPath, int indexHint) {
mSource = Utils.checkNotNull(mediaSet);
mPhotoView = Utils.checkNotNull(view);
mItemPath = Utils.checkNotNull(itemPath);
mCurrentIndex = indexHint;
mThreadPool = activity.getThreadPool();
Arrays.fill(mChanges, MediaObject.INVALID_DATA_VERSION);
mMainHandler = new SynchronizedHandler(activity.getGLRoot()) {
@SuppressWarnings("unchecked")
@Override
public void handleMessage(Message message) {
switch (message.what) {
case MSG_RUN_OBJECT:
((Runnable) message.obj).run();
return;
case MSG_LOAD_START: {
if (mDataListener != null) mDataListener.onLoadingStarted();
return;
}
case MSG_LOAD_FINISH: {
if (mDataListener != null) mDataListener.onLoadingFinished();
return;
}
default: throw new AssertionError();
}
}
};
updateSlidingWindow();
}
private long getVersion(int index) {
if (index < 0 || index >= mSize) return VERSION_OUT_OF_RANGE;
if (index >= mContentStart && index < mContentEnd) {
MediaItem item = mData[index % DATA_CACHE_SIZE];
if (item != null) return item.getDataVersion();
}
return MediaObject.INVALID_DATA_VERSION;
}
private void fireModelInvalidated() {
for (int i = -1; i <= 1; ++i) {
long current = getVersion(mCurrentIndex + i);
long change = mChanges[i + 1];
if (current != change) {
mPhotoView.notifyImageInvalidated(i);
mChanges[i + 1] = current;
}
}
}
public void setDataListener(DataListener listener) {
mDataListener = listener;
}
private void updateScreenNail(long version, Future<ScreenNail> future) {
ImageEntry entry = mImageCache.get(version);
ScreenNail screenNail = future.get();
if (entry == null || entry.screenNailTask != future) {
if (screenNail != null) screenNail.recycle();
return;
}
entry.screenNailTask = null;
entry.screenNail = screenNail;
if (screenNail == null) {
entry.failToLoad = true;
}
if (mDataListener != null) {
mDataListener.onPhotoAvailable(version, false);
}
for (int i = -1; i <= 1; ++i) {
if (version == getVersion(mCurrentIndex + i)) {
if (i == 0) updateTileProvider(entry);
mPhotoView.notifyImageInvalidated(i);
}
}
updateImageRequests();
}
private void updateFullImage(long version, Future<BitmapRegionDecoder> future) {
ImageEntry entry = mImageCache.get(version);
if (entry == null || entry.fullImageTask != future) {
BitmapRegionDecoder fullImage = future.get();
if (fullImage != null) fullImage.recycle();
return;
}
entry.fullImageTask = null;
entry.fullImage = future.get();
if (entry.fullImage != null) {
if (mDataListener != null) {
mDataListener.onPhotoAvailable(version, true);
}
if (version == getVersion(mCurrentIndex)) {
updateTileProvider(entry);
mPhotoView.notifyImageInvalidated(0);
}
}
updateImageRequests();
}
public void resume() {
mIsActive = true;
mSource.addContentListener(mSourceListener);
updateImageCache();
updateImageRequests();
mReloadTask = new ReloadTask();
mReloadTask.start();
mPhotoView.notifyModelInvalidated();
}
public void pause() {
mIsActive = false;
mReloadTask.terminate();
mReloadTask = null;
mSource.removeContentListener(mSourceListener);
for (ImageEntry entry : mImageCache.values()) {
if (entry.fullImageTask != null) entry.fullImageTask.cancel();
if (entry.screenNailTask != null) entry.screenNailTask.cancel();
}
mImageCache.clear();
mTileProvider.clear();
}
private ScreenNail getImage(int index) {
if (index < 0 || index >= mSize || !mIsActive) return null;
Utils.assertTrue(index >= mActiveStart && index < mActiveEnd);
ImageEntry entry = mImageCache.get(getVersion(index));
return entry == null ? null : entry.screenNail;
}
public ScreenNail getPrevScreenNail() {
return getImage(mCurrentIndex - 1);
}
public ScreenNail getNextScreenNail() {
return getImage(mCurrentIndex + 1);
}
private void updateCurrentIndex(int index) {
mCurrentIndex = index;
updateSlidingWindow();
MediaItem item = mData[index % DATA_CACHE_SIZE];
mItemPath = item == null ? null : item.getPath();
updateImageCache();
updateImageRequests();
updateTileProvider();
mPhotoView.notifyOnNewImage();
if (mDataListener != null) {
mDataListener.onPhotoChanged(index, mItemPath);
}
fireModelInvalidated();
}
public void next() {
updateCurrentIndex(mCurrentIndex + 1);
}
public void previous() {
updateCurrentIndex(mCurrentIndex - 1);
}
public void jumpTo(int index) {
if (mCurrentIndex == index) return;
updateCurrentIndex(index);
}
public ScreenNail getScreenNail() {
return mTileProvider.getScreenNail();
}
public int getImageHeight() {
return mTileProvider.getImageHeight();
}
public int getImageWidth() {
return mTileProvider.getImageWidth();
}
public int getImageRotation() {
ImageEntry entry = mImageCache.get(getVersion(mCurrentIndex));
return entry == null ? 0 : entry.rotation;
}
public int getLevelCount() {
return mTileProvider.getLevelCount();
}
public Bitmap getTile(int level, int x, int y, int tileSize,
int borderSize) {
return mTileProvider.getTile(level, x, y, tileSize, borderSize);
}
public boolean isFailedToLoad() {
return mTileProvider.isFailedToLoad();
}
public boolean isEmpty() {
return mSize == 0;
}
public int getCurrentIndex() {
return mCurrentIndex;
}
public MediaItem getCurrentMediaItem() {
return mData[mCurrentIndex % DATA_CACHE_SIZE];
}
public void setCurrentPhoto(Path path, int indexHint) {
if (mItemPath == path) return;
mItemPath = path;
mCurrentIndex = indexHint;
updateSlidingWindow();
updateImageCache();
fireModelInvalidated();
// We need to reload content if the path doesn't match.
MediaItem item = getCurrentMediaItem();
if (item != null && item.getPath() != path) {
if (mReloadTask != null) mReloadTask.notifyDirty();
}
}
private void updateTileProvider() {
ImageEntry entry = mImageCache.get(getVersion(mCurrentIndex));
if (entry == null) { // in loading
mTileProvider.clear();
} else {
updateTileProvider(entry);
}
}
private void updateTileProvider(ImageEntry entry) {
ScreenNail screenNail = entry.screenNail;
BitmapRegionDecoder fullImage = entry.fullImage;
if (screenNail != null) {
if (fullImage != null) {
mTileProvider.setScreenNail(screenNail,
fullImage.getWidth(), fullImage.getHeight());
mTileProvider.setRegionDecoder(fullImage);
} else {
int width = screenNail.getWidth();
int height = screenNail.getHeight();
mTileProvider.setScreenNail(screenNail, width, height);
}
} else {
mTileProvider.clear();
if (entry.failToLoad) mTileProvider.setFailedToLoad();
}
}
private void updateSlidingWindow() {
// 1. Update the image window
int start = Utils.clamp(mCurrentIndex - IMAGE_CACHE_SIZE / 2,
0, Math.max(0, mSize - IMAGE_CACHE_SIZE));
int end = Math.min(mSize, start + IMAGE_CACHE_SIZE);
if (mActiveStart == start && mActiveEnd == end) return;
mActiveStart = start;
mActiveEnd = end;
// 2. Update the data window
start = Utils.clamp(mCurrentIndex - DATA_CACHE_SIZE / 2,
0, Math.max(0, mSize - DATA_CACHE_SIZE));
end = Math.min(mSize, start + DATA_CACHE_SIZE);
if (mContentStart > mActiveStart || mContentEnd < mActiveEnd
|| Math.abs(start - mContentStart) > MIN_LOAD_COUNT) {
for (int i = mContentStart; i < mContentEnd; ++i) {
if (i < start || i >= end) {
mData[i % DATA_CACHE_SIZE] = null;
}
}
mContentStart = start;
mContentEnd = end;
if (mReloadTask != null) mReloadTask.notifyDirty();
}
}
private void updateImageRequests() {
if (!mIsActive) return;
int currentIndex = mCurrentIndex;
MediaItem item = mData[currentIndex % DATA_CACHE_SIZE];
if (item == null || item.getPath() != mItemPath) {
// current item mismatch - don't request image
return;
}
// 1. Find the most wanted request and start it (if not already started).
Future<?> task = null;
for (int i = 0; i < sImageFetchSeq.length; i++) {
int offset = sImageFetchSeq[i].indexOffset;
int bit = sImageFetchSeq[i].imageBit;
task = startTaskIfNeeded(currentIndex + offset, bit);
if (task != null) break;
}
// 2. Cancel everything else.
for (ImageEntry entry : mImageCache.values()) {
if (entry.screenNailTask != null && entry.screenNailTask != task) {
entry.screenNailTask.cancel();
entry.screenNailTask = null;
entry.requestedBits &= ~BIT_SCREEN_NAIL;
}
if (entry.fullImageTask != null && entry.fullImageTask != task) {
entry.fullImageTask.cancel();
entry.fullImageTask = null;
entry.requestedBits &= ~BIT_FULL_IMAGE;
}
}
}
private static class ScreenNailJob implements Job<ScreenNail> {
private MediaItem mItem;
public ScreenNailJob(MediaItem item) {
mItem = item;
}
@Override
public ScreenNail run(JobContext jc) {
// We try to get a ScreenNail first, if it fails, we fallback to get
// a Bitmap and then wrap it in a BitmapScreenNail instead.
ScreenNail s = mItem.getScreenNail();
if (s != null) return s;
Bitmap bitmap = mItem.requestImage(MediaItem.TYPE_THUMBNAIL).run(jc);
if (jc.isCancelled()) return null;
if (bitmap != null) {
bitmap = BitmapUtils.rotateBitmap(bitmap,
mItem.getRotation() - mItem.getFullImageRotation(), true);
}
return new BitmapScreenNail(bitmap, mItem.getFullImageRotation());
}
}
// Returns the task if we started the task or the task is already started.
private Future<?> startTaskIfNeeded(int index, int which) {
if (index < mActiveStart || index >= mActiveEnd) return null;
ImageEntry entry = mImageCache.get(getVersion(index));
if (entry == null) return null;
if (which == BIT_SCREEN_NAIL && entry.screenNailTask != null) {
return entry.screenNailTask;
} else if (which == BIT_FULL_IMAGE && entry.fullImageTask != null) {
return entry.fullImageTask;
}
MediaItem item = mData[index % DATA_CACHE_SIZE];
Utils.assertTrue(item != null);
if (which == BIT_SCREEN_NAIL
&& (entry.requestedBits & BIT_SCREEN_NAIL) == 0) {
entry.requestedBits |= BIT_SCREEN_NAIL;
entry.screenNailTask = mThreadPool.submit(
new ScreenNailJob(item),
new ScreenNailListener(item.getDataVersion()));
// request screen nail
return entry.screenNailTask;
}
if (which == BIT_FULL_IMAGE
&& (entry.requestedBits & BIT_FULL_IMAGE) == 0
&& (item.getSupportedOperations()
& MediaItem.SUPPORT_FULL_IMAGE) != 0) {
entry.requestedBits |= BIT_FULL_IMAGE;
entry.fullImageTask = mThreadPool.submit(
item.requestLargeImage(),
new FullImageListener(item.getDataVersion()));
// request full image
return entry.fullImageTask;
}
return null;
}
private void updateImageCache() {
HashSet<Long> toBeRemoved = new HashSet<Long>(mImageCache.keySet());
for (int i = mActiveStart; i < mActiveEnd; ++i) {
MediaItem item = mData[i % DATA_CACHE_SIZE];
long version = item == null
? MediaObject.INVALID_DATA_VERSION
: item.getDataVersion();
if (version == MediaObject.INVALID_DATA_VERSION) continue;
ImageEntry entry = mImageCache.get(version);
toBeRemoved.remove(version);
if (entry != null) {
if (Math.abs(i - mCurrentIndex) > 1) {
if (entry.fullImageTask != null) {
entry.fullImageTask.cancel();
entry.fullImageTask = null;
}
entry.fullImage = null;
entry.requestedBits &= ~BIT_FULL_IMAGE;
}
} else {
entry = new ImageEntry();
entry.rotation = item.getFullImageRotation();
mImageCache.put(version, entry);
}
}
// Clear the data and requests for ImageEntries outside the new window.
for (Long version : toBeRemoved) {
ImageEntry entry = mImageCache.remove(version);
if (entry.fullImageTask != null) entry.fullImageTask.cancel();
if (entry.screenNailTask != null) entry.screenNailTask.cancel();
}
}
private class FullImageListener
implements Runnable, FutureListener<BitmapRegionDecoder> {
private final long mVersion;
private Future<BitmapRegionDecoder> mFuture;
public FullImageListener(long version) {
mVersion = version;
}
@Override
public void onFutureDone(Future<BitmapRegionDecoder> future) {
mFuture = future;
mMainHandler.sendMessage(
mMainHandler.obtainMessage(MSG_RUN_OBJECT, this));
}
@Override
public void run() {
updateFullImage(mVersion, mFuture);
}
}
private class ScreenNailListener
implements Runnable, FutureListener<ScreenNail> {
private final long mVersion;
private Future<ScreenNail> mFuture;
public ScreenNailListener(long version) {
mVersion = version;
}
@Override
public void onFutureDone(Future<ScreenNail> future) {
mFuture = future;
mMainHandler.sendMessage(
mMainHandler.obtainMessage(MSG_RUN_OBJECT, this));
}
@Override
public void run() {
updateScreenNail(mVersion, mFuture);
}
}
private static class ImageEntry {
public int requestedBits = 0;
public int rotation;
public BitmapRegionDecoder fullImage;
public ScreenNail screenNail;
public Future<ScreenNail> screenNailTask;
public Future<BitmapRegionDecoder> fullImageTask;
public boolean failToLoad = false;
}
private class SourceListener implements ContentListener {
public void onContentDirty() {
if (mReloadTask != null) mReloadTask.notifyDirty();
}
}
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 boolean reloadContent;
public Path target;
public int indexHint;
public int contentStart;
public int contentEnd;
public int size;
public ArrayList<MediaItem> items;
}
private class GetUpdateInfo implements Callable<UpdateInfo> {
private boolean needContentReload() {
for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
if (mData[i % DATA_CACHE_SIZE] == null) return true;
}
MediaItem current = mData[mCurrentIndex % DATA_CACHE_SIZE];
return current == null || current.getPath() != mItemPath;
}
@Override
public UpdateInfo call() throws Exception {
// TODO: Try to load some data in first update
UpdateInfo info = new UpdateInfo();
info.version = mSourceVersion;
info.reloadContent = needContentReload();
info.target = mItemPath;
info.indexHint = mCurrentIndex;
info.contentStart = mContentStart;
info.contentEnd = mContentEnd;
info.size = mSize;
return info;
}
}
private class UpdateContent implements Callable<Void> {
UpdateInfo mUpdateInfo;
public UpdateContent(UpdateInfo updateInfo) {
mUpdateInfo = updateInfo;
}
@Override
public Void call() throws Exception {
UpdateInfo info = mUpdateInfo;
mSourceVersion = info.version;
if (info.size != mSize) {
mSize = info.size;
if (mContentEnd > mSize) mContentEnd = mSize;
if (mActiveEnd > mSize) mActiveEnd = mSize;
}
if (info.indexHint == MediaSet.INDEX_NOT_FOUND) {
// The image has been deleted, clear mItemPath, the
// mCurrentIndex will be updated in the updateCurrentItem().
mItemPath = null;
updateCurrentItem();
} else {
mCurrentIndex = info.indexHint;
}
updateSlidingWindow();
if (info.items != null) {
int start = Math.max(info.contentStart, mContentStart);
int end = Math.min(info.contentStart + info.items.size(), mContentEnd);
int dataIndex = start % DATA_CACHE_SIZE;
for (int i = start; i < end; ++i) {
mData[dataIndex] = info.items.get(i - info.contentStart);
if (++dataIndex == DATA_CACHE_SIZE) dataIndex = 0;
}
}
if (mItemPath == null) {
MediaItem current = mData[mCurrentIndex % DATA_CACHE_SIZE];
mItemPath = current == null ? null : current.getPath();
}
updateImageCache();
updateTileProvider();
updateImageRequests();
fireModelInvalidated();
return null;
}
private void updateCurrentItem() {
if (mSize == 0) return;
if (mCurrentIndex >= mSize) {
mCurrentIndex = mSize - 1;
mPhotoView.notifyOnNewImage();
mPhotoView.startSlideInAnimation(PhotoView.TRANS_SLIDE_IN_LEFT);
} else {
mPhotoView.notifyOnNewImage();
mPhotoView.startSlideInAnimation(PhotoView.TRANS_SLIDE_IN_RIGHT);
}
}
}
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() {
while (mActive) {
synchronized (this) {
if (!mDirty && mActive) {
updateLoading(false);
Utils.waitWithoutInterrupt(this);
continue;
}
}
mDirty = false;
UpdateInfo info = executeAndWait(new GetUpdateInfo());
synchronized (DataManager.LOCK) {
updateLoading(true);
long version = mSource.reload();
if (info.version != version) {
info.reloadContent = true;
info.size = mSource.getMediaItemCount();
}
if (!info.reloadContent) continue;
info.items = mSource.getMediaItem(info.contentStart, info.contentEnd);
MediaItem item = findCurrentMediaItem(info);
if (item == null || item.getPath() != info.target) {
info.indexHint = findIndexOfTarget(info);
}
}
executeAndWait(new UpdateContent(info));
}
}
public synchronized void notifyDirty() {
mDirty = true;
notifyAll();
}
public synchronized void terminate() {
mActive = false;
notifyAll();
}
private MediaItem findCurrentMediaItem(UpdateInfo info) {
ArrayList<MediaItem> items = info.items;
int index = info.indexHint - info.contentStart;
return index < 0 || index >= items.size() ? null : items.get(index);
}
private int findIndexOfTarget(UpdateInfo info) {
if (info.target == null) return info.indexHint;
ArrayList<MediaItem> items = info.items;
// First, try to find the item in the data just loaded
if (items != null) {
for (int i = 0, n = items.size(); i < n; ++i) {
if (items.get(i).getPath() == info.target) return i + info.contentStart;
}
}
// Not found, find it in mSource.
return mSource.getIndexOfItem(info.target, info.indexHint);
}
}
}