1 /*
<lambda>null2  * Copyright 2019 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 
17 package androidx.paging
18 
19 import androidx.annotation.VisibleForTesting
20 import androidx.annotation.WorkerThread
21 import androidx.arch.core.util.Function
22 import androidx.paging.DataSource.KeyType.POSITIONAL
23 import androidx.paging.PagingSource.LoadResult.Page.Companion.COUNT_UNDEFINED
24 import kotlin.coroutines.resume
25 import kotlinx.coroutines.suspendCancellableCoroutine
26 
27 /**
28  * Position-based data loader for a fixed-size, countable data set, supporting fixed-size loads at
29  * arbitrary page positions.
30  *
31  * Extend PositionalDataSource if you can load pages of a requested size at arbitrary positions, and
32  * provide a fixed item count. If your data source can't support loading arbitrary requested page
33  * sizes (e.g. when network page size constraints are only known at runtime), either use
34  * [PageKeyedDataSource] or [ItemKeyedDataSource], or pass the initial result with the two parameter
35  * [LoadInitialCallback.onResult].
36  *
37  * Room can generate a Factory of PositionalDataSources for you:
38  * ```
39  * @Dao
40  * interface UserDao {
41  *     @Query("SELECT * FROM user ORDER BY age DESC")
42  *     public abstract DataSource.Factory<Integer, User> loadUsersByAgeDesc();
43  * }
44  * ```
45  *
46  * @param T Type of items being loaded by the [PositionalDataSource].
47  */
48 @Deprecated(
49     message = "PositionalDataSource is deprecated and has been replaced by PagingSource",
50     replaceWith = ReplaceWith("PagingSource<Int, T>", "androidx.paging.PagingSource")
51 )
52 public abstract class PositionalDataSource<T : Any> : DataSource<Int, T>(POSITIONAL) {
53 
54     /** Holder object for inputs to [loadInitial]. */
55     public open class LoadInitialParams(
56         /**
57          * Initial load position requested.
58          *
59          * Note that this may not be within the bounds of your data set, it may need to be adjusted
60          * before you execute your load.
61          */
62         @JvmField public val requestedStartPosition: Int,
63         /**
64          * Requested number of items to load.
65          *
66          * Note that this may be larger than available data.
67          */
68         @JvmField public val requestedLoadSize: Int,
69         /**
70          * Defines page size acceptable for return values.
71          *
72          * List of items passed to the callback must be an integer multiple of page size.
73          */
74         @JvmField public val pageSize: Int,
75         /**
76          * Defines whether placeholders are enabled, and whether the loaded total count will be
77          * ignored.
78          */
79         @JvmField public val placeholdersEnabled: Boolean
80     ) {
81         init {
82             check(requestedStartPosition >= 0) { "invalid start position: $requestedStartPosition" }
83             check(requestedLoadSize >= 0) { "invalid load size: $requestedLoadSize" }
84             check(pageSize >= 0) { "invalid page size: $pageSize" }
85         }
86     }
87 
88     /** Holder object for inputs to [loadRange]. */
89     public open class LoadRangeParams(
90         /**
91          * START position of data to load.
92          *
93          * Returned data must start at this position.
94          */
95         @JvmField public val startPosition: Int,
96         /**
97          * Number of items to load.
98          *
99          * Returned data must be of this size, unless at end of the list.
100          */
101         @JvmField public val loadSize: Int
102     )
103 
104     /**
105      * Callback for [loadInitial] to return data, position, and count.
106      *
107      * A callback should be called only once, and may throw if called again.
108      *
109      * It is always valid for a DataSource loading method that takes a callback to stash the
110      * callback and call it later. This enables DataSources to be fully asynchronous, and to handle
111      * temporary, recoverable error states (such as a network error that can be retried).
112      *
113      * @param T Type of items being loaded.
114      */
115     public abstract class LoadInitialCallback<T> {
116         /**
117          * Called to pass initial load state from a DataSource.
118          *
119          * Call this method from [loadInitial] function to return data, and inform how many
120          * placeholders should be shown before and after. If counting is cheap compute (for example,
121          * if a network load returns the information regardless), it's recommended to pass the total
122          * size to the totalCount parameter. If placeholders are not requested (when
123          * [LoadInitialParams.placeholdersEnabled] is false), you can instead call [onResult].
124          *
125          * @param data List of items loaded from the [DataSource]. If this is empty, the
126          *   [DataSource] is treated as empty, and no further loads will occur.
127          * @param position Position of the item at the front of the list. If there are N items
128          *   before the items in data that can be loaded from this DataSource, pass N.
129          * @param totalCount Total number of items that may be returned from this DataSource.
130          *   Includes the number in the initial [data] parameter as well as any items that can be
131          *   loaded in front or behind of [data].
132          */
133         public abstract fun onResult(data: List<T>, position: Int, totalCount: Int)
134 
135         /**
136          * Called to pass initial load state from a DataSource without total count, when
137          * placeholders aren't requested.
138          *
139          * **Note:** This method can only be called when placeholders are disabled (i.e.,
140          * [LoadInitialParams.placeholdersEnabled] is `false`).
141          *
142          * Call this method from [loadInitial] function to return data, if position is known but
143          * total size is not. If placeholders are requested, call the three parameter variant:
144          * [onResult].
145          *
146          * @param data List of items loaded from the [DataSource]. If this is empty, the
147          *   [DataSource] is treated as empty, and no further loads will occur.
148          * @param position Position of the item at the front of the list. If there are N items
149          *   before the items in data that can be provided by this [DataSource], pass N.
150          */
151         public abstract fun onResult(data: List<T>, position: Int)
152     }
153 
154     /**
155      * Callback for PositionalDataSource [loadRange] to return data.
156      *
157      * A callback should be called only once, and may throw if called again.
158      *
159      * It is always valid for a [DataSource] loading method that takes a callback to stash the
160      * callback and call it later. This enables DataSources to be fully asynchronous, and to handle
161      * temporary, recoverable error states (such as a network error that can be retried).
162      *
163      * @param T Type of items being loaded.
164      */
165     public abstract class LoadRangeCallback<T> {
166         /**
167          * Called to pass loaded data from [loadRange].
168          *
169          * @param data List of items loaded from the [DataSource]. Must be same size as requested,
170          *   unless at end of list.
171          */
172         public abstract fun onResult(data: List<T>)
173     }
174 
175     public companion object {
176         /**
177          * Helper for computing an initial position in [loadInitial] when total data set size can be
178          * computed ahead of loading.
179          *
180          * The value computed by this function will do bounds checking, page alignment, and
181          * positioning based on initial load size requested.
182          *
183          * Example usage in a [PositionalDataSource] subclass:
184          * ```
185          * class ItemDataSource extends PositionalDataSource<Item> {
186          *     private int computeCount() {
187          *         // actual count code here
188          *     }
189          *
190          *     private List<Item> loadRangeInternal(int startPosition, int loadCount) {
191          *         // actual load code here
192          *     }
193          *
194          *     @Override
195          *     public void loadInitial(@NonNull LoadInitialParams params,
196          *         @NonNull LoadInitialCallback<Item> callback) {
197          *         int totalCount = computeCount();
198          *         int position = computeInitialLoadPosition(params, totalCount);
199          *         int loadSize = computeInitialLoadSize(params, position, totalCount);
200          *         callback.onResult(loadRangeInternal(position, loadSize), position, totalCount);
201          *     }
202          *
203          *     @Override
204          *     public void loadRange(@NonNull LoadRangeParams params,
205          *         @NonNull LoadRangeCallback<Item> callback) {
206          *         callback.onResult(loadRangeInternal(params.startPosition, params.loadSize));
207          *     }
208          * }
209          * ```
210          *
211          * @param params Params passed to [loadInitial], including page size, and requested start /
212          *   loadSize.
213          * @param totalCount Total size of the data set.
214          * @return Position to start loading at.
215          * @see [computeInitialLoadSize]
216          */
217         @JvmStatic
218         public fun computeInitialLoadPosition(params: LoadInitialParams, totalCount: Int): Int {
219             val position = params.requestedStartPosition
220             val initialLoadSize = params.requestedLoadSize
221             val pageSize = params.pageSize
222 
223             var pageStart = position / pageSize * pageSize
224 
225             // maximum start pos is that which will encompass end of list
226             val maximumLoadPage =
227                 (totalCount - initialLoadSize + pageSize - 1) / pageSize * pageSize
228             pageStart = minOf(maximumLoadPage, pageStart)
229 
230             // minimum start position is 0
231             pageStart = maxOf(0, pageStart)
232 
233             return pageStart
234         }
235 
236         /**
237          * Helper for computing an initial load size in [loadInitial] when total data set size can
238          * be computed ahead of loading.
239          *
240          * This function takes the requested load size, and bounds checks it against the value
241          * returned by [computeInitialLoadPosition].
242          *
243          * Example usage in a [PositionalDataSource] subclass:
244          * ```
245          * class ItemDataSource extends PositionalDataSource<Item> {
246          *     private int computeCount() {
247          *         // actual count code here
248          *     }
249          *
250          *     private List<Item> loadRangeInternal(int startPosition, int loadCount) {
251          *         // actual load code here
252          *     }
253          *
254          *     @Override
255          *     public void loadInitial(@NonNull LoadInitialParams params,
256          *         @NonNull LoadInitialCallback<Item> callback) {
257          *         int totalCount = computeCount();
258          *         int position = computeInitialLoadPosition(params, totalCount);
259          *         int loadSize = computeInitialLoadSize(params, position, totalCount);
260          *         callback.onResult(loadRangeInternal(position, loadSize), position, totalCount);
261          *     }
262          *
263          *     @Override
264          *     public void loadRange(@NonNull LoadRangeParams params,
265          *         @NonNull LoadRangeCallback<Item> callback) {
266          *         callback.onResult(loadRangeInternal(params.startPosition, params.loadSize));
267          *     }
268          * }
269          * ```
270          *
271          * @param params Params passed to [loadInitial], including page size, and requested start /
272          *   loadSize.
273          * @param initialLoadPosition Value returned by [computeInitialLoadPosition]
274          * @param totalCount Total size of the data set.
275          * @return Number of items to load.
276          * @see [computeInitialLoadPosition]
277          */
278         @JvmStatic
279         public fun computeInitialLoadSize(
280             params: LoadInitialParams,
281             initialLoadPosition: Int,
282             totalCount: Int
283         ): Int = minOf(totalCount - initialLoadPosition, params.requestedLoadSize)
284     }
285 
286     final override suspend fun load(params: Params<Int>): BaseResult<T> {
287         if (params.type == LoadType.REFRESH) {
288             var initialPosition = 0
289             var initialLoadSize = params.initialLoadSize
290             if (params.key != null) {
291                 initialPosition = params.key
292 
293                 if (params.placeholdersEnabled) {
294                     // snap load size to page multiple (minimum two)
295                     initialLoadSize = maxOf(initialLoadSize / params.pageSize, 2) * params.pageSize
296 
297                     // move start so the load is centered around the key, not starting at it
298                     val idealStart = initialPosition - initialLoadSize / 2
299                     initialPosition = maxOf(0, idealStart / params.pageSize * params.pageSize)
300                 } else {
301                     // not tiled, so don't try to snap or force multiple of a page size
302                     initialPosition = maxOf(0, initialPosition - initialLoadSize / 2)
303                 }
304             }
305             val initParams =
306                 LoadInitialParams(
307                     initialPosition,
308                     initialLoadSize,
309                     params.pageSize,
310                     params.placeholdersEnabled
311                 )
312             return loadInitial(initParams)
313         } else {
314             var startIndex = params.key!!
315             var loadSize = params.pageSize
316             if (params.type == LoadType.PREPEND) {
317                 // clamp load size to positive indices only, and shift start index by load size
318                 loadSize = minOf(loadSize, startIndex)
319                 startIndex -= loadSize
320             }
321             return loadRange(LoadRangeParams(startIndex, loadSize))
322         }
323     }
324 
325     /**
326      * Load initial list data.
327      *
328      * This method is called to load the initial page(s) from the DataSource.
329      *
330      * LoadResult list must be a multiple of pageSize to enable efficient tiling.
331      */
332     @VisibleForTesting
333     internal suspend fun loadInitial(params: LoadInitialParams) =
334         suspendCancellableCoroutine<BaseResult<T>> { cont ->
335             loadInitial(
336                 params,
337                 object : LoadInitialCallback<T>() {
338                     override fun onResult(data: List<T>, position: Int, totalCount: Int) {
339                         if (isInvalid) {
340                             // NOTE: this isInvalid check works around
341                             // https://issuetracker.google.com/issues/124511903
342                             cont.resume(BaseResult.empty())
343                         } else {
344                             val nextKey = position + data.size
345                             resume(
346                                 params,
347                                 BaseResult(
348                                     data = data,
349                                     // skip passing prevKey if nothing else to load
350                                     prevKey = if (position == 0) null else position,
351                                     // skip passing nextKey if nothing else to load
352                                     nextKey = if (nextKey == totalCount) null else nextKey,
353                                     itemsBefore = position,
354                                     itemsAfter = totalCount - data.size - position
355                                 )
356                             )
357                         }
358                     }
359 
360                     override fun onResult(data: List<T>, position: Int) {
361                         if (isInvalid) {
362                             // NOTE: this isInvalid check works around
363                             // https://issuetracker.google.com/issues/124511903
364                             cont.resume(BaseResult.empty())
365                         } else {
366                             resume(
367                                 params,
368                                 BaseResult(
369                                     data = data,
370                                     // skip passing prevKey if nothing else to load
371                                     prevKey = if (position == 0) null else position,
372                                     // can't do same for nextKey, since we don't know if load is
373                                     // terminal
374                                     nextKey = position + data.size,
375                                     itemsBefore = position,
376                                     itemsAfter = COUNT_UNDEFINED
377                                 )
378                             )
379                         }
380                     }
381 
382                     private fun resume(params: LoadInitialParams, result: BaseResult<T>) {
383                         if (params.placeholdersEnabled) {
384                             result.validateForInitialTiling(params.pageSize)
385                         }
386                         cont.resume(result)
387                     }
388                 }
389             )
390         }
391 
392     /**
393      * Called to load a range of data from the DataSource.
394      *
395      * This method is called to load additional pages from the DataSource after the
396      * [ItemKeyedDataSource.LoadInitialCallback] passed to dispatchLoadInitial has initialized a
397      * [PagedList].
398      *
399      * Unlike [ItemKeyedDataSource.loadInitial], this method must return the number of items
400      * requested, at the position requested.
401      */
402     private suspend fun loadRange(params: LoadRangeParams) =
403         suspendCancellableCoroutine<BaseResult<T>> { cont ->
404             loadRange(
405                 params,
406                 object : LoadRangeCallback<T>() {
407                     override fun onResult(data: List<T>) {
408                         // skip passing prevKey if nothing else to load. We only do this for prepend
409                         // direction, since 0 as first index is well defined, but max index may not
410                         // be
411                         val prevKey = if (params.startPosition == 0) null else params.startPosition
412                         when {
413                             isInvalid -> cont.resume(BaseResult.empty())
414                             else ->
415                                 cont.resume(
416                                     BaseResult(
417                                         data = data,
418                                         prevKey = prevKey,
419                                         nextKey = params.startPosition + data.size
420                                     )
421                                 )
422                         }
423                     }
424                 }
425             )
426         }
427 
428     /**
429      * Load initial list data.
430      *
431      * This method is called to load the initial page(s) from the [DataSource].
432      *
433      * LoadResult list must be a multiple of pageSize to enable efficient tiling.
434      *
435      * @param params Parameters for initial load, including requested start position, load size, and
436      *   page size.
437      * @param callback Callback that receives initial load data, including position and total data
438      *   set size.
439      */
440     @WorkerThread
441     public abstract fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<T>)
442 
443     /**
444      * Called to load a range of data from the DataSource.
445      *
446      * This method is called to load additional pages from the DataSource after the
447      * [LoadInitialCallback] passed to dispatchLoadInitial has initialized a [PagedList].
448      *
449      * Unlike [loadInitial], this method must return the number of items requested, at the position
450      * requested.
451      *
452      * @param params Parameters for load, including start position and load size.
453      * @param callback Callback that receives loaded data.
454      */
455     @WorkerThread
456     public abstract fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<T>)
457 
458     @Suppress("RedundantVisibilityModifier") // Metalava doesn't inherit visibility properly.
459     internal override val isContiguous = false
460 
461     @Suppress("RedundantVisibilityModifier") // Metalava doesn't inherit visibility properly.
462     internal final override fun getKeyInternal(item: T): Int =
463         throw IllegalStateException("Cannot get key by item in positionalDataSource")
464 
465     @Suppress("DEPRECATION")
466     final override fun <V : Any> mapByPage(
467         function: Function<List<T>, List<V>>
468     ): PositionalDataSource<V> = WrapperPositionalDataSource(this, function)
469 
470     @Suppress("DEPRECATION")
471     final override fun <V : Any> mapByPage(
472         function: (List<T>) -> List<V>
473     ): PositionalDataSource<V> = mapByPage(Function { function(it) })
474 
475     @Suppress("DEPRECATION")
476     final override fun <V : Any> map(function: Function<T, V>): PositionalDataSource<V> =
477         mapByPage(Function { list -> list.map { function.apply(it) } })
478 
479     @Suppress("DEPRECATION")
480     final override fun <V : Any> map(function: (T) -> V): PositionalDataSource<V> =
481         mapByPage(Function { list -> list.map(function) })
482 }
483