OSDN Git Service

Best effort fix to prevent stretched videos.
authorSam Judd <judds@google.com>
Mon, 17 Mar 2014 20:07:22 +0000 (13:07 -0700)
committerSam Judd <judds@google.com>
Fri, 21 Mar 2014 20:16:08 +0000 (13:16 -0700)
To avoid increasing load times, we now only parse video headers asynchronously
while the user scrolls. We make a best effort attempt to prefetch video headers
while we're scrolling to avoid the layout jumping around when we update
dimensions for an item that's visible to the user.

Bug: 13505062
Change-Id: Ib7d7835c39d50f22f45db5673ec4c49d84b81124

14 files changed:
src/com/android/camera/CameraActivity.java
src/com/android/camera/VideoModule.java
src/com/android/camera/data/AbstractLocalDataAdapterWrapper.java
src/com/android/camera/data/CameraDataAdapter.java
src/com/android/camera/data/FixedFirstDataAdapter.java
src/com/android/camera/data/FixedLastDataAdapter.java
src/com/android/camera/data/LocalDataAdapter.java
src/com/android/camera/data/LocalDataUtil.java
src/com/android/camera/data/LocalMediaData.java
src/com/android/camera/data/MetadataLoader.java
src/com/android/camera/data/VideoRotationMetadataLoader.java [new file with mode: 0644]
src/com/android/camera/filmstrip/FilmstripController.java
src/com/android/camera/widget/FilmstripView.java
src/com/android/camera/widget/Preloader.java [new file with mode: 0644]

index da0c559..e07750f 100644 (file)
@@ -43,6 +43,7 @@ import android.net.Uri;
 import android.nfc.NfcAdapter;
 import android.nfc.NfcAdapter.CreateBeamUrisCallback;
 import android.nfc.NfcEvent;
+import android.os.AsyncTask;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
@@ -122,6 +123,7 @@ import com.android.camera.util.PhotoSphereHelper.PanoramaViewHelper;
 import com.android.camera.util.ReleaseDialogHelper;
 import com.android.camera.util.UsageStatistics;
 import com.android.camera.widget.FilmstripView;
+import com.android.camera.widget.Preloader;
 import com.android.camera2.R;
 import com.google.common.logging.eventprotos;
 import com.google.common.logging.eventprotos.CameraEvent.InteractionCause;
@@ -163,6 +165,8 @@ public class CameraActivity extends Activity
     private static final int MSG_CLEAR_SCREEN_ON_FLAG = 2;
     private static final long SCREEN_DELAY_MS = 2 * 60 * 1000; // 2 mins.
     private static final int MAX_PEEK_BITMAP_PIXELS = 1600000; // 1.6 * 4 MBs.
+    /** Load metadata for 10 items ahead of our current. */
+    private static final int FILMSTRIP_PRELOAD_AHEAD_ITEMS = 10;
 
     /** Should be used wherever a context is needed. */
     private Context mAppContext;
@@ -228,6 +232,7 @@ public class CameraActivity extends Activity
     private long mOnCreateTime;
 
     private Menu mActionBarMenu;
+    private Preloader<Integer, AsyncTask> mPreloader;
 
     @Override
     public CameraAppUI getCameraAppUI() {
@@ -630,6 +635,11 @@ public class CameraActivity extends Activity
                         }
                     });
                 }
+
+                @Override
+                public void onScroll(int firstVisiblePosition, int visibleItemCount, int totalItemCount) {
+                    mPreloader.onScroll(null /*absListView*/, firstVisiblePosition, visibleItemCount, totalItemCount);
+                }
             };
 
     private final LocalDataAdapter.LocalDataListener mLocalDataListener =
@@ -1268,6 +1278,9 @@ public class CameraActivity extends Activity
                 new ColorDrawable(getResources().getColor(R.color.photo_placeholder)));
         mDataAdapter.setLocalDataListener(mLocalDataListener);
 
+        mPreloader = new Preloader<Integer, AsyncTask>(FILMSTRIP_PRELOAD_AHEAD_ITEMS, mDataAdapter,
+                mDataAdapter);
+
         mCameraAppUI.getFilmstripContentPanel().setFilmstripListener(mFilmstripListener);
 
         mLocationManager = new LocationManager(mAppContext);
@@ -1424,6 +1437,7 @@ public class CameraActivity extends Activity
         mLocalImagesObserver.setForegroundChangeListener(null);
         mLocalImagesObserver.setActivityPaused(true);
         mLocalVideosObserver.setActivityPaused(true);
+        mPreloader.cancelAllLoads();
         resetScreenOn();
         super.onPause();
     }
index 36decc7..92f426c 100644 (file)
@@ -1135,6 +1135,8 @@ public class VideoModule extends CameraModule
         mCurrentVideoValues.put(MediaColumns.DATE_MODIFIED, dateTaken / 1000);
         mCurrentVideoValues.put(Video.Media.MIME_TYPE, mime);
         mCurrentVideoValues.put(Video.Media.DATA, path);
+        mCurrentVideoValues.put(Video.Media.WIDTH, mProfile.videoFrameWidth);
+        mCurrentVideoValues.put(Video.Media.HEIGHT, mProfile.videoFrameHeight);
         mCurrentVideoValues.put(Video.Media.RESOLUTION,
                 Integer.toString(mProfile.videoFrameWidth) + "x" +
                 Integer.toString(mProfile.videoFrameHeight));
index af69dc9..9481b57 100644 (file)
@@ -18,6 +18,9 @@ package com.android.camera.data;
 
 import android.content.Context;
 import android.net.Uri;
+import android.os.AsyncTask;
+
+import java.util.List;
 
 /**
  * An abstract {@link LocalDataAdapter} implementation to wrap another
@@ -102,12 +105,32 @@ public abstract class AbstractLocalDataAdapterWrapper implements LocalDataAdapte
     }
 
     @Override
-    public void updateMetadata(int dataId) {
-        mAdapter.updateMetadata(dataId);
+    public AsyncTask updateMetadata(int dataId) {
+        return mAdapter.updateMetadata(dataId);
     }
 
     @Override
     public boolean isMetadataUpdated(int dataId) {
         return mAdapter.isMetadataUpdated(dataId);
     }
+
+    @Override
+    public List<AsyncTask> preloadItems(List<Integer> items) {
+        return mAdapter.preloadItems(items);
+    }
+
+    @Override
+    public void cancelItems(List<AsyncTask> loadTokens) {
+        mAdapter.cancelItems(loadTokens);
+    }
+
+    @Override
+    public List<Integer> getItemsInRange(int startPosition, int endPosition) {
+        return mAdapter.getItemsInRange(startPosition, endPosition);
+    }
+
+    @Override
+    public int getCount() {
+        return mAdapter.getCount();
+    }
 }
index cbb301d..5e4c860 100644 (file)
@@ -72,12 +72,14 @@ public class CameraDataAdapter implements LocalDataAdapter {
     @Override
     public void requestLoad() {
         QueryTask qtask = new QueryTask();
-        qtask.execute(mContext.getContentResolver());
+        qtask.execute(mContext);
     }
 
     @Override
-    public void updateMetadata(int dataId) {
-        new MetadataUpdateTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, dataId);
+    public AsyncTask updateMetadata(int dataId) {
+        MetadataUpdateTask result = new MetadataUpdateTask();
+        result.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, dataId);
+        return result;
     }
 
     @Override
@@ -253,6 +255,7 @@ public class CameraDataAdapter implements LocalDataAdapter {
         for (; pos < mImages.size()
                 && comp.compare(data, mImages.get(pos)) > 0; pos++);
         mImages.add(pos, data);
+        updateMetadata(pos);
         if (mListener != null) {
             mListener.onDataInserted(pos, data);
         }
@@ -269,6 +272,40 @@ public class CameraDataAdapter implements LocalDataAdapter {
         }
     }
 
+    @Override
+    public List<AsyncTask> preloadItems(List<Integer> items) {
+        List<AsyncTask> result = new ArrayList<AsyncTask>();
+        for (Integer id : items) {
+            if (!isMetadataUpdated(id)) {
+                result.add(updateMetadata(id));
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public void cancelItems(List<AsyncTask> loadTokens) {
+        for (AsyncTask asyncTask : loadTokens) {
+            if (asyncTask != null) {
+                asyncTask.cancel(false);
+            }
+        }
+    }
+
+    @Override
+    public List<Integer> getItemsInRange(int startPosition, int endPosition) {
+        List<Integer> result = new ArrayList<Integer>();
+        for (int i = Math.max(0, startPosition); i < endPosition; i++) {
+            result.add(i);
+        }
+        return result;
+    }
+
+    @Override
+    public int getCount() {
+        return getTotalNumber();
+    }
+
     private class LoadNewPhotosTask extends AsyncTask<ContentResolver, Void, List<LocalData>> {
 
         private long mMinPhotoId;
@@ -314,20 +351,23 @@ public class CameraDataAdapter implements LocalDataAdapter {
         }
     }
 
-    private class QueryTask extends AsyncTask<ContentResolver, Void, QueryTaskResult> {
+    private class QueryTask extends AsyncTask<Context, Void, QueryTaskResult> {
+        // The maximum number of data to load metadata for in a single task.
+        private static final int MAX_METADATA = 5;
 
         /**
          * Loads all the photo and video data in the camera folder in background
          * and combine them into one single list.
          *
-         * @param contentResolvers {@link ContentResolver} to load all the data.
+         * @param contexts {@link Context} to load all the data.
          * @return An {@link com.android.camera.data.CameraDataAdapter.QueryTaskResult} containing
          *  all loaded data and the highest photo id in the dataset.
          */
         @Override
-        protected QueryTaskResult doInBackground(ContentResolver... contentResolvers) {
+        protected QueryTaskResult doInBackground(Context... contexts) {
+            final Context context = contexts[0];
+            final ContentResolver cr = context.getContentResolver();
             LocalDataList l = new LocalDataList();
-            final ContentResolver cr = contentResolvers[0];
             // Photos
             List<LocalData> photoData = LocalMediaData.PhotoData.query(cr,
                     LocalMediaData.PhotoData.CONTENT_URI, LocalMediaData.QUERY_ALL_MEDIA_ID);
@@ -343,6 +383,12 @@ public class CameraDataAdapter implements LocalDataAdapter {
             l.addAll(videoData);
             l.sort(new LocalData.NewestFirstComparator());
 
+            // Load enough metadata so it's already loaded when we open the filmstrip.
+            for (int i = 0; i < MAX_METADATA && i < l.size(); i++) {
+                LocalData data = l.get(i);
+                MetadataLoader.loadMetadata(context, data);
+            }
+
             return new QueryTaskResult(l, lastPhotoId);
         }
 
@@ -378,11 +424,9 @@ public class CameraDataAdapter implements LocalDataAdapter {
                     continue;
                 }
                 final LocalData data = mImages.get(id);
-                if (data.getLocalDataType() != LocalData.LOCAL_IMAGE) {
-                    continue;
+                if (MetadataLoader.loadMetadata(mContext, data)) {
+                    updatedList.add(id);
                 }
-                MetadataLoader.loadMetadata(mContext, data);
-                updatedList.add(id);
             }
             return updatedList;
         }
index 12d836e..71ee632 100644 (file)
@@ -18,6 +18,7 @@ package com.android.camera.data;
 
 import android.content.Context;
 import android.net.Uri;
+import android.os.AsyncTask;
 import android.view.View;
 
 import com.android.camera.filmstrip.DataAdapter;
@@ -192,18 +193,19 @@ public class FixedFirstDataAdapter extends AbstractLocalDataAdapterWrapper
     }
 
     @Override
-    public void updateMetadata(int dataId) {
+    public AsyncTask updateMetadata(int dataId) {
         if (dataId > 0) {
-            mAdapter.updateMetadata(dataId);
+            return mAdapter.updateMetadata(dataId - 1);
         } else {
             MetadataLoader.loadMetadata(mContext, mFirstData);
         }
+        return null;
     }
 
     @Override
     public boolean isMetadataUpdated(int dataId) {
         if (dataId > 0) {
-        return mAdapter.isMetadataUpdated(dataId);
+        return mAdapter.isMetadataUpdated(dataId - 1);
         } else {
             return mFirstData.isMetadataUpdated();
         }
index f4afbf1..c94c1b4 100644 (file)
@@ -18,6 +18,7 @@ package com.android.camera.data;
 
 import android.content.Context;
 import android.net.Uri;
+import android.os.AsyncTask;
 import android.view.View;
 
 import com.android.camera.filmstrip.ImageData;
@@ -154,12 +155,13 @@ public class FixedLastDataAdapter extends AbstractLocalDataAdapterWrapper {
     }
 
     @Override
-    public void updateMetadata(int dataId) {
+    public AsyncTask updateMetadata(int dataId) {
         if (dataId < mAdapter.getTotalNumber()) {
-            mAdapter.updateMetadata(dataId);
+            return mAdapter.updateMetadata(dataId);
         } else {
             MetadataLoader.loadMetadata(mContext, mLastData);
         }
+        return null;
     }
 
     @Override
@@ -167,7 +169,7 @@ public class FixedLastDataAdapter extends AbstractLocalDataAdapterWrapper {
         if (dataId < mAdapter.getTotalNumber()) {
             return mAdapter.isMetadataUpdated(dataId);
         } else {
-            return MetadataLoader.isMetadataLoaded(mLastData);
+            return MetadataLoader.isMetadataCached(mLastData);
         }
     }
 }
index 90b895a..e5047ee 100644 (file)
@@ -18,7 +18,9 @@ package com.android.camera.data;
 
 import android.net.Uri;
 
+import android.os.AsyncTask;
 import com.android.camera.filmstrip.DataAdapter;
+import com.android.camera.widget.Preloader;
 
 import java.util.List;
 
@@ -26,7 +28,8 @@ import java.util.List;
  * An interface which extends {@link com.android.camera.filmstrip.DataAdapter}
  * and defines operations on the data in the local camera folder.
  */
-public interface LocalDataAdapter extends DataAdapter {
+public interface LocalDataAdapter extends DataAdapter,
+        Preloader.ItemLoader<Integer, AsyncTask>, Preloader.ItemSource<Integer> {
 
     public interface LocalDataListener {
         /**
@@ -127,8 +130,10 @@ public interface LocalDataAdapter extends DataAdapter {
      * {@link com.android.camera.data.LocalDataAdapter.LocalDataListener}.
      *
      * @param dataId The ID of the data to update the metadata for.
+     * @return An {@link android.os.AsyncTask} performing the background load
+     *      that can be used to cancel the load if it's no longer needed.
      */
-    public void updateMetadata(int dataId);
+    public AsyncTask updateMetadata(int dataId);
 
     /**
      * @return whether the metadata is already updated.
index f2df1ee..7877116 100644 (file)
@@ -66,6 +66,7 @@ public class LocalDataUtil {
         BitmapFactory.decodeFile(path, justBoundsOpts);
         if (justBoundsOpts.outWidth > 0 && justBoundsOpts.outHeight > 0) {
             size.set(justBoundsOpts.outWidth, justBoundsOpts.outHeight);
+        } else {
             Log.e(TAG, "Bitmap dimension decoding failed for " + path);
         }
         return size;
index 1c19dc5..62b44e8 100644 (file)
@@ -26,7 +26,7 @@ import android.graphics.BitmapFactory;
 import android.graphics.Point;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
-import android.media.MediaMetadataRetriever;
+import android.media.CamcorderProfile;
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Bundle;
@@ -128,6 +128,8 @@ public abstract class LocalMediaData implements LocalData {
                             + cursor.getString(dataIndex));
                 }
             }
+
+            cursor.close();
         }
         return result;
     }
@@ -299,7 +301,7 @@ public abstract class LocalMediaData implements LocalData {
 
     @Override
     public boolean isMetadataUpdated() {
-        return MetadataLoader.isMetadataLoaded(this);
+        return MetadataLoader.isMetadataCached(this);
     }
 
     /**
@@ -663,6 +665,44 @@ public abstract class LocalMediaData implements LocalData {
                     new VideoDataBuilder());
         }
 
+        /**
+         * We can't trust the media store and we can't afford the performance overhead of
+         * synchronously decoding the video header for every item when loading our data set
+         * from the media store, so we instead run the metadata loader in the background
+         * to decode the video header for each item and prefer whatever values it obtains.
+         */
+        private int getBestWidth() {
+            int metadataWidth = VideoRotationMetadataLoader.getWidth(this);
+            if (metadataWidth > 0) {
+                return metadataWidth;
+            } else {
+                return mWidth;
+            }
+        }
+
+        private int getBestHeight() {
+            int metadataHeight = VideoRotationMetadataLoader.getHeight(this);
+            if (metadataHeight > 0) {
+                return metadataHeight;
+            } else {
+                return mHeight;
+            }
+        }
+
+        /**
+         * If the metadata loader has determined from the video header that we need to rotate the video
+         * 90 or 270 degrees, then we swap the width and height.
+         */
+        @Override
+        public int getWidth() {
+            return VideoRotationMetadataLoader.isRotated(this) ? getBestHeight() : getBestWidth();
+        }
+
+        @Override
+        public int getHeight() {
+            return VideoRotationMetadataLoader.isRotated(this) ?  getBestWidth() : getBestHeight();
+        }
+
         private static VideoData buildFromCursor(Cursor c) {
             long id = c.getLong(COL_ID);
             String title = c.getString(COL_TITLE);
@@ -673,42 +713,15 @@ public abstract class LocalMediaData implements LocalData {
             int width = c.getInt(COL_WIDTH);
             int height = c.getInt(COL_HEIGHT);
 
-            // Extracts video height/width if available. If unavailable, set to
-            // 0.
+            // If the media store doesn't contain a width and a height, use the width and height
+            // of the default camera mode instead. When the metadata loader runs, it will set the
+            // correct values.
             if (width == 0 || height == 0) {
-                MediaMetadataRetriever retriever = new MediaMetadataRetriever();
-                String rotation = null;
-                try {
-                    retriever.setDataSource(path);
-                } catch (RuntimeException ex) {
-                    // setDataSource() can cause RuntimeException beyond
-                    // IllegalArgumentException. e.g: data contain *.avi file.
-                    retriever.release();
-                    Log.e(TAG, "MediaMetadataRetriever.setDataSource() fail:"
-                            + ex.getMessage());
-                    return null;
-                }
-                rotation = retriever.extractMetadata(
-                        MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
-
-                String val = retriever.extractMetadata(
-                        MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
-                width = (val == null) ? 0 : Integer.parseInt(val);
-                val = retriever.extractMetadata(
-                        MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
-                height = (val == null) ? 0 : Integer.parseInt(val);
-                retriever.release();
-                if (width == 0 || height == 0) {
-                    // Width or height is still not available.
-                    Log.e(TAG, "Unable to retrieve dimension of video:" + path);
-                    return null;
-                }
-                if (rotation != null
-                        && (rotation.equals("90") || rotation.equals("270"))) {
-                    int b = width;
-                    width = height;
-                    height = b;
-                }
+                Log.w(TAG, "failed to retrieve width and height from the media store, defaulting " +
+                        " to camera profile");
+                CamcorderProfile profile = CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH);
+                width = profile.videoFrameWidth;
+                height = profile.videoFrameHeight;
             }
 
             long sizeInBytes = c.getLong(COL_SIZE);
index 35bb88f..311f5cf 100644 (file)
@@ -24,16 +24,34 @@ import android.content.Context;
  */
 class MetadataLoader {
 
-    private static final String KEY_METADATA_UPDATED = "metadata_updated";
+    private static final String KEY_METADATA_CACHED = "metadata_cached";
 
-    static void loadMetadata(final Context context, final LocalData data) {
-        PanoramaMetadataLoader.loadPanoramaMetadata(context, data.getUri(),
-                data.getMetadata());
-        RgbzMetadataLoader.loadRgbzMetadata(context, data.getUri(), data.getMetadata());
-        data.getMetadata().putBoolean(MetadataLoader.KEY_METADATA_UPDATED, true);
+    /**
+     * Adds information to the data's metadata bundle if any is available and returns
+     * true if metadata was added and false otherwise. In either case, sets
+     * a flag indicating that we've cached any available metadata and don't need to
+     * load metadata again for this particular item.
+     *
+     * @param context A context.
+     * @param data The data to update metadata for.
+     * @return true if any metadata was added to the data, false otherwise.
+     */
+    static boolean loadMetadata(final Context context, final LocalData data) {
+        boolean metadataAdded = false;
+        if (data.getLocalDataType() == LocalData.LOCAL_IMAGE) {
+            PanoramaMetadataLoader.loadPanoramaMetadata(context, data.getUri(),
+                    data.getMetadata());
+            RgbzMetadataLoader.loadRgbzMetadata(context, data.getUri(), data.getMetadata());
+            metadataAdded = true;
+        } else if (data.getLocalDataType() == LocalData.LOCAL_VIDEO) {
+            VideoRotationMetadataLoader.loadRotationMetdata(data);
+            metadataAdded = true;
+        }
+        data.getMetadata().putBoolean(MetadataLoader.KEY_METADATA_CACHED, true);
+        return metadataAdded;
     }
 
-    static boolean isMetadataLoaded(final LocalData data) {
-        return data.getMetadata().getBoolean(MetadataLoader.KEY_METADATA_UPDATED);
+    static boolean isMetadataCached(final LocalData data) {
+        return data.getMetadata().getBoolean(MetadataLoader.KEY_METADATA_CACHED);
     }
 }
diff --git a/src/com/android/camera/data/VideoRotationMetadataLoader.java b/src/com/android/camera/data/VideoRotationMetadataLoader.java
new file mode 100644 (file)
index 0000000..608ceee
--- /dev/null
@@ -0,0 +1,55 @@
+package com.android.camera.data;
+
+import android.media.MediaMetadataRetriever;
+import android.util.Log;
+
+public class VideoRotationMetadataLoader {
+    private static final String TAG = "VideoRotationMetadataLoader";
+    private static final String ROTATION_KEY = "metadata_video_rotation";
+    private static final String WIDTH_KEY = "metadata_video_width";
+    private static final String HEIGHT_KEY = "metadata_video_height";
+
+    private static final String ROTATE_90 = "90";
+    private static final String ROTATE_270 = "270";
+
+    static boolean isRotated(LocalData localData) {
+        final String rotation = localData.getMetadata().getString(ROTATION_KEY);
+        return ROTATE_90.equals(rotation) || ROTATE_270.equals(rotation);
+    }
+
+    static int getWidth(LocalData localData) {
+        return localData.getMetadata().getInt(WIDTH_KEY);
+
+    }
+
+    static int getHeight(LocalData localData) {
+        return localData.getMetadata().getInt(HEIGHT_KEY);
+    }
+
+    static void loadRotationMetdata(final LocalData data) {
+        final String path = data.getPath();
+        MediaMetadataRetriever retriever = new MediaMetadataRetriever();
+        try {
+            retriever.setDataSource(path);
+            String rotation = retriever.extractMetadata(
+                MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
+            data.getMetadata().putString(ROTATION_KEY, rotation);
+
+            String val = retriever.extractMetadata(
+                    MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
+            int width = Integer.parseInt(val);
+
+            data.getMetadata().putInt(WIDTH_KEY, width);
+
+            val = retriever.extractMetadata(
+                    MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
+            int height = Integer.parseInt(val);
+
+            data.getMetadata().putInt(HEIGHT_KEY, height);
+        } catch (RuntimeException ex) {
+            // setDataSource() can cause RuntimeException beyond
+            // IllegalArgumentException. e.g: data contain *.avi file.
+            Log.e(TAG, "MediaMetdataRetriever.setDataSource() fail", ex);
+        }
+    }
+}
index 46ba12b..e3a6484 100644 (file)
@@ -250,5 +250,15 @@ public interface FilmstripController {
          * @param newDataId The ID of the focused data of {@code -1} if none.
          */
         public void onDataFocusChanged(int prevDataId, int newDataId);
+
+        /**
+         * The callback when we scroll.
+         *
+         * @param firstVisiblePosition The position of the first rendered item (may be slightly offscreen depending on
+         *                             the orientation of the device).
+         * @param visibleItemCount The total number of rendered items.
+         * @param totalItemCount The total number of items in the filmstrip.
+         */
+        public void onScroll(int firstVisiblePosition, int visibleItemCount, int totalItemCount);
     }
 }
index c305bba..2fec90c 100644 (file)
@@ -797,6 +797,10 @@ public class FilmstripView extends ViewGroup {
         invalidate();
         if (mListener != null) {
             mListener.onDataFocusChanged(prevDataId, mViewItem[mCurrentItem].getId());
+            final int firstVisible = mViewItem[mCurrentItem].getId() - 2;
+            final int visibleItemCount = firstVisible + BUFFER_SIZE;
+            final int totalItemCount = mDataAdapter.getTotalNumber();
+            mListener.onScroll(firstVisible, visibleItemCount, totalItemCount);
         }
     }
 
diff --git a/src/com/android/camera/widget/Preloader.java b/src/com/android/camera/widget/Preloader.java
new file mode 100644 (file)
index 0000000..75b9eab
--- /dev/null
@@ -0,0 +1,174 @@
+package com.android.camera.widget;
+
+import android.util.Log;
+import android.widget.AbsListView;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.LinkedBlockingQueue;
+
+/**
+ * Responsible for controlling preloading logic. Intended usage is for ListViews that
+ * benefit from initiating a load before the row appear on screen.
+ * @param <T> The type of items this class preload.
+ * @param <Y> The type of load tokens that can be used to cancel loads for the items this class
+ *           preloads.
+ */
+public class Preloader<T, Y> implements AbsListView.OnScrollListener {
+    private static final String TAG = "Preloader";
+
+    /**
+     * Implemented by the source for items that should be preloaded.
+     */
+    public interface ItemSource<T> {
+        /**
+         * Returns the objects in the range [startPosition; endPosition).
+         */
+        public List<T> getItemsInRange(int startPosition, int endPosition);
+
+        /**
+         * Returns the total number of items in the source.
+         */
+        public int getCount();
+    }
+
+    /**
+     * Responsible for the loading of items.
+     */
+    public interface ItemLoader<T, Y> {
+        /**
+         * Initiates a load for the specified items and returns a list of 0 or more load tokens that
+         * can be used to cancel the loads for the given items. Should preload the items in the list
+         * order,preloading the 0th item in the list fist.
+         */
+        public List<Y> preloadItems(List<T> items);
+
+        /**
+         * Cancels all of the loads represented by the given load tokens.
+         */
+        public void cancelItems(List<Y> loadTokens);
+    }
+
+    private final int mMaxConcurrentPreloads;
+
+    /**
+     * Keep track of the largest/smallest item we requested (depending on scroll direction) so
+     *  we don't preload the same items repeatedly. Without this var, scrolling down we preload
+     *  0-5, then 1-6 etc. Using this we instead preload 0-5, then 5-6, 6-7 etc.
+     */
+    private int mLastEnd = -1;
+    private int mLastStart;
+
+    private final int mLoadAheadItems;
+    private ItemSource<T> mItemSource;
+    private ItemLoader<T, Y> mItemLoader;
+    private Queue<List<Y>> mItemLoadTokens = new LinkedBlockingQueue<List<Y>>();
+
+    private int mLastVisibleItem;
+    private boolean mScrollingDown = false;
+
+    public Preloader(int loadAheadItems, ItemSource<T> itemSource, ItemLoader<T, Y> itemLoader) {
+        mItemSource = itemSource;
+        mItemLoader = itemLoader;
+        mLoadAheadItems = loadAheadItems;
+        // Add an additional item so we don't cancel a preload before we start a real load.
+        mMaxConcurrentPreloads = loadAheadItems + 1;
+    }
+
+    /**
+     * Initiates a pre load.
+     *
+     * @param first The source position to load from
+     * @param increasing The direction we're going in (increasing -> source positions are
+     *                   increasing -> we're scrolling down the list)
+     */
+    private void preload(int first, boolean increasing) {
+        final int start;
+        final int end;
+        if (increasing) {
+            start = Math.max(first, mLastEnd);
+            end = Math.min(first + mLoadAheadItems, mItemSource.getCount());
+        } else {
+            start = Math.max(0, first - mLoadAheadItems);
+            end = Math.min(first, mLastStart);
+        }
+
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, "preload first=" + first + " increasing=" + increasing + " start=" + start
+                    + " end=" + end);
+        }
+
+        mLastEnd = end;
+        mLastStart = start;
+
+        if (start == 0 && end == 0) {
+            return;
+        }
+
+        final List<T> items = mItemSource.getItemsInRange(start, end);
+        if (!increasing) {
+            Collections.reverse(items);
+        }
+        registerLoadTokens(mItemLoader.preloadItems(items));
+    }
+
+    private void registerLoadTokens(List<Y> loadTokens) {
+        mItemLoadTokens.offer(loadTokens);
+        // We pretend that one batch of load tokens corresponds to one item in the list. This isn't
+        // strictly true because we may batch preload multiple items at once when we first start
+        // scrolling in the list or change the direction we're scrolling in. In those cases, we will
+        // have a single large batch of load tokens for multiple items, and then go back to getting
+        // one batch per item as we continue to scroll. This means we may not cancel as many
+        // preloads as we expect when we change direction, but we can at least be sure we won't
+        // cancel preloads for items we still care about. We can't be more precise here because
+        // there is no guarantee that there is a one to one relationship between load tokens
+        // and list items.
+        if (mItemLoadTokens.size() > mMaxConcurrentPreloads) {
+            final List<Y> loadTokensToCancel = mItemLoadTokens.poll();
+            mItemLoader.cancelItems(loadTokensToCancel);
+        }
+    }
+
+    public void cancelAllLoads() {
+        for (List<Y> loadTokens : mItemLoadTokens) {
+            mItemLoader.cancelItems(loadTokens);
+        }
+        mItemLoadTokens.clear();
+    }
+
+    @Override
+    public void onScrollStateChanged(AbsListView absListView, int i) {
+        // Do nothing.
+    }
+
+    @Override
+    public void onScroll(AbsListView absListView, int firstVisible, int visibleItemCount,
+            int totalItemCount) {
+        boolean wasScrollingDown = mScrollingDown;
+        int preloadStart = -1;
+        if (firstVisible > mLastVisibleItem) {
+            // Scrolling list down
+            mScrollingDown = true;
+            preloadStart = firstVisible + visibleItemCount;
+        } else if (firstVisible < mLastVisibleItem) {
+            // Scrolling list Up
+            mScrollingDown = false;
+            preloadStart = firstVisible;
+        }
+
+        if (wasScrollingDown != mScrollingDown) {
+            // If we've changed directions, we don't care about any of our old preloads, so cancel
+            // all of them.
+            cancelAllLoads();
+        }
+
+        // onScroll can be called multiple times with the same arguments, so we only want to preload
+        // if we've actually scrolled at least an item in either direction.
+        if (preloadStart != -1) {
+            preload(preloadStart, mScrollingDown);
+        }
+
+        mLastVisibleItem = firstVisible;
+    }
+}