Allow multiple image request in MediaItem.

Change-Id: I6f6cf1d7a5e807ba823b916ec24999523907801d
diff --git a/new3d/src/com/android/gallery3d/app/Gallery.java b/new3d/src/com/android/gallery3d/app/Gallery.java
index 3708609..8df1263 100644
--- a/new3d/src/com/android/gallery3d/app/Gallery.java
+++ b/new3d/src/com/android/gallery3d/app/Gallery.java
@@ -80,9 +80,6 @@
     public void onDestroy() {
         super.onDestroy();
         Log.i(TAG, "onDestroy");
-        synchronized (this) {
-            if (mImageService != null) mImageService.close();
-        }
     }
 
     @Override
diff --git a/new3d/src/com/android/gallery3d/data/DecodeService.java b/new3d/src/com/android/gallery3d/data/DecodeService.java
index d236af8..b067bac 100644
--- a/new3d/src/com/android/gallery3d/data/DecodeService.java
+++ b/new3d/src/com/android/gallery3d/data/DecodeService.java
@@ -19,12 +19,17 @@
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.BitmapFactory.Options;
+import android.util.Log;
 
 import com.android.gallery3d.util.Future;
 import com.android.gallery3d.util.FutureListener;
 import com.android.gallery3d.util.FutureTask;
+import com.android.gallery3d.util.Utils;
 
+import java.io.BufferedInputStream;
 import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
 import java.util.concurrent.Callable;
 import java.util.concurrent.Executor;
 import java.util.concurrent.LinkedBlockingQueue;
@@ -32,26 +37,22 @@
 import java.util.concurrent.TimeUnit;
 
 public class DecodeService {
+    private static final String TAG = "DecodeService";
+
     private static final int CORE_POOL_SIZE = 1;
     private static final int MAX_POOL_SIZE = 1;
     private static final int KEEP_ALIVE_TIME = 10000; // 10 seconds
-
-    private static DecodeService sInstance;
+    private static final int JPEG_MARK_POSITION = 60 * 1024;
 
     private final Executor mExecutor = new ThreadPoolExecutor(
             CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME,
             TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
 
-    public static synchronized DecodeService getInstance() {
-        if (sInstance == null) sInstance = new DecodeService();
-        return sInstance;
-    }
-
     public Future<Bitmap> requestDecode(
             File file, Options options, FutureListener<? super Bitmap> listener) {
         if (options == null) options = new Options();
         FutureTask<Bitmap> task = new DecodeFutureTask(
-                new DecodeFile(file, options), listener);
+                new DecodeFile(file, options), options, listener);
         mExecutor.execute(task);
         return task;
     }
@@ -71,22 +72,35 @@
                     offset, length, bytes.length));
         }
         FutureTask<Bitmap> task = new DecodeFutureTask(
-                new DecodeByteArray(bytes, offset, length, options), listener);
+                new DecodeByteArray(bytes, offset, length, options),
+                options, listener);
+        mExecutor.execute(task);
+        return task;
+    }
+
+    public FutureTask<Bitmap> requestDecode(
+            File file, Options options, int targetLength, int maxPixelCount,
+            FutureListener<? super Bitmap> listener) {
+        if (options == null) options = new Options();
+        FutureTask<Bitmap> task = new DecodeFutureTask(
+                new DecodeAndSampleFile(file, options, targetLength, maxPixelCount),
+                options, listener);
         mExecutor.execute(task);
         return task;
     }
 
     private static class DecodeFutureTask extends FutureTask<Bitmap> {
 
-        private Options mOptions;
+        private final Options mOptions;
 
-        public DecodeFutureTask(
-                Callable<Bitmap> callable, FutureListener<? super Bitmap> listener) {
+        public DecodeFutureTask(Callable<Bitmap> callable,
+                Options options, FutureListener<? super Bitmap> listener) {
             super(callable, listener);
+            mOptions = options;
         }
 
         @Override
-        public void onCancel() {
+        public void cancelTask() {
             mOptions.requestCancelDecode();
         }
     }
@@ -125,4 +139,49 @@
         }
     }
 
+    private static class DecodeAndSampleFile implements Callable<Bitmap> {
+
+        private final int mTargetLength;
+        private final int mMaxPixelCount;
+        private final File mFile;
+        private final Options mOptions;
+
+        public DecodeAndSampleFile(
+                File file, Options options, int targetLength, int maxPixelCount) {
+            mFile = file;
+            mOptions = options;
+            mTargetLength = targetLength;
+            mMaxPixelCount = maxPixelCount;
+        }
+
+        public Bitmap call() throws IOException {
+            BufferedInputStream bis = new BufferedInputStream(
+                    new FileInputStream(mFile), JPEG_MARK_POSITION);
+            try {
+                // Decode bufferedInput for calculating a sample size.
+                final BitmapFactory.Options options = mOptions;
+                options.inJustDecodeBounds = true;
+                bis.mark(JPEG_MARK_POSITION);
+                BitmapFactory.decodeStream(bis, null, options);
+                if (options.mCancel) return null;
+
+                try {
+                    bis.reset();
+                } catch (IOException e) {
+                    Log.w(TAG, "failed in resetting the buffer after reading the jpeg header", e);
+                    bis.close();
+                    bis = new BufferedInputStream(new FileInputStream(mFile));
+                }
+
+                options.inSampleSize =  Utils.computeSampleSize(
+                        options, mTargetLength, mMaxPixelCount);
+                options.inJustDecodeBounds = false;
+                return BitmapFactory.decodeStream(bis, null, options);
+            } finally {
+                bis.close();
+            }
+        }
+
+    }
+
 }
diff --git a/new3d/src/com/android/gallery3d/data/DownloadService.java b/new3d/src/com/android/gallery3d/data/DownloadService.java
index 22bda53..47246e5 100644
--- a/new3d/src/com/android/gallery3d/data/DownloadService.java
+++ b/new3d/src/com/android/gallery3d/data/DownloadService.java
@@ -38,15 +38,10 @@
     private static final int MAX_POOL_SIZE = 4;
     private static final int KEEP_ALIVE_TIME = 10000;
 
-    private static DownloadService sInstance;
-
     private final ThreadPoolExecutor mExecutor = new ThreadPoolExecutor(
             CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME,
             TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
 
-    public DownloadService() {
-    }
-
     public FutureTask<Void> requestDownload(
             URL url, File file, FutureListener<Void> listener) {
         FutureTask<Void> task = new FutureTask<Void>(
diff --git a/new3d/src/com/android/gallery3d/data/ImageService.java b/new3d/src/com/android/gallery3d/data/ImageService.java
index 1d91047..33498ab 100644
--- a/new3d/src/com/android/gallery3d/data/ImageService.java
+++ b/new3d/src/com/android/gallery3d/data/ImageService.java
@@ -18,171 +18,214 @@
 
 import android.content.ContentResolver;
 import android.graphics.Bitmap;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
-import android.util.Log;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.Video;
 
-import java.util.HashMap;
-import java.util.PriorityQueue;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureHelper;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.Utils;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
 
 public class ImageService {
-
     private static final String TAG = "ImageService";
 
-    private static final int DECODE_TIMEOUT = 0;
+    private static final int MICRO_TARGET_PIXELS = 128 * 128;
+    private static final int CORE_POOL_SIZE = 1;
+    private static final int MAX_POOL_SIZE = 1;
+    private static final int KEEP_ALIVE_TIME = 10000; // 10 seconds
 
-    private static final int INITIAL_TIMEOUT = 2000;
-    private static final int MAXIMAL_TIMEOUT = 32000;
+    private final Executor mExecutor = new ThreadPoolExecutor(
+            CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME,
+            TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
 
-    private final HashMap<Integer, DecodeTask> mMap =
-            new HashMap<Integer, DecodeTask>();
-    private final PriorityQueue<DecodeTask> mQueue = new PriorityQueue<DecodeTask>();
-    private final Handler mHandler;
     private final ContentResolver mContentResolver;
 
-    private boolean mActive = true;
-    private int mTimeSerial;
-
-    private DecodeTask mCurrentTask;
-    private final DecodeThread mDecodeThread = new DecodeThread();
-
     public ImageService(ContentResolver contentResolver) {
         mContentResolver = contentResolver;
-
-        mHandler = new Handler(Looper.getMainLooper()) {
-            @Override
-            public void handleMessage(Message m) {
-                if (m.what != DECODE_TIMEOUT) return;
-                DecodeTask task = mCurrentTask;
-                if (task != null) {
-                    task.mItem.cancelImageGeneration(mContentResolver, task.mType);
-                }
-            }
-        };
-
-        mDecodeThread.start();
     }
 
-    protected int requestImage(LocalMediaItem item, int type) {
-        DecodeTask task = new DecodeTask();
-        task.mRequestId = ++mTimeSerial;
-        task.mItem = item;
-        task.mType = type;
-        task.mTimeout = INITIAL_TIMEOUT;
-
-        synchronized (mQueue) {
-            mMap.put(task.mRequestId, task);
-            mQueue.add(task);
-            if (mQueue.size() == 1) mQueue.notifyAll();
+    public Future<Bitmap> requestImageThumbnail(
+            int id, int type, FutureListener<? super Bitmap> listener) {
+        if (type != MediaItem.TYPE_MICROTHUMBNAIL
+                && type != MediaItem.TYPE_THUMBNAIL) {
+            throw new IllegalArgumentException(String.format("type = %s", type));
         }
-        return task.mRequestId;
+        GetImageThumbnail task =
+                new GetImageThumbnail(id, type, mContentResolver, listener);
+        mExecutor.execute(task);
+        return task;
     }
 
-    protected void cancelRequest(int requestId) {
-        synchronized (mQueue) {
-            DecodeTask task = mMap.remove(requestId);
-            if (task == null) return;
-            task.mCanceled = true;
-            if (mQueue.remove(task)) {
-                task.mItem.onImageCanceled(task.mType);
-            } else {
-                task.mItem.cancelImageGeneration(mContentResolver, task.mType);
-            }
+    public Future<Bitmap> requestVideoThumbnail(
+            int id, int type, FutureListener<? super Bitmap> listener) {
+        if (type != MediaItem.TYPE_MICROTHUMBNAIL
+                && type != MediaItem.TYPE_THUMBNAIL
+                && type != MediaItem.TYPE_FULL_IMAGE) {
+            throw new IllegalArgumentException(String.format("type = %s", type));
         }
+        GetVideoThumbnail task =
+            new GetVideoThumbnail(id, type, mContentResolver, listener);
+        mExecutor.execute(task);
+        return task;
     }
 
-    public void close() {
-        synchronized (mQueue) {
-            mActive = false;
-            mQueue.notifyAll();
+    private static class GetImageThumbnail extends FutureHelper<Bitmap> implements Runnable {
+        private final int STATE_READY = 0;
+        private final int STATE_RUNNING = 1;
+        private final int STATE_CANCELED = 2;
+        private final int STATE_RAN = 4;
+
+        // mState tries to guard that onCancel() is called only when
+        // getThumbnail() is being executed.
+        private AtomicInteger mState = new AtomicInteger(STATE_READY);
+        private final int mId;
+        private final int mType;
+        private final ContentResolver mResolver;
+
+        public GetImageThumbnail(int id, int type, ContentResolver resolver,
+                FutureListener<? super Bitmap> listener) {
+            super(listener);
+            mId = id;
+            mType = type;
+            mResolver= resolver;
         }
-    }
 
-    protected DecodeTask nextDecodeTask() {
-        PriorityQueue<DecodeTask> queue = mQueue;
-        synchronized (queue) {
-            try {
-                while (queue.isEmpty() && mActive) {
-                    queue.wait();
-                }
-            } catch (InterruptedException e) {
-                Log.v(TAG, "decode-thread is interrupted");
-                Thread.currentThread().interrupt();
-                return null;
-            }
-            return !mActive ? null : queue.remove() ;
-        }
-    }
-
-    private class DecodeThread extends Thread {
-        @Override
         public void run() {
-            PriorityQueue<DecodeTask> queue = mQueue;
-            ContentResolver resolver = mContentResolver;
+            Bitmap bitmap = null;
+            try {
+                bitmap = getThumbnail();
+            } catch (Throwable throwable) {
+                setException(throwable);
+                return;
+            }
+            if (bitmap == null && isCancelled()) {
+                cancelled();
+            } else {
+                setResult(bitmap);
+            }
+        }
 
-            while (true) {
-                DecodeTask task = nextDecodeTask();
-                if (task == null) break;
-                LocalMediaItem item = task.mItem;
-                try {
-                    mCurrentTask = task;
-                    mHandler.sendEmptyMessageDelayed(
-                            DECODE_TIMEOUT, task.mTimeout);
-                    Bitmap bitmap = task.mCanceled
-                            ? null
-                            : item.generateImage(resolver, task.mType);
-                    mHandler.removeMessages(DECODE_TIMEOUT);
-                    mCurrentTask = null;
-                    if (bitmap != null) {
-                        task.mItem.onImageReady(task.mType, bitmap);
-                        synchronized (mQueue) {
-                            mMap.remove(task.mRequestId);
-                        }
-                    } else if (task.mCanceled) {
-                        task.mItem.onImageCanceled(task.mType);
-                    } else {
-                        // Try to decode the image again by increasing the
-                        // timeout by a factor of 2 unless MAXIMAL_TIMEOUT
-                        // is reached.
-                        task.mTimeout <<= 1;
-                        if (task.mTimeout > MAXIMAL_TIMEOUT) {
-                            throw new RuntimeException("decode timeout");
-                        } else {
-                            synchronized (mQueue) {
-                                mQueue.add(task);
-                            }
-                        }
-                    }
-                } catch (Exception e) {
-                    Log.e(TAG, "decode error", e);
-                    task.mItem.onImageError(task.mType, e);
-                    synchronized (mQueue) {
-                        mMap.remove(task.mRequestId);
-                    }
+        @Override
+        protected void onCancel() {
+            if (mState.compareAndSet(STATE_RUNNING, STATE_CANCELED)) {
+                switch (mType) {
+                    case MediaItem.TYPE_THUMBNAIL:
+                    case MediaItem.TYPE_MICROTHUMBNAIL:
+                        // TODO: MediaProvider doesn't provide a way to specify
+                        //       which kind of thumbnail to be canceled. We should
+                        //       try to fix the issue in MediaProvider or here.
+                        Images.Thumbnails.cancelThumbnailRequest(mResolver, mId);
+                        break;
+                    default:
+                        throw new IllegalArgumentException();
                 }
             }
-            synchronized (mQueue) {
-                for (DecodeTask task : mQueue) {
-                    task.mItem.onImageCanceled(task.mType);
+        }
+
+        private Bitmap getThumbnail() {
+            Bitmap result = null;
+
+            switch (mType) {
+                case MediaItem.TYPE_THUMBNAIL:
+                    if (mState.compareAndSet(STATE_READY, STATE_RUNNING)) {
+                        result =  Images.Thumbnails.getThumbnail(
+                                mResolver, mId, Images.Thumbnails.MINI_KIND, null);
+                        mState.compareAndSet(STATE_RUNNING, STATE_RAN);
+                    }
+                    break;
+                case MediaItem.TYPE_MICROTHUMBNAIL: {
+                    if (mState.compareAndSet(STATE_READY, STATE_RUNNING)) {
+                        result = Images.Thumbnails.getThumbnail(
+                            mResolver, mId, Images.Thumbnails.MINI_KIND, null);
+                        mState.compareAndSet(STATE_RUNNING, STATE_RAN);
+                        if (result != null) {
+                            result = Utils.resize(result, MICRO_TARGET_PIXELS);
+                        }
+                    }
+                    break;
                 }
-                mQueue.clear();
-                mMap.clear();
+                default:
+                    throw new IllegalArgumentException();
             }
+            return result;
         }
     }
 
-    private static class DecodeTask implements Comparable<DecodeTask> {
-        int mRequestId;
-        int mTimeout;
-        int mType;
-        volatile boolean mCanceled;
-        LocalMediaItem mItem;
+    private static class GetVideoThumbnail extends FutureHelper<Bitmap> implements Runnable {
+        private final int STATE_READY = 0;
+        private final int STATE_RUNNING = 1;
+        private final int STATE_CANCELED = 2;
+        private final int STATE_RAN = 4;
 
-        public int compareTo(DecodeTask task) {
-            return mTimeout != task.mTimeout
-                    ? mTimeout - task.mTimeout
-                    : mRequestId - task.mRequestId;
+        private AtomicInteger mState = new AtomicInteger(STATE_READY);
+        private final int mId;
+        private final int mType;
+        private final ContentResolver mResolver;
+
+        public GetVideoThumbnail(int id, int type, ContentResolver resolver,
+                FutureListener<? super Bitmap> listener) {
+            super(listener);
+            mId = id;
+            mType = type;
+            mResolver= resolver;
+        }
+
+        public void run() {
+            Bitmap bitmap = null;
+            try {
+                bitmap = getThumbnail();
+            } catch (Throwable throwable) {
+                setException(throwable);
+                return;
+            }
+            if (bitmap == null && isCancelled()) {
+                cancelled();
+            } else {
+                setResult(bitmap);
+            }
+        }
+
+        @Override
+        protected void onCancel() {
+            if (mState.compareAndSet(STATE_RUNNING, STATE_CANCELED)) {
+                // TODO: fix the issue that we cannot cancel only one request
+                Video.Thumbnails.cancelThumbnailRequest(mResolver, mId);
+            }
+        }
+
+        private Bitmap getThumbnail() {
+            Bitmap result = null;
+            switch (mType) {
+                case MediaItem.TYPE_FULL_IMAGE:
+                case MediaItem.TYPE_THUMBNAIL:
+                    if (mState.compareAndSet(STATE_READY, STATE_RUNNING)) {
+                        result =  Images.Thumbnails.getThumbnail(
+                                mResolver, mId, Images.Thumbnails.MINI_KIND, null);
+                        mState.compareAndSet(STATE_RUNNING, STATE_RAN);
+                    }
+                    break;
+                case MediaItem.TYPE_MICROTHUMBNAIL: {
+                    if (mState.compareAndSet(STATE_READY, STATE_RUNNING)) {
+                        result = Video.Thumbnails.getThumbnail(
+                                mResolver, mId, Video.Thumbnails.MINI_KIND, null);
+                        mState.compareAndSet(STATE_RUNNING, STATE_RAN);
+                        if (result != null) {
+                            result = Utils.resize(result, MICRO_TARGET_PIXELS);
+                        }
+                    }
+                    break;
+                }
+                default:
+                    throw new IllegalArgumentException();
+            }
+            return result;
         }
     }
+
 }
diff --git a/new3d/src/com/android/gallery3d/data/LocalAlbum.java b/new3d/src/com/android/gallery3d/data/LocalAlbum.java
index ed76a92..f1de362 100644
--- a/new3d/src/com/android/gallery3d/data/LocalAlbum.java
+++ b/new3d/src/com/android/gallery3d/data/LocalAlbum.java
@@ -103,7 +103,7 @@
         try {
             while (cursor.moveToNext()) {
                 if (mIsImage) {
-                    list.add(LocalImage.load(imageService, cursor, dataManager));
+                    list.add(LocalImage.load(mContext, cursor, dataManager));
                 } else {
                     list.add(LocalVideo.load(imageService, cursor, dataManager));
                 }
diff --git a/new3d/src/com/android/gallery3d/data/LocalImage.java b/new3d/src/com/android/gallery3d/data/LocalImage.java
index 6aad1dc..b6751cf 100644
--- a/new3d/src/com/android/gallery3d/data/LocalImage.java
+++ b/new3d/src/com/android/gallery3d/data/LocalImage.java
@@ -16,25 +16,21 @@
 
 package com.android.gallery3d.data;
 
-import com.android.gallery3d.util.Utils;
-
-import android.content.ContentResolver;
 import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
-import android.provider.MediaStore.Images;
 import android.provider.MediaStore.Images.ImageColumns;
-import android.util.Log;
 
-import java.io.BufferedInputStream;
-import java.io.FileInputStream;
-import java.io.IOException;
+import com.android.gallery3d.app.GalleryContext;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+
+import java.io.File;
 
 // LocalImage represents an image in the local storage.
 public class LocalImage extends LocalMediaItem {
 
     private static final int MICRO_TARGET_PIXELS = 128 * 128;
-    private static final int JPEG_MARK_POSITION = 60 * 1024;
 
     private static final int FULLIMAGE_TARGET_SIZE = 2048;
     private static final int FULLIMAGE_MAX_NUM_PIXELS = 5 * 1024 * 1024;
@@ -69,91 +65,38 @@
 
     private long mUniqueId;
     private int mRotation;
+    private final GalleryContext mContext;
 
-    protected LocalImage(ImageService imageService) {
-        super(imageService);
+    protected LocalImage(GalleryContext context) {
+        mContext = context;
     }
 
+    @Override
     public long getUniqueId() {
         return mUniqueId;
     }
 
-    protected Bitmap decodeImage(String path) throws IOException {
-        // TODO: need to figure out why simply setting JPEG_MARK_POSITION doesn't work!
-        BufferedInputStream bis = new BufferedInputStream(
-                new FileInputStream(path), JPEG_MARK_POSITION);
-        try {
-            // Decode bufferedInput for calculating a sample size.
-            final BitmapFactory.Options options = mOptions;
-            options.inJustDecodeBounds = true;
-            bis.mark(JPEG_MARK_POSITION);
-            BitmapFactory.decodeStream(bis, null, options);
-            if (options.mCancel) return null;
-
-            try {
-                bis.reset();
-            } catch (IOException e) {
-                Log.w(TAG, "failed in resetting the buffer after reading the jpeg header", e);
-                bis.close();
-                bis = new BufferedInputStream(new FileInputStream(path));
-            }
-
-            options.inSampleSize =  Utils.computeSampleSize(options,
-                    FULLIMAGE_TARGET_SIZE, FULLIMAGE_MAX_NUM_PIXELS);
-            options.inJustDecodeBounds = false;
-            return BitmapFactory.decodeStream(bis, null, options);
-        } finally {
-            bis.close();
-        }
-    }
-
     @Override
-    protected void cancelImageGeneration(ContentResolver resolver, int type) {
-        switch (type) {
-            case TYPE_FULL_IMAGE:
-                mOptions.requestCancelDecode();
-                break;
-            case TYPE_THUMBNAIL:
-            case TYPE_MICROTHUMBNAIL:
-                Images.Thumbnails.cancelThumbnailRequest(resolver, mId);
-                break;
-            default:
-                throw new IllegalArgumentException();
+    public synchronized Future<Bitmap>
+            requestImage(int type, FutureListener<? super Bitmap> listener) {
+        if (type == TYPE_FULL_IMAGE) {
+            return mContext.getDecodeService().requestDecode(
+                    new File(mFilePath), null, FULLIMAGE_TARGET_SIZE,
+                    FULLIMAGE_MAX_NUM_PIXELS, listener);
+        } else {
+            return mContext.getImageService()
+                    .requestImageThumbnail(mId, type, listener);
         }
     }
 
-    @Override
-    protected Bitmap generateImage(ContentResolver resolver, int type)
-            throws Exception {
-
-        switch (type) {
-            case TYPE_FULL_IMAGE: {
-                mOptions.mCancel = false;
-                return decodeImage(mFilePath);
-            }
-            case TYPE_THUMBNAIL:
-                return Images.Thumbnails.getThumbnail(
-                        resolver, mId, Images.Thumbnails.MINI_KIND, null);
-            case TYPE_MICROTHUMBNAIL: {
-                Bitmap bitmap = Images.Thumbnails.getThumbnail(
-                        resolver, mId, Images.Thumbnails.MINI_KIND, null);
-                return bitmap == null
-                        ? null
-                        : Utils.resize(bitmap, MICRO_TARGET_PIXELS);
-            }
-            default:
-                throw new IllegalArgumentException();
-        }
-    }
-
-    public static LocalImage load(ImageService imageService, Cursor cursor,
+    public static LocalImage load(GalleryContext context, Cursor cursor,
             DataManager dataManager) {
         int itemId = cursor.getInt(INDEX_ID);
         long uniqueId = DataManager.makeId(DataManager.ID_LOCAL_IMAGE, itemId);
         LocalImage item = (LocalImage) dataManager.getFromCache(uniqueId);
         if (item != null) return item;
 
-        item = new LocalImage(imageService);
+        item = new LocalImage(context);
         dataManager.putToCache(uniqueId, item);
 
         item.mId = itemId;
diff --git a/new3d/src/com/android/gallery3d/data/LocalMediaItem.java b/new3d/src/com/android/gallery3d/data/LocalMediaItem.java
index 3c1a0ca..a3b8bd0 100644
--- a/new3d/src/com/android/gallery3d/data/LocalMediaItem.java
+++ b/new3d/src/com/android/gallery3d/data/LocalMediaItem.java
@@ -16,13 +16,6 @@
 
 package com.android.gallery3d.data;
 
-import android.content.ContentResolver;
-import android.graphics.Bitmap;
-
-import com.android.gallery3d.util.Future;
-import com.android.gallery3d.util.FutureHelper;
-import com.android.gallery3d.util.FutureListener;
-
 //
 // LocalMediaItem is an abstract class captures those common fields
 // in LocalImage and LocalVideo.
@@ -41,69 +34,4 @@
     protected long mDateAddedInSec;
     protected long mDateModifiedInSec;
     protected String mFilePath;
-
-    protected int mRequestId[];
-    private MyFuture mFutureBitmaps[];
-
-    protected final ImageService mImageService;
-
-    protected LocalMediaItem(ImageService imageService) {
-        mImageService = imageService;
-        mFutureBitmaps = new MyFuture[TYPE_COUNT];
-        mRequestId = new int[TYPE_COUNT];
-    }
-
-    public synchronized Future<Bitmap>
-            requestImage(int type, FutureListener<? super Bitmap> listener) {
-        if (mFutureBitmaps[type] != null) {
-            // TODO: we should not allow overlapped requests
-            return null;
-        } else {
-            mFutureBitmaps[type] = new MyFuture(type, listener);
-            mRequestId[type] = mImageService.requestImage(this, type);
-            return mFutureBitmaps[type];
-        }
-    }
-
-    private synchronized void cancelImageRequest(int type) {
-        mImageService.cancelRequest(mRequestId[type]);
-        mFutureBitmaps[type] = null;
-    }
-
-    protected synchronized void onImageReady(int type, Bitmap bitmap) {
-        FutureHelper<Bitmap> helper = mFutureBitmaps[type];
-        mFutureBitmaps[type] = null;
-        if (helper != null) helper.setResult(bitmap);
-    }
-
-    protected synchronized void onImageError(int type, Throwable e) {
-        FutureHelper<Bitmap> helper = mFutureBitmaps[type];
-        mFutureBitmaps[type] = null;
-        if (helper != null) helper.setException(e);
-    }
-
-    protected synchronized void onImageCanceled(int type) {
-        FutureHelper<Bitmap> helper = mFutureBitmaps[type];
-        if (helper != null) helper.cancelled();
-    }
-
-    abstract protected Bitmap generateImage(
-            ContentResolver resolver, int type) throws Exception;
-
-    abstract protected void cancelImageGeneration(
-            ContentResolver resolver, int type);
-
-    private class MyFuture extends FutureHelper<Bitmap> {
-        private final int mSizeType;
-
-        public MyFuture(int sizeType, FutureListener<? super Bitmap> listener) {
-            super(listener);
-            mSizeType = sizeType;
-        }
-
-        @Override
-        public void onCancel() {
-            cancelImageRequest(mSizeType);
-        }
-    }
 }
diff --git a/new3d/src/com/android/gallery3d/data/LocalVideo.java b/new3d/src/com/android/gallery3d/data/LocalVideo.java
index 21328ff..5bb2fb7 100644
--- a/new3d/src/com/android/gallery3d/data/LocalVideo.java
+++ b/new3d/src/com/android/gallery3d/data/LocalVideo.java
@@ -16,14 +16,13 @@
 
 package com.android.gallery3d.data;
 
-import com.android.gallery3d.util.Utils;
-
-import android.content.ContentResolver;
 import android.database.Cursor;
 import android.graphics.Bitmap;
-import android.provider.MediaStore.Video;
 import android.provider.MediaStore.Video.VideoColumns;
 
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+
 // LocalVideo represents a video in the local storage.
 public class LocalVideo extends LocalMediaItem {
 
@@ -56,38 +55,21 @@
 
     private long mUniqueId;
     public int mDurationInSec;
+    private final ImageService mImageService;
 
     protected LocalVideo(ImageService imageService) {
-        super(imageService);
+        mImageService = imageService;
     }
 
+    @Override
     public long getUniqueId() {
         return mUniqueId;
     }
 
     @Override
-    protected void cancelImageGeneration(ContentResolver resolver, int type) {
-        Video.Thumbnails.cancelThumbnailRequest(resolver, mId);
-    }
-
-    @Override
-    protected Bitmap generateImage(ContentResolver resolver, int type) {
-        switch (type) {
-            // Return a MINI_KIND bitmap in the cases of TYPE_FULL_IMAGE
-            // and TYPE_THUMBNAIL.
-            case TYPE_FULL_IMAGE:
-            case TYPE_THUMBNAIL:
-                return Video.Thumbnails.getThumbnail(
-                        resolver, mId, Video.Thumbnails.MINI_KIND, null);
-            case TYPE_MICROTHUMBNAIL:
-                Bitmap bitmap = Video.Thumbnails.getThumbnail(
-                        resolver, mId, Video.Thumbnails.MINI_KIND, null);
-                return bitmap == null
-                        ? null
-                        : Utils.resize(bitmap, MICRO_TARGET_PIXELS);
-            default:
-                throw new IllegalArgumentException();
-        }
+    public synchronized Future<Bitmap>
+            requestImage(int type, FutureListener<? super Bitmap> listener) {
+        return mImageService.requestVideoThumbnail(mId, type, listener);
     }
 
     public static LocalVideo load(ImageService imageService, Cursor cursor,
diff --git a/new3d/src/com/android/gallery3d/data/PicasaImage.java b/new3d/src/com/android/gallery3d/data/PicasaImage.java
index 92d599b..29f2cc8 100644
--- a/new3d/src/com/android/gallery3d/data/PicasaImage.java
+++ b/new3d/src/com/android/gallery3d/data/PicasaImage.java
@@ -31,13 +31,11 @@
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.nio.ByteBuffer;
-import java.util.Arrays;
 
 // PicasaImage is an image in the Picasa account.
 public class PicasaImage extends MediaItem {
     private static final String TAG = "PicasaImage";
 
-    private final PicasaTask[] mTasks = new PicasaTask[MediaItem.TYPE_COUNT];
     private final GalleryContext mContext;
     private final PhotoEntry mData;
     private final BlobCache mPicasaCache;
@@ -51,46 +49,42 @@
                 DataManager.ID_PICASA_IMAGE, (int) entry.id);
     }
 
+    @Override
     public long getUniqueId() {
         return mUniqueId;
     }
 
+    @Override
     public synchronized Future<Bitmap>
             requestImage(int type, FutureListener<? super Bitmap> listener) {
-        if (mTasks[type] != null) {
-            // TODO: enable the check when cancelling is done
-            // throw new IllegalStateException();
-        } else {
-            URL photoUrl = getPhotoUrl(type);
-            if (mPicasaCache != null) {
+        URL photoUrl = getPhotoUrl(type);
+        if (mPicasaCache != null) {
 
-                // Try to get the image from cache.
-                LookupRequest request = new LookupRequest();
-                request.key = Utils.crc64Long(photoUrl.toString());
-                boolean isCached = false;
-                try {
-                    isCached = mPicasaCache.lookup(request);
-                } catch (IOException e) {
-                    Log.w(TAG, "IOException in getting an image from " +
-                            "PicasaCache", e);
-                }
-
-                if (isCached) {
-                    byte[] uri = Utils.getBytesInUtf8(photoUrl.toString());
-                    if (isSameUri(uri, request.buffer)) {
-                        Log.i(TAG, "Get Image from Cache (type, url): " + type +
-                                " " + photoUrl.toString());
-                        DecodeService service = mContext.getDecodeService();
-                        return service.requestDecode(request.buffer, uri.length,
-                                request.length - uri.length, null, listener);
-                    }
-                }
+            // Try to get the image from cache.
+            LookupRequest request = new LookupRequest();
+            request.key = Utils.crc64Long(photoUrl.toString());
+            boolean isCached = false;
+            try {
+                isCached = mPicasaCache.lookup(request);
+            } catch (IOException e) {
+                Log.w(TAG, "IOException in getting an image from " +
+                        "PicasaCache", e);
             }
 
-            // Get the image from Picasaweb instead.
-            mTasks[type] = new PicasaTask(type, photoUrl, listener);
+            if (isCached) {
+                byte[] uri = Utils.getBytesInUtf8(photoUrl.toString());
+                if (isSameUri(uri, request.buffer)) {
+                    Log.i(TAG, "Get Image from Cache (type, url): " + type +
+                            " " + photoUrl.toString());
+                    DecodeService service = mContext.getDecodeService();
+                    return service.requestDecode(request.buffer, uri.length,
+                            request.length - uri.length, null, listener);
+                }
+            }
         }
-        return mTasks[type];
+
+        // Get the image from Picasaweb instead.
+        return new PicasaTask(type, photoUrl, listener);
     }
 
     private boolean isSameUri(byte[] uri, byte[] buffer) {
@@ -168,12 +162,6 @@
                     DecodeService service = mContext.getDecodeService();
                     return service.requestDecode(downloadedImage, null, this);
                 }
-                case 2: {
-                    synchronized (PicasaImage.this) {
-                        mTasks[mType] = null;
-                    }
-                    break;
-                }
             }
             return null;
         }
diff --git a/new3d/src/com/android/gallery3d/ui/ScrollerHelper.java b/new3d/src/com/android/gallery3d/ui/ScrollerHelper.java
index 5a32cc3..c3c42a4 100644
--- a/new3d/src/com/android/gallery3d/ui/ScrollerHelper.java
+++ b/new3d/src/com/android/gallery3d/ui/ScrollerHelper.java
@@ -24,6 +24,7 @@
 
     private static final int ANIM_KIND_FLING = 1;
     private static final int ANIM_KIND_SCROLL = 2;
+
     private static final int DECELERATED_FACTOR = 4;
 
     private long mStartTime = NO_ANIMATION;
@@ -55,10 +56,11 @@
             if (mAnimationKind == ANIM_KIND_SCROLL) {
                 f = 1 - f;  // linear
             } else if (mAnimationKind == ANIM_KIND_FLING) {
-                f = 1 - f * f * f * f;  // x ^ DECELERATED_FACTOR
+                f = 1 - (float) Math.pow(f, DECELERATED_FACTOR);
             }
             mPosition = Math.round(mStart + (mFinal - mStart) * f);
-            Log.v("Fling", String.format("mStart = %s, mFinal = %s, mPosition = %s, f = %s, progress = %s",
+            Log.v("Fling", String.format(
+                    "mStart = %s, mFinal = %s, mPosition = %s, f = %s, progress = %s",
                     mStart, mFinal, mPosition, f, progress));
             if (mPosition == mFinal) {
                 mStartTime = NO_ANIMATION;
@@ -87,16 +89,18 @@
 
     public void fling(float velocity, int min, int max) {
         /*
-         * The position formula: x = s + (e - s) * (1 - (1 - t / T) ^ d)
-         *     velocity formula: v = d * (e - s) * (1 - t / T) ^ (d - 1) / T
+         * The position formula: x(t) = s + (e - s) * (1 - (1 - t / T) ^ d)
+         *     velocity formula: v(t) = d * (e - s) * (1 - t / T) ^ (d - 1) / T
          * Thus,
          *     v0 = (e - s) / T * d => (e - s) = v0 * T / d
          */
         mStartTime = START_ANIMATION;
         mAnimationKind = ANIM_KIND_FLING;
         mStart = mPosition;
-        double x = Math.pow(Math.abs(velocity), 1.0 / (DECELERATED_FACTOR - 1));
-        mDuration = (int) Math.round(FLING_DURATION_PARAM * x);
+
+        // Ta = T_ref * (Va / V_ref) ^ (1 / (d - 1)); V_ref = 1 pixel/second;
+        mDuration = (int) Math.round(FLING_DURATION_PARAM
+                    * Math.pow(Math.abs(velocity), 1.0 / (DECELERATED_FACTOR - 1)));
         int distance = Math.round(
                 velocity * mDuration / DECELERATED_FACTOR / 1000);
         mFinal = Utils.clamp(mStart + distance, min, max);
diff --git a/new3d/src/com/android/gallery3d/util/FutureListener.java b/new3d/src/com/android/gallery3d/util/FutureListener.java
index 147030b..0770660 100644
--- a/new3d/src/com/android/gallery3d/util/FutureListener.java
+++ b/new3d/src/com/android/gallery3d/util/FutureListener.java
@@ -1,5 +1,5 @@
 package com.android.gallery3d.util;
 
 public interface FutureListener<V> {
-	public void onFutureDone(Future<? extends V> future);
+    public void onFutureDone(Future<? extends V> future);
 }
diff --git a/new3d/src/com/android/gallery3d/util/FutureTask.java b/new3d/src/com/android/gallery3d/util/FutureTask.java
index 0f4ff75..ca2c68c 100644
--- a/new3d/src/com/android/gallery3d/util/FutureTask.java
+++ b/new3d/src/com/android/gallery3d/util/FutureTask.java
@@ -17,99 +17,120 @@
  */
 public class FutureTask<V> implements Future<V>, Runnable {
 
-	private static final int STATE_READY = 0;
-	private static final int STATE_RUNNING = 1;
-	private static final int STATE_CANCELLED = 2;
-	private static final int STATE_INTERRUPTED = 4;
-	private static final int STATE_RAN = 8;
+    private static final int STATE_READY = 0;
+    private static final int STATE_RUNNING = 1;
+    private static final int STATE_CANCELLED = 2;
+    private static final int STATE_INTERRUPTED = 4;
+    private static final int STATE_RAN = 8;
 
-	private Callable<V> mCallable;
-	private final MyHelper mHelper;
-	private volatile Thread mRunner;
-	private AtomicInteger mState = new AtomicInteger(STATE_READY);
+    private Callable<V> mCallable;
+    private final MyHelper mHelper;
+    private volatile Thread mRunner;
+    private AtomicInteger mState = new AtomicInteger(STATE_READY);
+    private final boolean mInterruptible;
 
-	public FutureTask(Callable<V> callable, FutureListener<? super V> listener) {
-		mCallable = callable;
-		mHelper = new MyHelper(listener);
-	}
+    public FutureTask(Callable<V> callable, FutureListener<? super V> listener) {
+        this(callable, false, listener);
+    }
 
-	public void requestCancel() {
-		mHelper.requestCancel();
-	}
+    // @param interruptible Sets whether this task is interruptible, if true,
+    //         the thread running the task will be interrupted when canceling.
+    public FutureTask(Callable<V> callable,
+            boolean interruptible, FutureListener<? super V> listener) {
+        mInterruptible = interruptible;
+        mCallable = callable;
+        mHelper = new MyHelper(listener);
+    }
 
-	public V get() throws ExecutionException, InterruptedException {
-		return mHelper.get();
-	}
+    public void requestCancel() {
+        mHelper.requestCancel();
+    }
 
-	public V get(long duration, TimeUnit unit) throws ExecutionException,
-			InterruptedException, TimeoutException {
-		return mHelper.get(duration, unit);
-	}
+    public V get() throws ExecutionException, InterruptedException {
+        return mHelper.get();
+    }
 
-	public boolean isCancelled() {
-		return mHelper.isCancelled();
-	}
+    public V get(long duration, TimeUnit unit) throws ExecutionException,
+            InterruptedException, TimeoutException {
+        return mHelper.get(duration, unit);
+    }
 
-	public boolean isDone() {
-		return mHelper.isDone();
-	}
+    public boolean isCancelled() {
+        return mHelper.isCancelled();
+    }
 
-	public void run() {
-		mRunner = Thread.currentThread();
-		if (!mState.compareAndSet(STATE_READY, STATE_RUNNING)) return;
+    public boolean isDone() {
+        return mHelper.isDone();
+    }
 
-		try {
-			V result = mCallable.call();
-			mCallable = null;
-			if (result == null && mHelper.isCancelling()) {
-				mHelper.cancelled();
-			} else {
-				mHelper.setResult(result);
-			}
-		} catch (InterruptedException e) {
-			if (mHelper.isCancelling()) {
-				mHelper.cancelled();
-			} else {
-				mHelper.setException(e);
-			}
-		} catch (InterruptedIOException e) {
-			if (mHelper.isCancelling()) {
-				mHelper.cancelled();
-			} else {
-				mHelper.setException(e);
-			}
-		} catch (Throwable t) {
-			mHelper.setException(t);
-		}
+    public void run() {
+        mRunner = Thread.currentThread();
+        if (!mState.compareAndSet(STATE_READY, STATE_RUNNING)) return;
 
-		if (!mState.compareAndSet(STATE_RUNNING, STATE_RAN)) {
-			// STATE_INTERRUPTED
-			synchronized (this) {
-				Thread.interrupted(); // consume the interrupted signal
-			}
-		}
-		mRunner = null;
-	}
-
-	protected synchronized void onCancel() {
-        if (mState.compareAndSet(STATE_RUNNING, STATE_INTERRUPTED)){
-            mRunner.interrupt();
+        boolean noException = false;
+        V result = null;
+        try {
+            result = mCallable.call();
+            noException = true;
+        } catch (InterruptedException e) {
+            if (mHelper.isCancelling()) {
+                mHelper.cancelled();
+            } else {
+                mHelper.setException(e);
+            }
+        } catch (InterruptedIOException e) {
+            if (mHelper.isCancelling()) {
+                mHelper.cancelled();
+            } else {
+                mHelper.setException(e);
+            }
+        } catch (Throwable t) {
+            mHelper.setException(t);
+        } finally {
+            mCallable = null;
         }
-	}
 
-	private class MyHelper extends FutureHelper<V> {
+        if (noException) {
+            if (result == null && mHelper.isCancelling()) {
+                mHelper.cancelled();
+            } else {
+                mHelper.setResult(result);
+            }
+        }
 
-		MyHelper(FutureListener<? super V> listener) {
-			super(listener);
-		}
+        if (mInterruptible &&
+                !mState.compareAndSet(STATE_RUNNING, STATE_RAN)) {
+            // STATE_INTERRUPTED
+            synchronized (this) {
+                Thread.interrupted(); // consume the interrupted signal
+            }
+        }
+        mRunner = null;
+    }
 
-		@Override
+    protected void onRequestCancel() {
+    }
+
+    protected synchronized void cancelTask() {
+        if (mState.compareAndSet(STATE_RUNNING, STATE_INTERRUPTED)){
+            if (mInterruptible) mRunner.interrupt();
+            onRequestCancel();
+        }
+    }
+
+    private class MyHelper extends FutureHelper<V> {
+
+        MyHelper(FutureListener<? super V> listener) {
+            super(listener);
+        }
+
+        @Override
         protected void onCancel() {
-	        if (mState.compareAndSet(STATE_READY, STATE_CANCELLED)) {
-	            cancelled();
-	        } else if (mState.get() == STATE_RUNNING) {
-	            FutureTask.this.onCancel();
-	        } // else mState == STATE_DONE;
-		}
-	}
+            if (mState.compareAndSet(STATE_READY, STATE_CANCELLED)) {
+                cancelled();
+            } else if (mState.get() == STATE_RUNNING) {
+                FutureTask.this.cancelTask();
+            } // else mState == STATE_DONE;
+        }
+    }
 }