1package com.android.camera.widget;
2
3import android.widget.AbsListView;
4
5import com.android.camera.debug.Log;
6
7import java.util.Collections;
8import java.util.List;
9import java.util.Queue;
10import java.util.concurrent.LinkedBlockingQueue;
11
12/**
13 * Responsible for controlling preloading logic. Intended usage is for ListViews that
14 * benefit from initiating a load before the row appear on screen.
15 * @param <T> The type of items this class preload.
16 * @param <Y> The type of load tokens that can be used to cancel loads for the items this class
17 *           preloads.
18 */
19public class Preloader<T, Y> implements AbsListView.OnScrollListener {
20    private static final Log.Tag TAG = new Log.Tag("Preloader");
21
22    /**
23     * Implemented by the source for items that should be preloaded.
24     */
25    public interface ItemSource<T> {
26        /**
27         * Returns the objects in the range [startPosition; endPosition).
28         */
29        public List<T> getItemsInRange(int startPosition, int endPosition);
30
31        /**
32         * Returns the total number of items in the source.
33         */
34        public int getCount();
35    }
36
37    /**
38     * Responsible for the loading of items.
39     */
40    public interface ItemLoader<T, Y> {
41        /**
42         * Initiates a load for the specified items and returns a list of 0 or more load tokens that
43         * can be used to cancel the loads for the given items. Should preload the items in the list
44         * order,preloading the 0th item in the list fist.
45         */
46        public List<Y> preloadItems(List<T> items);
47
48        /**
49         * Cancels all of the loads represented by the given load tokens.
50         */
51        public void cancelItems(List<Y> loadTokens);
52    }
53
54    private final int mMaxConcurrentPreloads;
55
56    /**
57     * Keep track of the largest/smallest item we requested (depending on scroll direction) so
58     *  we don't preload the same items repeatedly. Without this var, scrolling down we preload
59     *  0-5, then 1-6 etc. Using this we instead preload 0-5, then 5-6, 6-7 etc.
60     */
61    private int mLastEnd = -1;
62    private int mLastStart;
63
64    private final int mLoadAheadItems;
65    private ItemSource<T> mItemSource;
66    private ItemLoader<T, Y> mItemLoader;
67    private Queue<List<Y>> mItemLoadTokens = new LinkedBlockingQueue<List<Y>>();
68
69    private int mLastVisibleItem;
70    private boolean mScrollingDown = false;
71
72    public Preloader(int loadAheadItems, ItemSource<T> itemSource, ItemLoader<T, Y> itemLoader) {
73        mItemSource = itemSource;
74        mItemLoader = itemLoader;
75        mLoadAheadItems = loadAheadItems;
76        // Add an additional item so we don't cancel a preload before we start a real load.
77        mMaxConcurrentPreloads = loadAheadItems + 1;
78    }
79
80    /**
81     * Initiates a pre load.
82     *
83     * @param first The source position to load from
84     * @param increasing The direction we're going in (increasing -> source positions are
85     *                   increasing -> we're scrolling down the list)
86     */
87    private void preload(int first, boolean increasing) {
88        final int start;
89        final int end;
90        if (increasing) {
91            start = Math.max(first, mLastEnd);
92            end = Math.min(first + mLoadAheadItems, mItemSource.getCount());
93        } else {
94            start = Math.max(0, first - mLoadAheadItems);
95            end = Math.min(first, mLastStart);
96        }
97
98        Log.v(TAG, "preload first=" + first + " increasing=" + increasing + " start=" + start +
99                " end=" + end);
100
101        mLastEnd = end;
102        mLastStart = start;
103
104        if (start == 0 && end == 0) {
105            return;
106        }
107
108        final List<T> items = mItemSource.getItemsInRange(start, end);
109        if (!increasing) {
110            Collections.reverse(items);
111        }
112        registerLoadTokens(mItemLoader.preloadItems(items));
113    }
114
115    private void registerLoadTokens(List<Y> loadTokens) {
116        mItemLoadTokens.offer(loadTokens);
117        // We pretend that one batch of load tokens corresponds to one item in the list. This isn't
118        // strictly true because we may batch preload multiple items at once when we first start
119        // scrolling in the list or change the direction we're scrolling in. In those cases, we will
120        // have a single large batch of load tokens for multiple items, and then go back to getting
121        // one batch per item as we continue to scroll. This means we may not cancel as many
122        // preloads as we expect when we change direction, but we can at least be sure we won't
123        // cancel preloads for items we still care about. We can't be more precise here because
124        // there is no guarantee that there is a one to one relationship between load tokens
125        // and list items.
126        if (mItemLoadTokens.size() > mMaxConcurrentPreloads) {
127            final List<Y> loadTokensToCancel = mItemLoadTokens.poll();
128            mItemLoader.cancelItems(loadTokensToCancel);
129        }
130    }
131
132    public void cancelAllLoads() {
133        for (List<Y> loadTokens : mItemLoadTokens) {
134            mItemLoader.cancelItems(loadTokens);
135        }
136        mItemLoadTokens.clear();
137    }
138
139    @Override
140    public void onScrollStateChanged(AbsListView absListView, int i) {
141        // Do nothing.
142    }
143
144    @Override
145    public void onScroll(AbsListView absListView, int firstVisible, int visibleItemCount,
146            int totalItemCount) {
147        boolean wasScrollingDown = mScrollingDown;
148        int preloadStart = -1;
149        if (firstVisible > mLastVisibleItem) {
150            // Scrolling list down
151            mScrollingDown = true;
152            preloadStart = firstVisible + visibleItemCount;
153        } else if (firstVisible < mLastVisibleItem) {
154            // Scrolling list Up
155            mScrollingDown = false;
156            preloadStart = firstVisible;
157        }
158
159        if (wasScrollingDown != mScrollingDown) {
160            // If we've changed directions, we don't care about any of our old preloads, so cancel
161            // all of them.
162            cancelAllLoads();
163        }
164
165        // onScroll can be called multiple times with the same arguments, so we only want to preload
166        // if we've actually scrolled at least an item in either direction.
167        if (preloadStart != -1) {
168            preload(preloadStart, mScrollingDown);
169        }
170
171        mLastVisibleItem = firstVisible;
172    }
173}
174