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