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.RestrictTo
20 import androidx.paging.PagingSource.LoadResult.Page
21 import java.util.AbstractList
22 
23 /**
24  * Class holding the pages of data backing a [PagedList], presenting sparse loaded data as a List.
25  *
26  * This class only holds data, and does not have any notion of the ideas of async loads, or
27  * prefetching.
28  */
29 internal class PagedStorage<T : Any> :
30     AbstractList<T>, LegacyPageFetcher.KeyProvider<Any>, PlaceholderPaddedList<T> {
31     private val pages = mutableListOf<Page<*, T>>()
32 
33     internal val firstLoadedItem: T
34         get() = pages.first().data.first()
35 
36     internal val lastLoadedItem: T
37         get() = pages.last().data.last()
38 
39     override var placeholdersBefore: Int = 0
40         private set
41 
42     override var placeholdersAfter: Int = 0
43         private set
44 
45     var positionOffset: Int = 0
46         private set
47 
48     private var counted = true
49 
50     /** Number of loaded items held by [pages]. */
51     override var dataCount: Int = 0
52         private set
53 
54     /** Last accessed index for loadAround in storage space */
55     private var lastLoadAroundLocalIndex: Int = 0
56     var lastLoadAroundIndex: Int
57         get() = placeholdersBefore + lastLoadAroundLocalIndex
58         set(value) {
59             lastLoadAroundLocalIndex = (value - placeholdersBefore).coerceIn(0, dataCount - 1)
60         }
61 
62     val middleOfLoadedRange: Int
63         get() = placeholdersBefore + dataCount / 2
64 
65     constructor()
66 
67     constructor(leadingNulls: Int, page: Page<*, T>, trailingNulls: Int) : this() {
68         init(leadingNulls, page, trailingNulls, 0, true)
69     }
70 
71     private constructor(other: PagedStorage<T>) {
72         pages.addAll(other.pages)
73         placeholdersBefore = other.placeholdersBefore
74         placeholdersAfter = other.placeholdersAfter
75         positionOffset = other.positionOffset
76         counted = other.counted
77         dataCount = other.dataCount
78         lastLoadAroundLocalIndex = other.lastLoadAroundLocalIndex
79     }
80 
81     fun snapshot() = PagedStorage(this)
82 
83     private fun init(
84         leadingNulls: Int,
85         page: Page<*, T>,
86         trailingNulls: Int,
87         positionOffset: Int,
88         counted: Boolean
89     ) {
90         placeholdersBefore = leadingNulls
91         pages.clear()
92         pages.add(page)
93         placeholdersAfter = trailingNulls
94 
95         this.positionOffset = positionOffset
96         dataCount = page.data.size
97         this.counted = counted
98 
99         lastLoadAroundLocalIndex = page.data.size / 2
100     }
101 
102     @RestrictTo(RestrictTo.Scope.LIBRARY)
103     fun init(
104         leadingNulls: Int,
105         page: Page<*, T>,
106         trailingNulls: Int,
107         positionOffset: Int,
108         callback: Callback,
109         counted: Boolean = true
110     ) {
111         init(leadingNulls, page, trailingNulls, positionOffset, counted)
112         callback.onInitialized(size)
113     }
114 
115     // ------------- Adjacent Provider interface ------------------
116 
117     override val prevKey: Any?
118         get() =
119             if (!counted || placeholdersBefore + positionOffset > 0) {
120                 pages.first().prevKey
121             } else {
122                 null
123             }
124 
125     override val nextKey: Any?
126         get() =
127             if (!counted || placeholdersAfter > 0) {
128                 pages.last().nextKey
129             } else {
130                 null
131             }
132 
133     /**
134      * Traverse to the page and pageInternalIndex of localIndex.
135      *
136      * Bounds check (between 0 and storageCount) must be performed before calling this function.
137      */
138     private inline fun <V> traversePages(
139         localIndex: Int,
140         crossinline onLastPage: (page: Page<*, T>, pageInternalIndex: Int) -> V
141     ): V {
142         var localPageIndex = 0
143         var pageInternalIndex: Int = localIndex
144 
145         // Since we don't know if page sizes are regular, we walk to correct page.
146         val localPageCount = pages.size
147         while (localPageIndex < localPageCount) {
148             val pageSize = pages[localPageIndex].data.size
149             if (pageSize > pageInternalIndex) {
150                 // stop, found the page
151                 break
152             }
153             pageInternalIndex -= pageSize
154             localPageIndex++
155         }
156         return onLastPage(pages[localPageIndex], pageInternalIndex)
157     }
158 
159     /** Walk through the list of pages to find the data at local index */
160     override fun getItem(index: Int): T =
161         traversePages(index) { page, pageInternalIndex -> page.data[pageInternalIndex] }
162 
163     fun getRefreshKeyInfo(@Suppress("DEPRECATION") config: PagedList.Config): PagingState<*, T>? {
164         if (pages.isEmpty()) {
165             return null
166         }
167 
168         @Suppress("UNCHECKED_CAST")
169         return PagingState(
170             pages = pages.toList() as List<Page<Any, T>>,
171             anchorPosition = lastLoadAroundIndex,
172             config =
173                 PagingConfig(
174                     config.pageSize,
175                     config.prefetchDistance,
176                     config.enablePlaceholders,
177                     config.initialLoadSizeHint,
178                     config.maxSize
179                 ),
180             leadingPlaceholderCount = placeholdersBefore
181         )
182     }
183 
184     override fun get(index: Int): T? {
185         // is it definitely outside 'pages'?
186         val localIndex = index - placeholdersBefore
187 
188         return when {
189             index < 0 || index >= size ->
190                 throw IndexOutOfBoundsException("Index: $index, Size: $size")
191             localIndex < 0 || localIndex >= dataCount -> null
192             else -> getItem(localIndex)
193         }
194     }
195 
196     @RestrictTo(RestrictTo.Scope.LIBRARY)
197     interface Callback {
198         fun onInitialized(count: Int)
199 
200         fun onPagePrepended(leadingNulls: Int, changed: Int, added: Int)
201 
202         fun onPageAppended(endPosition: Int, changed: Int, added: Int)
203 
204         fun onPagesRemoved(startOfDrops: Int, count: Int)
205 
206         fun onPagesSwappedToPlaceholder(startOfDrops: Int, count: Int)
207     }
208 
209     override val size
210         get() = placeholdersBefore + dataCount + placeholdersAfter
211 
212     // ---------------- Trimming API -------------------
213     // Trimming is always done at the beginning or end of the list, as content is loaded.
214     // In addition to trimming pages in the storage, we also support pre-trimming pages (dropping
215     // them just before they're added) to avoid dispatching an add followed immediately by a trim.
216     //
217     // Note - we avoid trimming down to a single page to reduce chances of dropping page in
218     // viewport, since we don't strictly know the viewport. If trim is aggressively set to size of a
219     // single page, trimming while the user can see a page boundary is dangerous. To be safe, we
220     // just avoid trimming in these cases entirely.
221 
222     private fun needsTrim(maxSize: Int, requiredRemaining: Int, localPageIndex: Int): Boolean {
223         val page = pages[localPageIndex]
224         return dataCount > maxSize &&
225             pages.size > 2 &&
226             dataCount - page.data.size >= requiredRemaining
227     }
228 
229     fun needsTrimFromFront(maxSize: Int, requiredRemaining: Int) =
230         needsTrim(maxSize, requiredRemaining, 0)
231 
232     fun needsTrimFromEnd(maxSize: Int, requiredRemaining: Int) =
233         needsTrim(maxSize, requiredRemaining, pages.size - 1)
234 
235     fun shouldPreTrimNewPage(maxSize: Int, requiredRemaining: Int, countToBeAdded: Int) =
236         dataCount + countToBeAdded > maxSize && pages.size > 1 && dataCount >= requiredRemaining
237 
238     internal fun trimFromFront(
239         insertNulls: Boolean,
240         maxSize: Int,
241         requiredRemaining: Int,
242         callback: Callback
243     ): Boolean {
244         var totalRemoved = 0
245         while (needsTrimFromFront(maxSize, requiredRemaining)) {
246             val page = pages.removeAt(0)
247             val removed = page.data.size
248             totalRemoved += removed
249             dataCount -= removed
250         }
251         lastLoadAroundLocalIndex = (lastLoadAroundLocalIndex - totalRemoved).coerceAtLeast(0)
252 
253         if (totalRemoved > 0) {
254             if (insertNulls) {
255                 // replace removed items with nulls
256                 val previousLeadingNulls = placeholdersBefore
257                 placeholdersBefore += totalRemoved
258                 callback.onPagesSwappedToPlaceholder(previousLeadingNulls, totalRemoved)
259             } else {
260                 // simply remove, and handle offset
261                 positionOffset += totalRemoved
262                 callback.onPagesRemoved(placeholdersBefore, totalRemoved)
263             }
264         }
265         return totalRemoved > 0
266     }
267 
268     internal fun trimFromEnd(
269         insertNulls: Boolean,
270         maxSize: Int,
271         requiredRemaining: Int,
272         callback: Callback
273     ): Boolean {
274         var totalRemoved = 0
275         while (needsTrimFromEnd(maxSize, requiredRemaining)) {
276             val page = pages.removeAt(pages.size - 1)
277             val removed = page.data.size
278             totalRemoved += removed
279             dataCount -= removed
280         }
281         lastLoadAroundLocalIndex = lastLoadAroundLocalIndex.coerceAtMost(dataCount - 1)
282 
283         if (totalRemoved > 0) {
284             val newEndPosition = placeholdersBefore + dataCount
285             if (insertNulls) {
286                 // replace removed items with nulls
287                 placeholdersAfter += totalRemoved
288                 callback.onPagesSwappedToPlaceholder(newEndPosition, totalRemoved)
289             } else {
290                 // items were just removed, signal
291                 callback.onPagesRemoved(newEndPosition, totalRemoved)
292             }
293         }
294         return totalRemoved > 0
295     }
296 
297     // ---------------- Contiguous API -------------------
298 
299     internal fun prependPage(page: Page<*, T>, callback: Callback? = null) {
300         val count = page.data.size
301         if (count == 0) {
302             // Nothing returned from source, nothing to do
303             return
304         }
305 
306         pages.add(0, page)
307         dataCount += count
308 
309         val changedCount = minOf(placeholdersBefore, count)
310         val addedCount = count - changedCount
311 
312         if (changedCount != 0) {
313             placeholdersBefore -= changedCount
314         }
315         positionOffset -= addedCount
316         callback?.onPagePrepended(placeholdersBefore, changedCount, addedCount)
317     }
318 
319     internal fun appendPage(page: Page<*, T>, callback: Callback? = null) {
320         val count = page.data.size
321         if (count == 0) {
322             // Nothing returned from source, nothing to do
323             return
324         }
325 
326         pages.add(page)
327         dataCount += count
328 
329         val changedCount = minOf(placeholdersAfter, count)
330         val addedCount = count - changedCount
331 
332         if (changedCount != 0) {
333             placeholdersAfter -= changedCount
334         }
335 
336         callback?.onPageAppended(placeholdersBefore + dataCount - count, changedCount, addedCount)
337     }
338 
339     override fun toString(): String =
340         "leading $placeholdersBefore, dataCount $dataCount, trailing $placeholdersAfter " +
341             pages.joinToString(" ")
342 }
343