blob: baece70231d3d52a9b2795cdff98616b01c4bf64 [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 android.text.TextUtils;
import android.view.View;
import com.android.gallery3d.common.BitmapUtils;
import com.android.gallery3d.common.Utils;
import com.android.gallery3d.data.CameraShortcutImage;
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.data.SnailItem;
import com.android.gallery3d.glrenderer.TiledTexture;
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.ui.TiledScreenNail;
import com.android.gallery3d.util.Future;
import com.android.gallery3d.util.FutureListener;
import com.android.gallery3d.util.GifDecoder;
import com.android.gallery3d.util.GifRequest;
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.lang.Exception;
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;
import java.util.Locale;
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;
private static final int BIT_GIF_ANIMATION = 3;
private static final long NOTIFY_DIRTY_WAIT_TIME = 10;
// 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 + 4];
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);
sImageFetchSeq[k++] = new ImageFetch(0, BIT_GIF_ANIMATION);
}
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;
private AbstractGalleryActivity mActivity;
// If Bundle is from widget, it's true, otherwise it's false.
private boolean mIsFromWidget = false;
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) {
mActivity = activity;
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;
}
/**
* Set this to true if it is from widget.
*/
public void setFromWidget(boolean isFromWidget) {
mIsFromWidget = isFromWidget;
}
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;
ScreenNail targetScreenNail = getTargetScreenNail(entry);
if (targetScreenNail != null) return targetScreenNail;
// 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
|| item.getMediaType() == MediaItem.MEDIA_TYPE_DRM_VIDEO;
}
@Override
public boolean isGif(int offset) {
MediaItem item = getItem(mCurrentIndex + offset);
return (item != null) &&
MediaItem.MIME_TYPE_GIF.equalsIgnoreCase(item.getMimeType());
}
@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 = getTargetScreenNail(entry);
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 ScreenNail getTargetScreenNail(ImageEntry entry) {
if (null == entry) return null;
if (null != entry.currentGifFrame){
return entry.currentGifFrame;
}
return entry.screenNail;
}
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();
}
}
/**
* Update the image window and data window for RTL.
*/
private void updateSlidingWindowForRTL() {
// 1. Update the image window
int nStart = Utils.clamp(mCurrentIndex - IMAGE_CACHE_SIZE / 2,
0, Math.max(0, mSize - IMAGE_CACHE_SIZE));
int nEnd = Math.min(mSize, nStart + IMAGE_CACHE_SIZE);
if (mActiveStart == nStart && mActiveEnd == nEnd) {
return; // don't need to refresh
}
mActiveStart = nStart;
mActiveEnd = nEnd;
// 2. Update the data window
nStart = Utils.clamp(mCurrentIndex - DATA_CACHE_SIZE / 2,
0, Math.max(0, mSize - DATA_CACHE_SIZE));
nEnd = Math.min(mSize, nStart + DATA_CACHE_SIZE);
mContentStart = nStart;
mContentEnd = nEnd;
}
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;
}
if (entry.gifDecoderTask != null && entry.gifDecoderTask != task) {
entry.gifDecoderTask.cancel();
entry.gifDecoderTask = null;
entry.requestedGif = 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);
}
}
private class GifDecoderJob implements Job<GifDecoder> {
private MediaItem mItem;
public GifDecoderJob(MediaItem item) {
mItem = item;
}
@Override
public GifDecoder run(JobContext jc) {
if (isTemporaryItem(mItem)) {
return null;
}
return new GifRequest(mItem.getContentUri(), mActivity).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;
} else if (which == BIT_GIF_ANIMATION && entry.gifDecoderTask != null
&& entry.requestedGif == version) {
return entry.gifDecoderTask;
}
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;
}
if (which == BIT_GIF_ANIMATION
&& (entry.requestedGif != version)
&& isGif(0)) {
entry.requestedGif = version;
entry.gifDecoderTask = mThreadPool.submit(
new GifDecoderJob(item),
new GifDecoderListener(item.getPath()));
// request gif decoder
return entry.gifDecoderTask;
}
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());
}
}
if (Math.abs(i - mCurrentIndex) > 0) {
if (entry.gifDecoderTask != null) {
entry.gifDecoderTask.cancel();
entry.gifDecoderTask = null;
}
entry.gifDecoder = null;
entry.requestedGif = MediaItem.INVALID_DATA_VERSION;
if (null != entry.currentGifFrame) {
//recycle cached gif frame
entry.currentGifFrame.recycle();
entry.currentGifFrame = null;
}
}
} 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 class GifDecoderListener
implements Runnable, FutureListener<GifDecoder> {
private final Path mPath;
private Future<GifDecoder> mFuture;
public GifDecoderListener(Path path) {
mPath = path;
}
@Override
public void onFutureDone(Future<GifDecoder> future) {
mFuture = future;
if (null != mFuture.get()) {
mMainHandler.sendMessage(
mMainHandler.obtainMessage(MSG_RUN_OBJECT, this));
}
}
@Override
public void run() {
updateGifDecoder(mPath, mFuture);
}
}
private void updateGifDecoder(Path path,
Future<GifDecoder> future) {
ImageEntry entry = mImageCache.get(path);
if (entry == null || entry.gifDecoderTask != future) {
return;
}
entry.gifDecoderTask = null;
entry.gifDecoder = future.get();
playGif(path);
updateImageRequests();
}
private void playGif(Path path) {
ImageEntry entry = mImageCache.get(path);
if (null == entry) return;
if (entry.gifDecoder != null && entry.gifDecoder.getFrameCount() != 0) {
if (entry.gifDecoder.mWidth <= 0 || entry.gifDecoder.mHeight <= 0) {
return;
}
int currentIndex = mCurrentIndex;
if (path == getPath(currentIndex)) {
GifEntry gifEntry = new GifEntry();
gifEntry.gifDecoder = entry.gifDecoder;
gifEntry.animatedIndex = currentIndex;
gifEntry.entry = entry;
mMainHandler.sendMessage(
mMainHandler.obtainMessage(MSG_RUN_OBJECT,
new GifRunnable(path, gifEntry)));
}
}
}
private class GifRunnable implements Runnable {
private GifEntry mGifEntry;
private Path mPath;
private void free() {
if (null != mGifEntry) {
if (null != mGifEntry.gifDecoder) {
mGifEntry.gifDecoder.free();
mGifEntry.gifDecoder = null;
}
mGifEntry = null;
}
}
public GifRunnable(Path path, GifEntry gifEntry) {
mPath = path;
mGifEntry = gifEntry;
if (null == mGifEntry || null == mGifEntry.gifDecoder) {
free();
return;
}
boolean imageChanged = mGifEntry.animatedIndex != mCurrentIndex;
MediaItem item = getMediaItem(0);
Path currentPath = (item != null ? item.getPath() : null);
imageChanged |= path != currentPath;
if (imageChanged) {
free();
return;
}
mGifEntry.currentFrame = 0;
mGifEntry.totalFrameCount = mGifEntry.gifDecoder.getFrameCount();
if (mGifEntry.totalFrameCount <= 1) {
free();
return;
}
}
@Override
public void run() {
if (!mIsActive) {
free();
return;
}
if (null == mGifEntry || null == mGifEntry.gifDecoder) {
free();
return;
}
boolean imageChanged = mGifEntry.animatedIndex != mCurrentIndex;
MediaItem item = getMediaItem(0);
Path currentPath = (item != null ? item.getPath() : null);
imageChanged |= mPath != currentPath;
if (imageChanged) {
free();
return;
}
Bitmap frameBitmap = mGifEntry.gifDecoder.getFrameImage(mGifEntry.currentFrame);
if (null == frameBitmap) {
free();
return;
}
long delay = (long) mGifEntry.gifDecoder.getDelay(mGifEntry.currentFrame);
mGifEntry.currentFrame = (mGifEntry.currentFrame + 1) % mGifEntry.totalFrameCount;
ScreenNail gifFrame = new BitmapScreenNail(frameBitmap);
if (mGifEntry.entry.currentGifFrame != null) {
mGifEntry.entry.currentGifFrame.recycle();
mGifEntry.entry.currentGifFrame = null;
}
mGifEntry.entry.currentGifFrame = gifFrame;
updateTileProvider(mGifEntry.entry);
mPhotoView.notifyImageChange(0);
mMainHandler.sendMessageDelayed(
mMainHandler.obtainMessage(MSG_RUN_OBJECT, this), delay);
}
}
private static class GifEntry {
public ImageEntry entry;
public GifDecoder gifDecoder;
public int animatedIndex;
public int currentFrame;
public int totalFrameCount;
}
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;
public GifDecoder gifDecoder;
public Future<GifDecoder> gifDecoderTask;
public ScreenNail currentGifFrame;
public long requestedGif = MediaObject.INVALID_DATA_VERSION;
}
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() {
long version = MediaObject.INVALID_DATA_VERSION;
while (mActive) {
synchronized (this) {
if (!mDirty && mActive && version != MediaObject.INVALID_DATA_VERSION) {
updateLoading(false);
Utils.waitWithoutInterrupt(this);
continue;
}
}
mDirty = false;
version = mSource.reload();
//if data is not ready, continue to reload
if (version == MediaObject.INVALID_DATA_VERSION) {
continue;
}
if (mSource.isLoading()) {
mDirty = true;
continue;
} else {
mSourceVersion = version;
}
UpdateInfo info = executeAndWait(new GetUpdateInfo());
updateLoading(true);
// Used for delete photo, RTL need to re-decide the slide range.
if (View.LAYOUT_DIRECTION_RTL == TextUtils
.getLayoutDirectionFromLocale(Locale.getDefault())
&& mSource.getCurrectSize() == 1 && mCurrentIndex > 0) {
mCurrentIndex = mCurrentIndex - 1;
mSize = mSource.getMediaItemCount();
updateSlidingWindowForRTL();
info = executeAndWait(new GetUpdateInfo());
}
info.size = mSource.getMediaItemCount();
// Check it is from camera or not
boolean isCameraFlag = false;
if (mCameraIndex == mCurrentIndex) {
info.items = mSource.getMediaItem(mCameraIndex, 1);
if (info.items.get(0) instanceof CameraShortcutImage
|| info.items.get(0) instanceof SnailItem) {
isCameraFlag = true;
}
}
// If RTL, need to descending photos
if (!isCameraFlag
&& info.contentStart < info.contentEnd
&& (View.LAYOUT_DIRECTION_RTL == TextUtils
.getLayoutDirectionFromLocale(Locale.getDefault()))) {
// Calculate picture index/range etc..
int nIndex = isCameraFlag ? mCurrentIndex : info.size - mCurrentIndex - 1;
int nStart = Utils.clamp(nIndex - DATA_CACHE_SIZE / 2, 0,
Math.max(0, info.size - DATA_CACHE_SIZE));
info.items = mSource.getMediaItem(nStart, DATA_CACHE_SIZE);
// Initialize temporary picture list
ArrayList<MediaItem> mediaItemList = new ArrayList<MediaItem>();
// Fetch source, check the first item is camera or not
ArrayList<MediaItem> itemstmpList = mSource.getMediaItem(0, 1);
MediaItem itemstmp = itemstmpList.size() > 0 ? itemstmpList.get(0) : null;
boolean isCameraItem = (itemstmp != null)
&& (itemstmp instanceof CameraShortcutImage
|| itemstmp instanceof SnailItem);
if (isCameraItem) {
// If it's camera mode, need to put camera to first position
mediaItemList.add(itemstmp);
}
// Descending
for (int i = info.items.size() - 1; i >= 0; i--) {
if (isCameraItem && 0 == i) {
continue;
}
mediaItemList.add(info.items.get(i));
}
info.items = (ArrayList<MediaItem>) mediaItemList.clone();
// Clear temporary list and free memory immediately
mediaItemList.clear();
mediaItemList = null;
} else {
info.items = mSource.getMediaItem(
info.contentStart, info.contentEnd);
} // If RTL, need to descending photos end
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 {
// If RTL and it's not from widget, the index don't need to be amended
if (View.LAYOUT_DIRECTION_RTL == TextUtils
.getLayoutDirectionFromLocale(Locale.getDefault())
&& info.contentStart < info.contentEnd
&& !mIsFromWidget) {
index = info.size - findIndexOfTarget(info) - 1;
} else {
index = findIndexOfTarget(info);
mIsFromWidget = false;
}
}
}
// 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;
}
}
}