summaryrefslogtreecommitdiffstats
path: root/src/com/android/gallery3d/app/PhotoDataAdapter.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/gallery3d/app/PhotoDataAdapter.java')
-rw-r--r--src/com/android/gallery3d/app/PhotoDataAdapter.java1133
1 files changed, 1133 insertions, 0 deletions
diff --git a/src/com/android/gallery3d/app/PhotoDataAdapter.java b/src/com/android/gallery3d/app/PhotoDataAdapter.java
new file mode 100644
index 000000000..fd3a7cf73
--- /dev/null
+++ b/src/com/android/gallery3d/app/PhotoDataAdapter.java
@@ -0,0 +1,1133 @@
+/*
+ * 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.LocalMediaItem;
+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.glrenderer.TiledTexture;
+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.ui.TiledScreenNail;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.MediaSetUtils;
+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 MSG_UPDATE_IMAGE_REQUESTS = 4;
+
+ private static final int MIN_LOAD_COUNT = 16;
+ private static final int DATA_CACHE_SIZE = 256;
+ private static final int SCREEN_NAIL_MAX = PhotoView.SCREEN_NAIL_MAX;
+ private static final int IMAGE_CACHE_SIZE = 2 * SCREEN_NAIL_MAX + 1;
+
+ private static final int BIT_SCREEN_NAIL = 1;
+ private static final int BIT_FULL_IMAGE = 2;
+
+ // 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 Path-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<Path, ImageEntry> mImageCache =
+ new HashMap<Path, 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 images. If any
+ // of the version number changes, we notify the view. This is used after a
+ // database reload or mCurrentIndex changes.
+ private final long mChanges[] = new long[IMAGE_CACHE_SIZE];
+ // mPaths keeps the corresponding Path (of MediaItem) for the images. This
+ // is used to determine the item movement.
+ private final Path mPaths[] = new Path[IMAGE_CACHE_SIZE];
+
+ 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 int mCameraIndex;
+ private boolean mIsPanorama;
+ private boolean mIsStaticCamera;
+ private boolean mIsActive;
+ private boolean mNeedFullImage;
+ private int mFocusHintDirection = FOCUS_HINT_NEXT;
+ private Path mFocusHintPath = null;
+
+ public interface DataListener extends LoadingListener {
+ public void onPhotoChanged(int index, Path item);
+ }
+
+ private DataListener mDataListener;
+
+ private final SourceListener mSourceListener = new SourceListener();
+ private final TiledTexture.Uploader mUploader;
+
+ // 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. cameraIndex is the index of the camera
+ // preview. If cameraIndex < 0, there is no camera preview.
+ public PhotoDataAdapter(AbstractGalleryActivity activity, PhotoView view,
+ MediaSet mediaSet, Path itemPath, int indexHint, int cameraIndex,
+ boolean isPanorama, boolean isStaticCamera) {
+ mSource = Utils.checkNotNull(mediaSet);
+ mPhotoView = Utils.checkNotNull(view);
+ mItemPath = Utils.checkNotNull(itemPath);
+ mCurrentIndex = indexHint;
+ mCameraIndex = cameraIndex;
+ mIsPanorama = isPanorama;
+ mIsStaticCamera = isStaticCamera;
+ mThreadPool = activity.getThreadPool();
+ mNeedFullImage = true;
+
+ Arrays.fill(mChanges, MediaObject.INVALID_DATA_VERSION);
+
+ mUploader = new TiledTexture.Uploader(activity.getGLRoot());
+
+ 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(false);
+ }
+ return;
+ }
+ case MSG_UPDATE_IMAGE_REQUESTS: {
+ updateImageRequests();
+ return;
+ }
+ default: throw new AssertionError();
+ }
+ }
+ };
+
+ updateSlidingWindow();
+ }
+
+ private MediaItem getItemInternal(int index) {
+ if (index < 0 || index >= mSize) return null;
+ if (index >= mContentStart && index < mContentEnd) {
+ return mData[index % DATA_CACHE_SIZE];
+ }
+ return null;
+ }
+
+ private long getVersion(int index) {
+ MediaItem item = getItemInternal(index);
+ if (item == null) return MediaObject.INVALID_DATA_VERSION;
+ return item.getDataVersion();
+ }
+
+ private Path getPath(int index) {
+ MediaItem item = getItemInternal(index);
+ if (item == null) return null;
+ return item.getPath();
+ }
+
+ private void fireDataChange() {
+ // First check if data actually changed.
+ boolean changed = false;
+ for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) {
+ long newVersion = getVersion(mCurrentIndex + i);
+ if (mChanges[i + SCREEN_NAIL_MAX] != newVersion) {
+ mChanges[i + SCREEN_NAIL_MAX] = newVersion;
+ changed = true;
+ }
+ }
+
+ if (!changed) return;
+
+ // Now calculate the fromIndex array. fromIndex represents the item
+ // movement. It records the index where the picture come from. The
+ // special value Integer.MAX_VALUE means it's a new picture.
+ final int N = IMAGE_CACHE_SIZE;
+ int fromIndex[] = new int[N];
+
+ // Remember the old path array.
+ Path oldPaths[] = new Path[N];
+ System.arraycopy(mPaths, 0, oldPaths, 0, N);
+
+ // Update the mPaths array.
+ for (int i = 0; i < N; ++i) {
+ mPaths[i] = getPath(mCurrentIndex + i - SCREEN_NAIL_MAX);
+ }
+
+ // Calculate the fromIndex array.
+ for (int i = 0; i < N; i++) {
+ Path p = mPaths[i];
+ if (p == null) {
+ fromIndex[i] = Integer.MAX_VALUE;
+ continue;
+ }
+
+ // Try to find the same path in the old array
+ int j;
+ for (j = 0; j < N; j++) {
+ if (oldPaths[j] == p) {
+ break;
+ }
+ }
+ fromIndex[i] = (j < N) ? j - SCREEN_NAIL_MAX : Integer.MAX_VALUE;
+ }
+
+ mPhotoView.notifyDataChange(fromIndex, -mCurrentIndex,
+ mSize - 1 - mCurrentIndex);
+ }
+
+ public void setDataListener(DataListener listener) {
+ mDataListener = listener;
+ }
+
+ private void updateScreenNail(Path path, Future<ScreenNail> future) {
+ ImageEntry entry = mImageCache.get(path);
+ ScreenNail screenNail = future.get();
+
+ if (entry == null || entry.screenNailTask != future) {
+ if (screenNail != null) screenNail.recycle();
+ return;
+ }
+
+ entry.screenNailTask = null;
+
+ // Combine the ScreenNails if we already have a BitmapScreenNail
+ if (entry.screenNail instanceof TiledScreenNail) {
+ TiledScreenNail original = (TiledScreenNail) entry.screenNail;
+ screenNail = original.combine(screenNail);
+ }
+
+ if (screenNail == null) {
+ entry.failToLoad = true;
+ } else {
+ entry.failToLoad = false;
+ entry.screenNail = screenNail;
+ }
+
+ for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) {
+ if (path == getPath(mCurrentIndex + i)) {
+ if (i == 0) updateTileProvider(entry);
+ mPhotoView.notifyImageChange(i);
+ break;
+ }
+ }
+ updateImageRequests();
+ updateScreenNailUploadQueue();
+ }
+
+ private void updateFullImage(Path path, Future<BitmapRegionDecoder> future) {
+ ImageEntry entry = mImageCache.get(path);
+ 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 (path == getPath(mCurrentIndex)) {
+ updateTileProvider(entry);
+ mPhotoView.notifyImageChange(0);
+ }
+ }
+ updateImageRequests();
+ }
+
+ @Override
+ public void resume() {
+ mIsActive = true;
+ TiledTexture.prepareResources();
+
+ mSource.addContentListener(mSourceListener);
+ updateImageCache();
+ updateImageRequests();
+
+ mReloadTask = new ReloadTask();
+ mReloadTask.start();
+
+ fireDataChange();
+ }
+
+ @Override
+ 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();
+ if (entry.screenNail != null) entry.screenNail.recycle();
+ }
+ mImageCache.clear();
+ mTileProvider.clear();
+
+ mUploader.clear();
+ TiledTexture.freeResources();
+ }
+
+ private MediaItem getItem(int index) {
+ if (index < 0 || index >= mSize || !mIsActive) return null;
+ Utils.assertTrue(index >= mActiveStart && index < mActiveEnd);
+
+ if (index >= mContentStart && index < mContentEnd) {
+ return mData[index % DATA_CACHE_SIZE];
+ }
+ return null;
+ }
+
+ private void updateCurrentIndex(int index) {
+ if (mCurrentIndex == index) return;
+ mCurrentIndex = index;
+ updateSlidingWindow();
+
+ MediaItem item = mData[index % DATA_CACHE_SIZE];
+ mItemPath = item == null ? null : item.getPath();
+
+ updateImageCache();
+ updateImageRequests();
+ updateTileProvider();
+
+ if (mDataListener != null) {
+ mDataListener.onPhotoChanged(index, mItemPath);
+ }
+
+ fireDataChange();
+ }
+
+ private void uploadScreenNail(int offset) {
+ int index = mCurrentIndex + offset;
+ if (index < mActiveStart || index >= mActiveEnd) return;
+
+ MediaItem item = getItem(index);
+ if (item == null) return;
+
+ ImageEntry e = mImageCache.get(item.getPath());
+ if (e == null) return;
+
+ ScreenNail s = e.screenNail;
+ if (s instanceof TiledScreenNail) {
+ TiledTexture t = ((TiledScreenNail) s).getTexture();
+ if (t != null && !t.isReady()) mUploader.addTexture(t);
+ }
+ }
+
+ private void updateScreenNailUploadQueue() {
+ mUploader.clear();
+ uploadScreenNail(0);
+ for (int i = 1; i < IMAGE_CACHE_SIZE; ++i) {
+ uploadScreenNail(i);
+ uploadScreenNail(-i);
+ }
+ }
+
+ @Override
+ public void moveTo(int index) {
+ updateCurrentIndex(index);
+ }
+
+ @Override
+ public ScreenNail getScreenNail(int offset) {
+ int index = mCurrentIndex + offset;
+ if (index < 0 || index >= mSize || !mIsActive) return null;
+ Utils.assertTrue(index >= mActiveStart && index < mActiveEnd);
+
+ MediaItem item = getItem(index);
+ if (item == null) return null;
+
+ ImageEntry entry = mImageCache.get(item.getPath());
+ if (entry == null) return null;
+
+ // Create a default ScreenNail if the real one is not available yet,
+ // except for camera that a black screen is better than a gray tile.
+ if (entry.screenNail == null && !isCamera(offset)) {
+ entry.screenNail = newPlaceholderScreenNail(item);
+ if (offset == 0) updateTileProvider(entry);
+ }
+
+ return entry.screenNail;
+ }
+
+ @Override
+ public void getImageSize(int offset, PhotoView.Size size) {
+ MediaItem item = getItem(mCurrentIndex + offset);
+ if (item == null) {
+ size.width = 0;
+ size.height = 0;
+ } else {
+ size.width = item.getWidth();
+ size.height = item.getHeight();
+ }
+ }
+
+ @Override
+ public int getImageRotation(int offset) {
+ MediaItem item = getItem(mCurrentIndex + offset);
+ return (item == null) ? 0 : item.getFullImageRotation();
+ }
+
+ @Override
+ public void setNeedFullImage(boolean enabled) {
+ mNeedFullImage = enabled;
+ mMainHandler.sendEmptyMessage(MSG_UPDATE_IMAGE_REQUESTS);
+ }
+
+ @Override
+ public boolean isCamera(int offset) {
+ return mCurrentIndex + offset == mCameraIndex;
+ }
+
+ @Override
+ public boolean isPanorama(int offset) {
+ return isCamera(offset) && mIsPanorama;
+ }
+
+ @Override
+ public boolean isStaticCamera(int offset) {
+ return isCamera(offset) && mIsStaticCamera;
+ }
+
+ @Override
+ public boolean isVideo(int offset) {
+ MediaItem item = getItem(mCurrentIndex + offset);
+ return (item == null)
+ ? false
+ : item.getMediaType() == MediaItem.MEDIA_TYPE_VIDEO;
+ }
+
+ @Override
+ public boolean isDeletable(int offset) {
+ MediaItem item = getItem(mCurrentIndex + offset);
+ return (item == null)
+ ? false
+ : (item.getSupportedOperations() & MediaItem.SUPPORT_DELETE) != 0;
+ }
+
+ @Override
+ public int getLoadingState(int offset) {
+ ImageEntry entry = mImageCache.get(getPath(mCurrentIndex + offset));
+ if (entry == null) return LOADING_INIT;
+ if (entry.failToLoad) return LOADING_FAIL;
+ if (entry.screenNail != null) return LOADING_COMPLETE;
+ return LOADING_INIT;
+ }
+
+ @Override
+ public ScreenNail getScreenNail() {
+ return getScreenNail(0);
+ }
+
+ @Override
+ public int getImageHeight() {
+ return mTileProvider.getImageHeight();
+ }
+
+ @Override
+ public int getImageWidth() {
+ return mTileProvider.getImageWidth();
+ }
+
+ @Override
+ public int getLevelCount() {
+ return mTileProvider.getLevelCount();
+ }
+
+ @Override
+ public Bitmap getTile(int level, int x, int y, int tileSize) {
+ return mTileProvider.getTile(level, x, y, tileSize);
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return mSize == 0;
+ }
+
+ @Override
+ public int getCurrentIndex() {
+ return mCurrentIndex;
+ }
+
+ @Override
+ public MediaItem getMediaItem(int offset) {
+ int index = mCurrentIndex + offset;
+ if (index >= mContentStart && index < mContentEnd) {
+ return mData[index % DATA_CACHE_SIZE];
+ }
+ return null;
+ }
+
+ @Override
+ public void setCurrentPhoto(Path path, int indexHint) {
+ if (mItemPath == path) return;
+ mItemPath = path;
+ mCurrentIndex = indexHint;
+ updateSlidingWindow();
+ updateImageCache();
+ fireDataChange();
+
+ // We need to reload content if the path doesn't match.
+ MediaItem item = getMediaItem(0);
+ if (item != null && item.getPath() != path) {
+ if (mReloadTask != null) mReloadTask.notifyDirty();
+ }
+ }
+
+ @Override
+ public void setFocusHintDirection(int direction) {
+ mFocusHintDirection = direction;
+ }
+
+ @Override
+ public void setFocusHintPath(Path path) {
+ mFocusHintPath = path;
+ }
+
+ private void updateTileProvider() {
+ ImageEntry entry = mImageCache.get(getPath(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();
+ }
+ }
+
+ 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;
+ if (bit == BIT_FULL_IMAGE && !mNeedFullImage) continue;
+ 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.requestedScreenNail = MediaObject.INVALID_DATA_VERSION;
+ }
+ if (entry.fullImageTask != null && entry.fullImageTask != task) {
+ entry.fullImageTask.cancel();
+ entry.fullImageTask = null;
+ entry.requestedFullImage = MediaObject.INVALID_DATA_VERSION;
+ }
+ }
+ }
+
+ private 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;
+
+ // If this is a temporary item, don't try to get its bitmap because
+ // it won't be available. We will get its bitmap after a data reload.
+ if (isTemporaryItem(mItem)) {
+ return newPlaceholderScreenNail(mItem);
+ }
+
+ 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 == null ? null : new TiledScreenNail(bitmap);
+ }
+ }
+
+ private class FullImageJob implements Job<BitmapRegionDecoder> {
+ private MediaItem mItem;
+
+ public FullImageJob(MediaItem item) {
+ mItem = item;
+ }
+
+ @Override
+ public BitmapRegionDecoder run(JobContext jc) {
+ if (isTemporaryItem(mItem)) {
+ return null;
+ }
+ return mItem.requestLargeImage().run(jc);
+ }
+ }
+
+ // Returns true if we think this is a temporary item created by Camera. A
+ // temporary item is an image or a video whose data is still being
+ // processed, but an incomplete entry is created first in MediaProvider, so
+ // we can display them (in grey tile) even if they are not saved to disk
+ // yet. When the image or video data is actually saved, we will get
+ // notification from MediaProvider, reload data, and show the actual image
+ // or video data.
+ private boolean isTemporaryItem(MediaItem mediaItem) {
+ // Must have camera to create a temporary item.
+ if (mCameraIndex < 0) return false;
+ // Must be an item in camera roll.
+ if (!(mediaItem instanceof LocalMediaItem)) return false;
+ LocalMediaItem item = (LocalMediaItem) mediaItem;
+ if (item.getBucketId() != MediaSetUtils.CAMERA_BUCKET_ID) return false;
+ // Must have no size, but must have width and height information
+ if (item.getSize() != 0) return false;
+ if (item.getWidth() == 0) return false;
+ if (item.getHeight() == 0) return false;
+ // Must be created in the last 10 seconds.
+ if (item.getDateInMs() - System.currentTimeMillis() > 10000) return false;
+ return true;
+ }
+
+ // Create a default ScreenNail when a ScreenNail is needed, but we don't yet
+ // have one available (because the image data is still being saved, or the
+ // Bitmap is still being loaded.
+ private ScreenNail newPlaceholderScreenNail(MediaItem item) {
+ int width = item.getWidth();
+ int height = item.getHeight();
+ return new TiledScreenNail(width, height);
+ }
+
+ // 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(getPath(index));
+ if (entry == null) return null;
+ MediaItem item = mData[index % DATA_CACHE_SIZE];
+ Utils.assertTrue(item != null);
+ long version = item.getDataVersion();
+
+ if (which == BIT_SCREEN_NAIL && entry.screenNailTask != null
+ && entry.requestedScreenNail == version) {
+ return entry.screenNailTask;
+ } else if (which == BIT_FULL_IMAGE && entry.fullImageTask != null
+ && entry.requestedFullImage == version) {
+ return entry.fullImageTask;
+ }
+
+ if (which == BIT_SCREEN_NAIL && entry.requestedScreenNail != version) {
+ entry.requestedScreenNail = version;
+ entry.screenNailTask = mThreadPool.submit(
+ new ScreenNailJob(item),
+ new ScreenNailListener(item));
+ // request screen nail
+ return entry.screenNailTask;
+ }
+ if (which == BIT_FULL_IMAGE && entry.requestedFullImage != version
+ && (item.getSupportedOperations()
+ & MediaItem.SUPPORT_FULL_IMAGE) != 0) {
+ entry.requestedFullImage = version;
+ entry.fullImageTask = mThreadPool.submit(
+ new FullImageJob(item),
+ new FullImageListener(item));
+ // request full image
+ return entry.fullImageTask;
+ }
+ return null;
+ }
+
+ private void updateImageCache() {
+ HashSet<Path> toBeRemoved = new HashSet<Path>(mImageCache.keySet());
+ for (int i = mActiveStart; i < mActiveEnd; ++i) {
+ MediaItem item = mData[i % DATA_CACHE_SIZE];
+ if (item == null) continue;
+ Path path = item.getPath();
+ ImageEntry entry = mImageCache.get(path);
+ toBeRemoved.remove(path);
+ if (entry != null) {
+ if (Math.abs(i - mCurrentIndex) > 1) {
+ if (entry.fullImageTask != null) {
+ entry.fullImageTask.cancel();
+ entry.fullImageTask = null;
+ }
+ entry.fullImage = null;
+ entry.requestedFullImage = MediaObject.INVALID_DATA_VERSION;
+ }
+ if (entry.requestedScreenNail != item.getDataVersion()) {
+ // This ScreenNail is outdated, we want to update it if it's
+ // still a placeholder.
+ if (entry.screenNail instanceof TiledScreenNail) {
+ TiledScreenNail s = (TiledScreenNail) entry.screenNail;
+ s.updatePlaceholderSize(
+ item.getWidth(), item.getHeight());
+ }
+ }
+ } else {
+ entry = new ImageEntry();
+ mImageCache.put(path, entry);
+ }
+ }
+
+ // Clear the data and requests for ImageEntries outside the new window.
+ for (Path path : toBeRemoved) {
+ ImageEntry entry = mImageCache.remove(path);
+ if (entry.fullImageTask != null) entry.fullImageTask.cancel();
+ if (entry.screenNailTask != null) entry.screenNailTask.cancel();
+ if (entry.screenNail != null) entry.screenNail.recycle();
+ }
+
+ updateScreenNailUploadQueue();
+ }
+
+ private class FullImageListener
+ implements Runnable, FutureListener<BitmapRegionDecoder> {
+ private final Path mPath;
+ private Future<BitmapRegionDecoder> mFuture;
+
+ public FullImageListener(MediaItem item) {
+ mPath = item.getPath();
+ }
+
+ @Override
+ public void onFutureDone(Future<BitmapRegionDecoder> future) {
+ mFuture = future;
+ mMainHandler.sendMessage(
+ mMainHandler.obtainMessage(MSG_RUN_OBJECT, this));
+ }
+
+ @Override
+ public void run() {
+ updateFullImage(mPath, mFuture);
+ }
+ }
+
+ private class ScreenNailListener
+ implements Runnable, FutureListener<ScreenNail> {
+ private final Path mPath;
+ private Future<ScreenNail> mFuture;
+
+ public ScreenNailListener(MediaItem item) {
+ mPath = item.getPath();
+ }
+
+ @Override
+ public void onFutureDone(Future<ScreenNail> future) {
+ mFuture = future;
+ mMainHandler.sendMessage(
+ mMainHandler.obtainMessage(MSG_RUN_OBJECT, this));
+ }
+
+ @Override
+ public void run() {
+ updateScreenNail(mPath, mFuture);
+ }
+ }
+
+ private static class ImageEntry {
+ public BitmapRegionDecoder fullImage;
+ public ScreenNail screenNail;
+ public Future<ScreenNail> screenNailTask;
+ public Future<BitmapRegionDecoder> fullImageTask;
+ public long requestedScreenNail = MediaObject.INVALID_DATA_VERSION;
+ public long requestedFullImage = MediaObject.INVALID_DATA_VERSION;
+ public boolean failToLoad = false;
+ }
+
+ private class SourceListener implements ContentListener {
+ @Override
+ 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;
+ }
+
+ 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;
+ }
+ }
+
+ // update mItemPath
+ MediaItem current = mData[mCurrentIndex % DATA_CACHE_SIZE];
+ mItemPath = current == null ? null : current.getPath();
+
+ updateImageCache();
+ updateTileProvider();
+ updateImageRequests();
+
+ if (mDataListener != null) {
+ mDataListener.onPhotoChanged(mCurrentIndex, mItemPath);
+ }
+
+ fireDataChange();
+ return null;
+ }
+ }
+
+ 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());
+ 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);
+
+ int index = MediaSet.INDEX_NOT_FOUND;
+
+ // First try to focus on the given hint path if there is one.
+ if (mFocusHintPath != null) {
+ index = findIndexOfPathInCache(info, mFocusHintPath);
+ mFocusHintPath = null;
+ }
+
+ // Otherwise try to see if the currently focused item can be found.
+ if (index == MediaSet.INDEX_NOT_FOUND) {
+ MediaItem item = findCurrentMediaItem(info);
+ if (item != null && item.getPath() == info.target) {
+ index = info.indexHint;
+ } else {
+ index = findIndexOfTarget(info);
+ }
+ }
+
+ // The image has been deleted. Focus on the next image (keep
+ // mCurrentIndex unchanged) or the previous image (decrease
+ // mCurrentIndex by 1). In page mode we want to see the next
+ // image, so we focus on the next one. In film mode we want the
+ // later images to shift left to fill the empty space, so we
+ // focus on the previous image (so it will not move). In any
+ // case the index needs to be limited to [0, mSize).
+ if (index == MediaSet.INDEX_NOT_FOUND) {
+ index = info.indexHint;
+ int focusHintDirection = mFocusHintDirection;
+ if (index == (mCameraIndex + 1)) {
+ focusHintDirection = FOCUS_HINT_NEXT;
+ }
+ if (focusHintDirection == FOCUS_HINT_PREVIOUS
+ && index > 0) {
+ index--;
+ }
+ }
+
+ // Don't change index if mSize == 0
+ if (mSize > 0) {
+ if (index >= mSize) index = mSize - 1;
+ }
+
+ info.indexHint = index;
+
+ 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) {
+ int i = findIndexOfPathInCache(info, info.target);
+ if (i != MediaSet.INDEX_NOT_FOUND) return i;
+ }
+
+ // Not found, find it in mSource.
+ return mSource.getIndexOfItem(info.target, info.indexHint);
+ }
+
+ private int findIndexOfPathInCache(UpdateInfo info, Path path) {
+ ArrayList<MediaItem> items = info.items;
+ for (int i = 0, n = items.size(); i < n; ++i) {
+ MediaItem item = items.get(i);
+ if (item != null && item.getPath() == path) {
+ return i + info.contentStart;
+ }
+ }
+ return MediaSet.INDEX_NOT_FOUND;
+ }
+ }
+}