1/*
2 * Copyright 2018 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package androidx.paging;
18
19import androidx.annotation.AnyThread;
20import androidx.annotation.NonNull;
21import androidx.annotation.Nullable;
22import androidx.annotation.WorkerThread;
23import androidx.arch.core.util.Function;
24
25import java.util.ArrayList;
26import java.util.List;
27import java.util.concurrent.CopyOnWriteArrayList;
28import java.util.concurrent.Executor;
29import java.util.concurrent.atomic.AtomicBoolean;
30
31/**
32 * Base class for loading pages of snapshot data into a {@link PagedList}.
33 * <p>
34 * DataSource is queried to load pages of content into a {@link PagedList}. A PagedList can grow as
35 * it loads more data, but the data loaded cannot be updated. If the underlying data set is
36 * modified, a new PagedList / DataSource pair must be created to represent the new data.
37 * <h4>Loading Pages</h4>
38 * PagedList queries data from its DataSource in response to loading hints. {@link PagedListAdapter}
39 * calls {@link PagedList#loadAround(int)} to load content as the user scrolls in a RecyclerView.
40 * <p>
41 * To control how and when a PagedList queries data from its DataSource, see
42 * {@link PagedList.Config}. The Config object defines things like load sizes and prefetch distance.
43 * <h4>Updating Paged Data</h4>
44 * A PagedList / DataSource pair are a snapshot of the data set. A new pair of
45 * PagedList / DataSource must be created if an update occurs, such as a reorder, insert, delete, or
46 * content update occurs. A DataSource must detect that it cannot continue loading its
47 * snapshot (for instance, when Database query notices a table being invalidated), and call
48 * {@link #invalidate()}. Then a new PagedList / DataSource pair would be created to load data from
49 * the new state of the Database query.
50 * <p>
51 * To page in data that doesn't update, you can create a single DataSource, and pass it to a single
52 * PagedList. For example, loading from network when the network's paging API doesn't provide
53 * updates.
54 * <p>
55 * To page in data from a source that does provide updates, you can create a
56 * {@link DataSource.Factory}, where each DataSource created is invalidated when an update to the
57 * data set occurs that makes the current snapshot invalid. For example, when paging a query from
58 * the Database, and the table being queried inserts or removes items. You can also use a
59 * DataSource.Factory to provide multiple versions of network-paged lists. If reloading all content
60 * (e.g. in response to an action like swipe-to-refresh) is required to get a new version of data,
61 * you can connect an explicit refresh signal to call {@link #invalidate()} on the current
62 * DataSource.
63 * <p>
64 * If you have more granular update signals, such as a network API signaling an update to a single
65 * item in the list, it's recommended to load data from network into memory. Then present that
66 * data to the PagedList via a DataSource that wraps an in-memory snapshot. Each time the in-memory
67 * copy changes, invalidate the previous DataSource, and a new one wrapping the new state of the
68 * snapshot can be created.
69 * <h4>Implementing a DataSource</h4>
70 * To implement, extend one of the subclasses: {@link PageKeyedDataSource},
71 * {@link ItemKeyedDataSource}, or {@link PositionalDataSource}.
72 * <p>
73 * Use {@link PageKeyedDataSource} if pages you load embed keys for loading adjacent pages. For
74 * example a network response that returns some items, and a next/previous page links.
75 * <p>
76 * Use {@link ItemKeyedDataSource} if you need to use data from item {@code N-1} to load item
77 * {@code N}. For example, if requesting the backend for the next comments in the list
78 * requires the ID or timestamp of the most recent loaded comment, or if querying the next users
79 * from a name-sorted database query requires the name and unique ID of the previous.
80 * <p>
81 * Use {@link PositionalDataSource} if you can load pages of a requested size at arbitrary
82 * positions, and provide a fixed item count. PositionalDataSource supports querying pages at
83 * arbitrary positions, so can provide data to PagedLists in arbitrary order. Note that
84 * PositionalDataSource is required to respect page size for efficient tiling. If you want to
85 * override page size (e.g. when network page size constraints are only known at runtime), use one
86 * of the other DataSource classes.
87 * <p>
88 * Because a {@code null} item indicates a placeholder in {@link PagedList}, DataSource may not
89 * return {@code null} items in lists that it loads. This is so that users of the PagedList
90 * can differentiate unloaded placeholder items from content that has been paged in.
91 *
92 * @param  Input used to trigger initial load from the DataSource. Often an Integer position.
93 * @param  Value type loaded by the DataSource.
94 */
95@SuppressWarnings("unused") // suppress warning to remove Key/Value, needed for subclass type safety
96public abstract class DataSource<Key, Value> {
97    /**
98     * Factory for DataSources.
99     * <p>
100     * Data-loading systems of an application or library can implement this interface to allow
101     * {@code LiveData<PagedList>}s to be created. For example, Room can provide a
102     * DataSource.Factory for a given SQL query:
103     *
104     * <pre>
105     * {@literal @}Dao
106     * interface UserDao {
107     *    {@literal @}Query("SELECT * FROM user ORDER BY lastName ASC")
108     *    public abstract DataSource.Factory&lt;Integer, User> usersByLastName();
109     * }
110     * </pre>
111     * In the above sample, {@code Integer} is used because it is the {@code Key} type of
112     * PositionalDataSource. Currently, Room uses the {@code LIMIT}/{@code OFFSET} SQL keywords to
113     * page a large query with a PositionalDataSource.
114     *
115     * @param  Key identifying items in DataSource.
116     * @param  Type of items in the list loaded by the DataSources.
117     */
118    public abstract static class Factory<Key, Value> {
119        /**
120         * Create a DataSource.
121         * <p>
122         * The DataSource should invalidate itself if the snapshot is no longer valid. If a
123         * DataSource becomes invalid, the only way to query more data is to create a new DataSource
124         * from the Factory.
125         * <p>
126         * {@link LivePagedListBuilder} for example will construct a new PagedList and DataSource
127         * when the current DataSource is invalidated, and pass the new PagedList through the
128         * {@code LiveData<PagedList>} to observers.
129         *
130         * @return the new DataSource.
131         */
132        public abstract DataSource<Key, Value> create();
133
134        /**
135         * Applies the given function to each value emitted by DataSources produced by this Factory.
136         * <p>
137         * Same as {@link #mapByPage(Function)}, but operates on individual items.
138         *
139         * @param function Function that runs on each loaded item, returning items of a potentially
140         *                  new type.
141         * @param  Type of items produced by the new DataSource, from the passed function.
142         *
143         * @return A new DataSource.Factory, which transforms items using the given function.
144         *
145         * @see #mapByPage(Function)
146         * @see DataSource#map(Function)
147         * @see DataSource#mapByPage(Function)
148         */
149        @NonNull
150        public <ToValue> DataSource.Factory<Key, ToValue> map(
151                @NonNull Function<Value, ToValue> function) {
152            return mapByPage(createListFunction(function));
153        }
154
155        /**
156         * Applies the given function to each value emitted by DataSources produced by this Factory.
157         * <p>
158         * Same as {@link #map(Function)}, but allows for batch conversions.
159         *
160         * @param function Function that runs on each loaded page, returning items of a potentially
161         *                  new type.
162         * @param  Type of items produced by the new DataSource, from the passed function.
163         *
164         * @return A new DataSource.Factory, which transforms items using the given function.
165         *
166         * @see #map(Function)
167         * @see DataSource#map(Function)
168         * @see DataSource#mapByPage(Function)
169         */
170        @NonNull
171        public <ToValue> DataSource.Factory<Key, ToValue> mapByPage(
172                @NonNull final Function<List<Value>, List<ToValue>> function) {
173            return new Factory<Key, ToValue>() {
174                @Override
175                public DataSource<Key, ToValue> create() {
176                    return Factory.this.create().mapByPage(function);
177                }
178            };
179        }
180    }
181
182    @NonNull
183    static <X, Y> Function<List<X>, List<Y>> createListFunction(
184            final @NonNull Function<X, Y> innerFunc) {
185        return new Function<List<X>, List<Y>>() {
186            @Override
187            public List<Y> apply(@NonNull List<X> source) {
188                List<Y> out = new ArrayList<>(source.size());
189                for (int i = 0; i < source.size(); i++) {
190                    out.add(innerFunc.apply(source.get(i)));
191                }
192                return out;
193            }
194        };
195    }
196
197    static <A, B> List<B> convert(Function<List<A>, List<B>> function, List<A> source) {
198        List<B> dest = function.apply(source);
199        if (dest.size() != source.size()) {
200            throw new IllegalStateException("Invalid Function " + function
201                    + " changed return size. This is not supported.");
202        }
203        return dest;
204    }
205
206    // Since we currently rely on implementation details of two implementations,
207    // prevent external subclassing, except through exposed subclasses
208    DataSource() {
209    }
210
211    /**
212     * Applies the given function to each value emitted by the DataSource.
213     * <p>
214     * Same as {@link #map(Function)}, but allows for batch conversions.
215     *
216     * @param function Function that runs on each loaded page, returning items of a potentially
217     *                  new type.
218     * @param  Type of items produced by the new DataSource, from the passed function.
219     *
220     * @return A new DataSource, which transforms items using the given function.
221     *
222     * @see #map(Function)
223     * @see DataSource.Factory#map(Function)
224     * @see DataSource.Factory#mapByPage(Function)
225     */
226    @NonNull
227    public abstract <ToValue> DataSource<Key, ToValue> mapByPage(
228            @NonNull Function<List<Value>, List<ToValue>> function);
229
230    /**
231     * Applies the given function to each value emitted by the DataSource.
232     * <p>
233     * Same as {@link #mapByPage(Function)}, but operates on individual items.
234     *
235     * @param function Function that runs on each loaded item, returning items of a potentially
236     *                  new type.
237     * @param  Type of items produced by the new DataSource, from the passed function.
238     *
239     * @return A new DataSource, which transforms items using the given function.
240     *
241     * @see #mapByPage(Function)
242     * @see DataSource.Factory#map(Function)
243     * @see DataSource.Factory#mapByPage(Function)
244     */
245    @NonNull
246    public abstract <ToValue> DataSource<Key, ToValue> map(
247            @NonNull Function<Value, ToValue> function);
248
249    /**
250     * Returns true if the data source guaranteed to produce a contiguous set of items,
251     * never producing gaps.
252     */
253    abstract boolean isContiguous();
254
255    static class LoadCallbackHelper<T> {
256        static void validateInitialLoadParams(@NonNull List<?> data, int position, int totalCount) {
257            if (position < 0) {
258                throw new IllegalArgumentException("Position must be non-negative");
259            }
260            if (data.size() + position > totalCount) {
261                throw new IllegalArgumentException(
262                        "List size + position too large, last item in list beyond totalCount.");
263            }
264            if (data.size() == 0 && totalCount > 0) {
265                throw new IllegalArgumentException(
266                        "Initial result cannot be empty if items are present in data set.");
267            }
268        }
269
270        @PageResult.ResultType
271        final int mResultType;
272        private final DataSource mDataSource;
273        private final PageResult.Receiver<T> mReceiver;
274
275        // mSignalLock protects mPostExecutor, and mHasSignalled
276        private final Object mSignalLock = new Object();
277        private Executor mPostExecutor = null;
278        private boolean mHasSignalled = false;
279
280        LoadCallbackHelper(@NonNull DataSource dataSource, @PageResult.ResultType int resultType,
281                @Nullable Executor mainThreadExecutor, @NonNull PageResult.Receiver<T> receiver) {
282            mDataSource = dataSource;
283            mResultType = resultType;
284            mPostExecutor = mainThreadExecutor;
285            mReceiver = receiver;
286        }
287
288        void setPostExecutor(Executor postExecutor) {
289            synchronized (mSignalLock) {
290                mPostExecutor = postExecutor;
291            }
292        }
293
294        /**
295         * Call before verifying args, or dispatching actul results
296         *
297         * @return true if DataSource was invalid, and invalid result dispatched
298         */
299        boolean dispatchInvalidResultIfInvalid() {
300            if (mDataSource.isInvalid()) {
301                dispatchResultToReceiver(PageResult.<T>getInvalidResult());
302                return true;
303            }
304            return false;
305        }
306
307        void dispatchResultToReceiver(final @NonNull PageResult<T> result) {
308            Executor executor;
309            synchronized (mSignalLock) {
310                if (mHasSignalled) {
311                    throw new IllegalStateException(
312                            "callback.onResult already called, cannot call again.");
313                }
314                mHasSignalled = true;
315                executor = mPostExecutor;
316            }
317
318            if (executor != null) {
319                executor.execute(new Runnable() {
320                    @Override
321                    public void run() {
322                        mReceiver.onPageResult(mResultType, result);
323                    }
324                });
325            } else {
326                mReceiver.onPageResult(mResultType, result);
327            }
328        }
329    }
330
331    /**
332     * Invalidation callback for DataSource.
333     * <p>
334     * Used to signal when a DataSource a data source has become invalid, and that a new data source
335     * is needed to continue loading data.
336     */
337    public interface InvalidatedCallback {
338        /**
339         * Called when the data backing the list has become invalid. This callback is typically used
340         * to signal that a new data source is needed.
341         * <p>
342         * This callback will be invoked on the thread that calls {@link #invalidate()}. It is valid
343         * for the data source to invalidate itself during its load methods, or for an outside
344         * source to invalidate it.
345         */
346        @AnyThread
347        void onInvalidated();
348    }
349
350    private AtomicBoolean mInvalid = new AtomicBoolean(false);
351
352    private CopyOnWriteArrayList<InvalidatedCallback> mOnInvalidatedCallbacks =
353            new CopyOnWriteArrayList<>();
354
355    /**
356     * Add a callback to invoke when the DataSource is first invalidated.
357     * <p>
358     * Once invalidated, a data source will not become valid again.
359     * <p>
360     * A data source will only invoke its callbacks once - the first time {@link #invalidate()}
361     * is called, on that thread.
362     *
363     * @param onInvalidatedCallback The callback, will be invoked on thread that
364     *                              {@link #invalidate()} is called on.
365     */
366    @AnyThread
367    @SuppressWarnings("WeakerAccess")
368    public void addInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
369        mOnInvalidatedCallbacks.add(onInvalidatedCallback);
370    }
371
372    /**
373     * Remove a previously added invalidate callback.
374     *
375     * @param onInvalidatedCallback The previously added callback.
376     */
377    @AnyThread
378    @SuppressWarnings("WeakerAccess")
379    public void removeInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
380        mOnInvalidatedCallbacks.remove(onInvalidatedCallback);
381    }
382
383    /**
384     * Signal the data source to stop loading, and notify its callback.
385     * <p>
386     * If invalidate has already been called, this method does nothing.
387     */
388    @AnyThread
389    public void invalidate() {
390        if (mInvalid.compareAndSet(false, true)) {
391            for (InvalidatedCallback callback : mOnInvalidatedCallbacks) {
392                callback.onInvalidated();
393            }
394        }
395    }
396
397    /**
398     * Returns true if the data source is invalid, and can no longer be queried for data.
399     *
400     * @return True if the data source is invalid, and can no longer return data.
401     */
402    @WorkerThread
403    public boolean isInvalid() {
404        return mInvalid.get();
405    }
406}
407