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.CheckResult
20 import androidx.paging.LoadState.Loading
21 import androidx.paging.LoadState.NotLoading
22 import androidx.paging.LoadType.APPEND
23 import androidx.paging.LoadType.PREPEND
24 import androidx.paging.LoadType.REFRESH
25 import androidx.paging.PageEvent.Insert.Companion.Append
26 import androidx.paging.PageEvent.Insert.Companion.Prepend
27 import androidx.paging.PageEvent.Insert.Companion.Refresh
28 import androidx.paging.PagingConfig.Companion.MAX_SIZE_UNBOUNDED
29 import androidx.paging.PagingSource.LoadResult.Page
30 import androidx.paging.PagingSource.LoadResult.Page.Companion.COUNT_UNDEFINED
31 import kotlinx.coroutines.channels.Channel
32 import kotlinx.coroutines.flow.Flow
33 import kotlinx.coroutines.flow.consumeAsFlow
34 import kotlinx.coroutines.flow.onStart
35 import kotlinx.coroutines.sync.Mutex
36 import kotlinx.coroutines.sync.withLock
37 
38 /**
39  * Internal state of [PageFetcherSnapshot] whose updates can be consumed as a [Flow] of [PageEvent].
40  *
41  * Note: This class is not thread-safe and must be guarded by a lock!
42  */
43 internal class PageFetcherSnapshotState<Key : Any, Value : Any>
44 private constructor(private val config: PagingConfig) {
45     private val _pages = mutableListOf<Page<Key, Value>>()
46     internal val pages: List<Page<Key, Value>> = _pages
47     internal var initialPageIndex = 0
48         private set
49 
50     internal val storageCount
51         get() = pages.sumOf { it.data.size }
52 
53     private var _placeholdersBefore = 0
54 
55     /** Always greater than or equal to 0. */
56     internal var placeholdersBefore
57         get() =
58             when {
59                 config.enablePlaceholders -> _placeholdersBefore
60                 else -> 0
61             }
62         set(value) {
63             _placeholdersBefore =
64                 when (value) {
65                     COUNT_UNDEFINED -> 0
66                     else -> value
67                 }
68         }
69 
70     private var _placeholdersAfter = 0
71 
72     /** Always greater than or equal to 0. */
73     internal var placeholdersAfter
74         get() =
75             when {
76                 config.enablePlaceholders -> _placeholdersAfter
77                 else -> 0
78             }
79         set(value) {
80             _placeholdersAfter =
81                 when (value) {
82                     COUNT_UNDEFINED -> 0
83                     else -> value
84                 }
85         }
86 
87     // Load generation ids used to respect cancellation in cases where suspending code continues to
88     // run even after cancellation.
89     private var prependGenerationId = 0
90     private var appendGenerationId = 0
91     private val prependGenerationIdCh = Channel<Int>(Channel.CONFLATED)
92     private val appendGenerationIdCh = Channel<Int>(Channel.CONFLATED)
93 
94     internal fun generationId(loadType: LoadType): Int =
95         when (loadType) {
96             REFRESH -> throw IllegalArgumentException("Cannot get loadId for loadType: REFRESH")
97             PREPEND -> prependGenerationId
98             APPEND -> appendGenerationId
99         }
100 
101     /**
102      * Cache previous ViewportHint which triggered any failed PagingSource APPEND / PREPEND that we
103      * can later retry. This is so we always trigger loads based on hints, instead of having two
104      * different ways to trigger.
105      */
106     internal val failedHintsByLoadType = mutableMapOf<LoadType, ViewportHint>()
107 
108     // Only track the local load states, remote states are injected from PageFetcher. This class
109     // only tracks state within a single generation from source side.
110     internal var sourceLoadStates =
111         MutableLoadStateCollection().apply {
112             // Synchronously initialize REFRESH with Loading.
113             // NOTE: It is important that we do this synchronously on init, since
114             // PageFetcherSnapshot
115             // expects to send this initial state immediately. It is always correct for a new
116             // generation to immediately begin loading refresh, so rather than start with NotLoading
117             // then updating to Loading, we simply start with Loading immediately to create less
118             // churn downstream.
119             set(REFRESH, Loading)
120         }
121         private set
122 
123     fun consumePrependGenerationIdAsFlow(): Flow<Int> {
124         return prependGenerationIdCh.consumeAsFlow().onStart {
125             prependGenerationIdCh.trySend(prependGenerationId)
126         }
127     }
128 
129     fun consumeAppendGenerationIdAsFlow(): Flow<Int> {
130         return appendGenerationIdCh.consumeAsFlow().onStart {
131             appendGenerationIdCh.trySend(appendGenerationId)
132         }
133     }
134 
135     /**
136      * Convert a loaded [Page] into a [PageEvent] for [PageFetcherSnapshot.pageEventCh].
137      *
138      * Note: This method should be called after state updated by [insert]
139      *
140      * TODO: Move this into Pager, which owns pageEventCh, since this logic is sensitive to its
141      *   implementation.
142      */
143     internal fun Page<Key, Value>.toPageEvent(loadType: LoadType): PageEvent<Value> {
144         val sourcePageIndex =
145             when (loadType) {
146                 REFRESH -> 0
147                 PREPEND -> 0 - initialPageIndex
148                 APPEND -> pages.size - initialPageIndex - 1
149             }
150         val pages = listOf(TransformablePage(sourcePageIndex, data))
151         // Mediator state is always set to null here because PageFetcherSnapshot is not responsible
152         // for Mediator state. Instead, PageFetcher will inject it if there is a remote mediator.
153         return when (loadType) {
154             REFRESH ->
155                 Refresh(
156                     pages = pages,
157                     placeholdersBefore = placeholdersBefore,
158                     placeholdersAfter = placeholdersAfter,
159                     sourceLoadStates = sourceLoadStates.snapshot(),
160                     mediatorLoadStates = null,
161                 )
162             PREPEND ->
163                 Prepend(
164                     pages = pages,
165                     placeholdersBefore = placeholdersBefore,
166                     sourceLoadStates = sourceLoadStates.snapshot(),
167                     mediatorLoadStates = null,
168                 )
169             APPEND ->
170                 Append(
171                     pages = pages,
172                     placeholdersAfter = placeholdersAfter,
173                     sourceLoadStates = sourceLoadStates.snapshot(),
174                     mediatorLoadStates = null,
175                 )
176         }
177     }
178 
179     /** @return true if insert was applied, false otherwise. */
180     @CheckResult
181     fun insert(loadId: Int, loadType: LoadType, page: Page<Key, Value>): Boolean {
182         when (loadType) {
183             REFRESH -> {
184                 check(pages.isEmpty()) { "cannot receive multiple init calls" }
185                 check(loadId == 0) { "init loadId must be the initial value, 0" }
186 
187                 _pages.add(page)
188                 initialPageIndex = 0
189                 placeholdersAfter = page.itemsAfter
190                 placeholdersBefore = page.itemsBefore
191             }
192             PREPEND -> {
193                 check(pages.isNotEmpty()) { "should've received an init before prepend" }
194 
195                 // Skip this insert if it is the result of a cancelled job due to page drop
196                 if (loadId != prependGenerationId) return false
197 
198                 _pages.add(0, page)
199                 initialPageIndex++
200                 placeholdersBefore =
201                     if (page.itemsBefore == COUNT_UNDEFINED) {
202                         (placeholdersBefore - page.data.size).coerceAtLeast(0)
203                     } else {
204                         page.itemsBefore
205                     }
206 
207                 // Clear error on successful insert
208                 failedHintsByLoadType.remove(PREPEND)
209             }
210             APPEND -> {
211                 check(pages.isNotEmpty()) { "should've received an init before append" }
212 
213                 // Skip this insert if it is the result of a cancelled job due to page drop
214                 if (loadId != appendGenerationId) return false
215 
216                 _pages.add(page)
217                 placeholdersAfter =
218                     if (page.itemsAfter == COUNT_UNDEFINED) {
219                         (placeholdersAfter - page.data.size).coerceAtLeast(0)
220                     } else {
221                         page.itemsAfter
222                     }
223 
224                 // Clear error on successful insert
225                 failedHintsByLoadType.remove(APPEND)
226             }
227         }
228 
229         return true
230     }
231 
232     fun drop(event: PageEvent.Drop<Value>) {
233         check(event.pageCount <= pages.size) {
234             "invalid drop count. have ${pages.size} but wanted to drop ${event.pageCount}"
235         }
236 
237         // Reset load state to NotLoading(endOfPaginationReached = false).
238         failedHintsByLoadType.remove(event.loadType)
239         sourceLoadStates.set(event.loadType, NotLoading.Incomplete)
240 
241         when (event.loadType) {
242             PREPEND -> {
243                 repeat(event.pageCount) { _pages.removeAt(0) }
244                 initialPageIndex -= event.pageCount
245 
246                 placeholdersBefore = event.placeholdersRemaining
247 
248                 prependGenerationId++
249                 prependGenerationIdCh.trySend(prependGenerationId)
250             }
251             APPEND -> {
252                 repeat(event.pageCount) { _pages.removeAt(pages.size - 1) }
253 
254                 placeholdersAfter = event.placeholdersRemaining
255 
256                 appendGenerationId++
257                 appendGenerationIdCh.trySend(appendGenerationId)
258             }
259             else -> throw IllegalArgumentException("cannot drop ${event.loadType}")
260         }
261     }
262 
263     /**
264      * @return [PageEvent.Drop] for [loadType] that would allow this [PageFetcherSnapshotState] to
265      *   respect [PagingConfig.maxSize], `null` if no pages should be dropped for the provided
266      *   [loadType].
267      */
268     fun dropEventOrNull(loadType: LoadType, hint: ViewportHint): PageEvent.Drop<Value>? {
269         if (config.maxSize == MAX_SIZE_UNBOUNDED) return null
270         // Never drop below 2 pages as this can cause UI flickering with certain configs and it's
271         // much more important to protect against this behaviour over respecting a config where
272         // maxSize is set unusually (probably incorrectly) strict.
273         if (pages.size <= 2) return null
274 
275         if (storageCount <= config.maxSize) return null
276 
277         require(loadType != REFRESH) {
278             "Drop LoadType must be PREPEND or APPEND, but got $loadType"
279         }
280 
281         // Compute pageCount and itemsToDrop
282         var pagesToDrop = 0
283         var itemsToDrop = 0
284         while (pagesToDrop < pages.size && storageCount - itemsToDrop > config.maxSize) {
285             val pageSize =
286                 when (loadType) {
287                     PREPEND -> pages[pagesToDrop].data.size
288                     else -> pages[pages.lastIndex - pagesToDrop].data.size
289                 }
290             val itemsAfterDrop =
291                 when (loadType) {
292                     PREPEND -> hint.presentedItemsBefore - itemsToDrop - pageSize
293                     else -> hint.presentedItemsAfter - itemsToDrop - pageSize
294                 }
295             // Do not drop pages that would fulfill prefetchDistance.
296             if (itemsAfterDrop < config.prefetchDistance) break
297 
298             itemsToDrop += pageSize
299             pagesToDrop++
300         }
301 
302         return when (pagesToDrop) {
303             0 -> null
304             else ->
305                 PageEvent.Drop(
306                     loadType = loadType,
307                     minPageOffset =
308                         when (loadType) {
309                             // originalPageOffset of the first page.
310                             PREPEND -> -initialPageIndex
311                             // maxPageOffset - pagesToDrop; We subtract one from pagesToDrop, since
312                             // this
313                             // value is inclusive.
314                             else -> pages.lastIndex - initialPageIndex - (pagesToDrop - 1)
315                         },
316                     maxPageOffset =
317                         when (loadType) {
318                             // minPageOffset + pagesToDrop; We subtract on from pagesToDrop, since
319                             // this
320                             // value is inclusive.
321                             PREPEND -> (pagesToDrop - 1) - initialPageIndex
322                             // originalPageOffset of the last page.
323                             else -> pages.lastIndex - initialPageIndex
324                         },
325                     placeholdersRemaining =
326                         when {
327                             !config.enablePlaceholders -> 0
328                             loadType == PREPEND -> placeholdersBefore + itemsToDrop
329                             else -> placeholdersAfter + itemsToDrop
330                         }
331                 )
332         }
333     }
334 
335     internal fun currentPagingState(viewportHint: ViewportHint.Access?) =
336         PagingState<Key, Value>(
337             pages = pages.toList(),
338             anchorPosition =
339                 viewportHint?.let { hint ->
340                     // Translate viewportHint to anchorPosition based on fetcher state
341                     // (pre-transformation),
342                     // so start with fetcher count of placeholdersBefore.
343                     var anchorPosition = placeholdersBefore
344 
345                     // Compute fetcher state pageOffsets.
346                     val fetcherPageOffsetFirst = -initialPageIndex
347                     val fetcherPageOffsetLast = pages.lastIndex - initialPageIndex
348 
349                     // ViewportHint is based off of presenter state, which may race with fetcher
350                     // state.
351                     // Since computing anchorPosition relies on hint.indexInPage, which accounts for
352                     // placeholders in presenter state, we need iterate through pages to
353                     // incrementally
354                     // build anchorPosition and adjust the value we use for placeholdersBefore
355                     // accordingly.
356                     for (pageOffset in fetcherPageOffsetFirst until hint.pageOffset) {
357                         // Aside from incrementing anchorPosition normally using the loaded page's
358                         // size, there are 4 race-cases to consider:
359                         //   - Fetcher has extra PREPEND pages
360                         //     - Simply add the size of the loaded page to anchorPosition to sync
361                         // with
362                         //       presenter; don't need to do anything special to handle this.
363                         //   - Fetcher is missing PREPEND pages
364                         //     - Already accounted for in placeholdersBefore; so don't need to do
365                         // anything.
366                         //   - Fetcher has extra APPEND pages
367                         //     - Already accounted for in hint.indexInPage (value can be greater
368                         // than
369                         //     page size to denote placeholders access).
370                         //   - Fetcher is missing APPEND pages
371                         //     - Increment anchorPosition using config.pageSize to estimate size of
372                         // the
373                         //     missing page.
374                         anchorPosition +=
375                             when {
376                                 // Fetcher is missing APPEND pages, i.e., viewportHint points to an
377                                 // item
378                                 // after a page that was dropped. Estimate how much to increment
379                                 // anchorPosition
380                                 // by using PagingConfig.pageSize.
381                                 pageOffset > fetcherPageOffsetLast -> config.pageSize
382                                 // pageOffset refers to a loaded page; increment anchorPosition with
383                                 // data.size.
384                                 else -> pages[pageOffset + initialPageIndex].data.size
385                             }
386                     }
387 
388                     // Handle the page referenced by hint.pageOffset. Increment anchorPosition by
389                     // hint.indexInPage, which accounts for placeholders and may not be within the
390                     // bounds
391                     // of page.data.indices.
392                     anchorPosition += hint.indexInPage
393 
394                     // In the special case where viewportHint references a missing PREPEND page, we
395                     // need
396                     // to decrement anchorPosition using config.pageSize as an estimate, otherwise
397                     // we
398                     // would be double counting it since it's accounted for in both indexInPage and
399                     // placeholdersBefore.
400                     if (hint.pageOffset < fetcherPageOffsetFirst) {
401                         anchorPosition -= config.pageSize
402                     }
403 
404                     return@let anchorPosition
405                 },
406             config = config,
407             leadingPlaceholderCount = placeholdersBefore
408         )
409 
410     /**
411      * Wrapper for [PageFetcherSnapshotState], which protects access behind a [Mutex] to prevent
412      * race scenarios.
413      */
414     internal class Holder<Key : Any, Value : Any>(private val config: PagingConfig) {
415         private val lock = Mutex()
416         private val state = PageFetcherSnapshotState<Key, Value>(config)
417 
418         suspend inline fun <T> withLock(
419             block: (state: PageFetcherSnapshotState<Key, Value>) -> T
420         ): T {
421             return lock.withLock { block(state) }
422         }
423     }
424 }
425