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