1 /*
<lambda>null2 * Copyright 2021 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 @file:RestrictTo(RestrictTo.Scope.LIBRARY)
18
19 package androidx.paging
20
21 import androidx.annotation.RestrictTo
22 import androidx.paging.LoadType.APPEND
23 import androidx.paging.LoadType.PREPEND
24 import androidx.paging.internal.ReentrantLock
25 import androidx.paging.internal.withLock
26 import kotlinx.coroutines.channels.BufferOverflow
27 import kotlinx.coroutines.flow.Flow
28 import kotlinx.coroutines.flow.MutableSharedFlow
29
30 /**
31 * Helper class to handle UI hints. It processes incoming hints and keeps a min/max (prepend/append)
32 * values and provides them as a flow to [PageFetcherSnapshot].
33 */
34 internal class HintHandler {
35 private val state = State()
36
37 /**
38 * Latest call to [processHint]. Note that this value might be ignored wrt prepend and append
39 * hints if it is not expanding the range.
40 */
41 val lastAccessHint: ViewportHint.Access?
42 get() = state.lastAccessHint
43
44 /** Returns a flow of hints for the given [loadType]. */
45 fun hintFor(loadType: LoadType): Flow<ViewportHint> =
46 when (loadType) {
47 PREPEND -> state.prependFlow
48 APPEND -> state.appendFlow
49 else -> throw IllegalArgumentException("invalid load type for hints")
50 }
51
52 /**
53 * Resets the hint for the given [loadType]. Note that this won't update [lastAccessHint] or the
54 * other load type.
55 */
56 fun forceSetHint(loadType: LoadType, viewportHint: ViewportHint) {
57 require(loadType == PREPEND || loadType == APPEND) {
58 "invalid load type for reset: $loadType"
59 }
60 state.modify(accessHint = null) { prependHint, appendHint ->
61 if (loadType == PREPEND) {
62 prependHint.value = viewportHint
63 } else {
64 appendHint.value = viewportHint
65 }
66 }
67 }
68
69 /** Processes the hint coming from UI. */
70 fun processHint(viewportHint: ViewportHint) {
71 state.modify(viewportHint as? ViewportHint.Access) { prependHint, appendHint ->
72 if (
73 viewportHint.shouldPrioritizeOver(previous = prependHint.value, loadType = PREPEND)
74 ) {
75 prependHint.value = viewportHint
76 }
77 if (viewportHint.shouldPrioritizeOver(previous = appendHint.value, loadType = APPEND)) {
78 appendHint.value = viewportHint
79 }
80 }
81 }
82
83 private inner class State {
84 private val prepend = HintFlow()
85 private val append = HintFlow()
86 var lastAccessHint: ViewportHint.Access? = null
87 private set
88
89 val prependFlow
90 get() = prepend.flow
91
92 val appendFlow
93 get() = append.flow
94
95 private val lock = ReentrantLock()
96
97 /** Modifies the state inside a lock where it gets access to the mutable values. */
98 fun modify(
99 accessHint: ViewportHint.Access?,
100 block: (prepend: HintFlow, append: HintFlow) -> Unit
101 ) {
102 lock.withLock {
103 if (accessHint != null) {
104 lastAccessHint = accessHint
105 }
106 block(prepend, append)
107 }
108 }
109 }
110
111 /**
112 * Like a StateFlow that holds the value but does not do de-duping. Note that, this class is not
113 * thread safe.
114 */
115 private inner class HintFlow {
116 var value: ViewportHint? = null
117 set(value) {
118 field = value
119 if (value != null) {
120 _flow.tryEmit(value)
121 }
122 }
123
124 private val _flow =
125 MutableSharedFlow<ViewportHint>(
126 replay = 1,
127 onBufferOverflow = BufferOverflow.DROP_OLDEST
128 )
129 val flow: Flow<ViewportHint>
130 get() = _flow
131 }
132 }
133
shouldPrioritizeOvernull134 internal fun ViewportHint.shouldPrioritizeOver(
135 previous: ViewportHint?,
136 loadType: LoadType
137 ): Boolean {
138 return when {
139 previous == null -> true
140 // Prioritize Access hints over Initialize hints
141 previous is ViewportHint.Initial && this is ViewportHint.Access -> true
142 this is ViewportHint.Initial && previous is ViewportHint.Access -> false
143 // Prioritize hints from most recent presenter state
144 // not that this it not a gt/lt check because we would like to prioritize any
145 // change in available pages, not necessarily more or less as drops can have an impact.
146 this.originalPageOffsetFirst != previous.originalPageOffsetFirst -> true
147 this.originalPageOffsetLast != previous.originalPageOffsetLast -> true
148 // Prioritize hints that would load the most items
149 previous.presentedItemsBeyondAnchor(loadType) <= presentedItemsBeyondAnchor(loadType) ->
150 false
151 else -> true
152 }
153 }
154