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;
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;
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;
private long mOnCreateTime;
private Menu mActionBarMenu;
+ private Preloader<Integer, AsyncTask> mPreloader;
@Override
public CameraAppUI getCameraAppUI() {
}
});
}
+
+ @Override
+ public void onScroll(int firstVisiblePosition, int visibleItemCount, int totalItemCount) {
+ mPreloader.onScroll(null /*absListView*/, firstVisiblePosition, visibleItemCount, totalItemCount);
+ }
};
private final LocalDataAdapter.LocalDataListener mLocalDataListener =
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);
mLocalImagesObserver.setForegroundChangeListener(null);
mLocalImagesObserver.setActivityPaused(true);
mLocalVideosObserver.setActivityPaused(true);
+ mPreloader.cancelAllLoads();
resetScreenOn();
super.onPause();
}
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));
import android.content.Context;
import android.net.Uri;
+import android.os.AsyncTask;
+
+import java.util.List;
/**
* An abstract {@link LocalDataAdapter} implementation to wrap another
}
@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();
+ }
}
@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
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);
}
}
}
+ @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;
}
}
- 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);
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);
}
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;
}
import android.content.Context;
import android.net.Uri;
+import android.os.AsyncTask;
import android.view.View;
import com.android.camera.filmstrip.DataAdapter;
}
@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();
}
import android.content.Context;
import android.net.Uri;
+import android.os.AsyncTask;
import android.view.View;
import com.android.camera.filmstrip.ImageData;
}
@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
if (dataId < mAdapter.getTotalNumber()) {
return mAdapter.isMetadataUpdated(dataId);
} else {
- return MetadataLoader.isMetadataLoaded(mLastData);
+ return MetadataLoader.isMetadataCached(mLastData);
}
}
}
import android.net.Uri;
+import android.os.AsyncTask;
import com.android.camera.filmstrip.DataAdapter;
+import com.android.camera.widget.Preloader;
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 {
/**
* {@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.
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;
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;
+ cursor.getString(dataIndex));
}
}
+
+ cursor.close();
}
return result;
}
@Override
public boolean isMetadataUpdated() {
- return MetadataLoader.isMetadataLoaded(this);
+ return MetadataLoader.isMetadataCached(this);
}
/**
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);
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);
*/
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);
}
}
--- /dev/null
+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);
+ }
+ }
+}
* @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);
}
}
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);
}
}
--- /dev/null
+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;
+ }
+}