@Override
 public MediaDetails getDetails() {
   MediaObject item = mAlbumSetDataAdapter.getMediaSet(mIndex);
   if (item != null) {
     mAlbumSetView.setHighlightItemPath(item.getPath());
     return item.getDetails();
   } else {
     return null;
   }
 }
 @Override
 public MediaDetails getDetails() {
   // this relies on setIndex() being called beforehand
   MediaObject item = mAlbumDataAdapter.get(mIndex);
   if (item != null) {
     mAlbumView.setHighlightItemPath(item.getPath());
     return item.getDetails();
   } else {
     return null;
   }
 }
Exemplo n.º 3
0
  private boolean execute(DataManager manager, JobContext jc, int cmd, Path path) {
    boolean result = true;
    Log.v(TAG, "Execute cmd: " + cmd + " for " + path);
    long startTime = System.currentTimeMillis();

    switch (cmd) {
      case R.id.action_delete:
        manager.delete(path);
        break;
      case R.id.action_rotate_cw:
        manager.rotate(path, 90);
        break;
      case R.id.action_rotate_ccw:
        manager.rotate(path, -90);
        break;
      case R.id.action_toggle_full_caching:
        {
          MediaObject obj = manager.getMediaObject(path);
          int cacheFlag = obj.getCacheFlag();
          if (cacheFlag == MediaObject.CACHE_FLAG_FULL) {
            cacheFlag = MediaObject.CACHE_FLAG_SCREENNAIL;
          } else {
            cacheFlag = MediaObject.CACHE_FLAG_FULL;
          }
          obj.cache(cacheFlag);
          break;
        }
      case R.id.action_show_on_map:
        {
          MediaItem item = (MediaItem) manager.getMediaObject(path);
          double latlng[] = new double[2];
          item.getLatLong(latlng);
          if (GalleryUtils.isValidLocation(latlng[0], latlng[1])) {
            GalleryUtils.showOnMap((Context) mActivity, latlng[0], latlng[1]);
          }
          break;
        }
      case R.id.action_import:
        {
          MediaObject obj = manager.getMediaObject(path);
          result = obj.Import();
          break;
        }
      default:
        throw new AssertionError();
    }
    Log.v(
        TAG,
        "It takes " + (System.currentTimeMillis() - startTime) + " ms to execute cmd for " + path);
    return result;
  }
Exemplo n.º 4
0
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<Bitmap> future) {
    ImageEntry entry = mImageCache.get(version);
    if (entry == null || entry.screenNailTask != future) {
      Bitmap screenNail = future.get();
      if (screenNail != null) screenNail.recycle();
      return;
    }

    entry.screenNailTask = null;
    entry.screenNail = future.get();

    if (entry.screenNail == null) {
      entry.failToLoad = true;
      /*a@nufront start*/
      for (int i = -1; i <= 1; ++i) {
        if (version == getVersion(mCurrentIndex + i)) {
          if (0 == i) updateTileProvider(entry);
          mPhotoView.notifyImageInvalidated(i);
        }
      }
      /*a@nufront end*/
    } else {
      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 ImageData getImage(int index) {
    if (index < 0 || index >= mSize || !mIsActive) return null;
    Utils.assertTrue(index >= mActiveStart && index < mActiveEnd);

    ImageEntry entry = mImageCache.get(getVersion(index));
    Bitmap screennail = entry == null ? null : entry.screenNail;
    if (screennail != null) {
      return new ImageData(screennail, entry.rotation);
    } else {
      return new ImageData(null, 0);
    }
  }

  public ImageData getPreviousImage() {
    return getImage(mCurrentIndex - 1);
  }

  public ImageData getNextImage() {
    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 Bitmap getBackupImage() {
    return mTileProvider.getBackupImage();
  }

  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) {
    return mTileProvider.getTile(level, x, y, tileSize);
  }

  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) {
    Bitmap screenNail = entry.screenNail;
    BitmapRegionDecoder fullImage = entry.fullImage;
    if (screenNail != null) {
      if (fullImage != null) {
        mTileProvider.setBackupImage(screenNail, fullImage.getWidth(), fullImage.getHeight());
        mTileProvider.setRegionDecoder(fullImage);
      } else {
        int width = screenNail.getWidth();
        int height = screenNail.getHeight();
        mTileProvider.setBackupImage(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<Bitmap> {
    private MediaItem mItem;

    public ScreenNailJob(MediaItem item) {
      mItem = item;
    }

    @Override
    public Bitmap run(JobContext jc) {
      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 bitmap;
    }
  }

  // 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<Bitmap> {
    private final long mVersion;
    private Future<Bitmap> mFuture;

    public ScreenNailListener(long version) {
      mVersion = version;
    }

    @Override
    public void onFutureDone(Future<Bitmap> 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 Bitmap screenNail;
    public Future<Bitmap> 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);
    }
  }
}