1 /*
2  * 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.paging.LoadType.APPEND
20 import androidx.paging.LoadType.PREPEND
21 import androidx.paging.LoadType.REFRESH
22 import androidx.paging.PageEvent.Insert.Companion.EMPTY_REFRESH_LOCAL
23 import androidx.paging.internal.BUGANIZER_URL
24 
25 /**
26  * Presents post-transform paging data as a list, with list update notifications when PageEvents are
27  * dispatched.
28  */
29 internal class PageStore<T : Any>(
30     pages: List<TransformablePage<T>>,
31     placeholdersBefore: Int,
32     placeholdersAfter: Int,
33 ) : PlaceholderPaddedList<T> {
34     constructor(
35         insertEvent: PageEvent.Insert<T>
36     ) : this(
37         pages = insertEvent.pages,
38         placeholdersBefore = insertEvent.placeholdersBefore,
39         placeholdersAfter = insertEvent.placeholdersAfter,
40     )
41 
42     private val pages: MutableList<TransformablePage<T>> = pages.toMutableList()
43     override var dataCount: Int = pages.fullCount()
44         private set
45 
46     private val originalPageOffsetFirst: Int
47         get() = pages.first().originalPageOffsets.minOrNull()!!
48 
49     private val originalPageOffsetLast: Int
50         get() = pages.last().originalPageOffsets.maxOrNull()!!
51 
52     override var placeholdersBefore: Int = placeholdersBefore
53         private set
54 
55     override var placeholdersAfter: Int = placeholdersAfter
56         private set
57 
checkIndexnull58     private fun checkIndex(index: Int) {
59         if (index < 0 || index >= size) {
60             throw IndexOutOfBoundsException("Index: $index, Size: $size")
61         }
62     }
63 
toStringnull64     override fun toString(): String {
65         val items = List(dataCount) { getItem(it) }.joinToString()
66         return "[($placeholdersBefore placeholders), $items, ($placeholdersAfter placeholders)]"
67     }
68 
getnull69     fun get(index: Int): T? {
70         checkIndex(index)
71 
72         val localIndex = index - placeholdersBefore
73         if (localIndex < 0 || localIndex >= dataCount) {
74             return null
75         }
76         return getItem(localIndex)
77     }
78 
snapshotnull79     fun snapshot(): ItemSnapshotList<T> {
80         return ItemSnapshotList(placeholdersBefore, placeholdersAfter, pages.flatMap { it.data })
81     }
82 
getItemnull83     override fun getItem(index: Int): T {
84         var pageIndex = 0
85         var indexInPage = index
86 
87         // Since we don't know if page sizes are regular, we walk to correct page.
88         val localPageCount = pages.size
89         while (pageIndex < localPageCount) {
90             val pageSize = pages[pageIndex].data.size
91             if (pageSize > indexInPage) {
92                 // stop, found the page
93                 break
94             }
95             indexInPage -= pageSize
96             pageIndex++
97         }
98         return pages[pageIndex].data[indexInPage]
99     }
100 
101     override val size: Int
102         get() = placeholdersBefore + dataCount + placeholdersAfter
103 
<lambda>null104     private fun List<TransformablePage<T>>.fullCount() = sumOf { it.data.size }
105 
processEventnull106     fun processEvent(pageEvent: PageEvent<T>): PagingDataEvent<T> {
107         return when (pageEvent) {
108             is PageEvent.Insert -> insertPage(pageEvent)
109             is PageEvent.Drop -> dropPages(pageEvent)
110             else ->
111                 throw IllegalStateException(
112                     """Paging received an event to process StaticList or LoadStateUpdate while
113                 |processing Inserts and Drops. If you see this exception, it is most
114                 |likely a bug in the library. Please file a bug so we can fix it at:
115                 |$BUGANIZER_URL"""
116                         .trimMargin()
117                 )
118         }
119     }
120 
initializeHintnull121     fun initializeHint(): ViewportHint.Initial {
122         val presentedItems = dataCount
123         return ViewportHint.Initial(
124             presentedItemsBefore = presentedItems / 2,
125             presentedItemsAfter = presentedItems / 2,
126             originalPageOffsetFirst = originalPageOffsetFirst,
127             originalPageOffsetLast = originalPageOffsetLast
128         )
129     }
130 
accessHintForPresenterIndexnull131     fun accessHintForPresenterIndex(index: Int): ViewportHint.Access {
132         var pageIndex = 0
133         var indexInPage = index - placeholdersBefore
134         while (indexInPage >= pages[pageIndex].data.size && pageIndex < pages.lastIndex) {
135             // index doesn't appear in current page, keep looking!
136             indexInPage -= pages[pageIndex].data.size
137             pageIndex++
138         }
139 
140         return pages[pageIndex].viewportHintFor(
141             index = indexInPage,
142             presentedItemsBefore = index - placeholdersBefore,
143             presentedItemsAfter = size - index - placeholdersAfter - 1,
144             originalPageOffsetFirst = originalPageOffsetFirst,
145             originalPageOffsetLast = originalPageOffsetLast
146         )
147     }
148 
149     /**
150      * Insert the event's page to the storage and return a [PagingDataEvent] to be dispatched to
151      * presenters.
152      */
insertPagenull153     private fun insertPage(insert: PageEvent.Insert<T>): PagingDataEvent<T> {
154         val insertSize = insert.pages.fullCount()
155         return when (insert.loadType) {
156             REFRESH ->
157                 throw IllegalStateException(
158                     """Paging received a refresh event in the middle of an actively loading generation
159                 |of PagingData. If you see this exception, it is most likely a bug in the library.
160                 |Please file a bug so we can fix it at:
161                 |$BUGANIZER_URL"""
162                         .trimMargin()
163                 )
164             PREPEND -> {
165                 val oldPlaceholdersBefore = placeholdersBefore
166                 // update all states
167                 pages.addAll(0, insert.pages)
168                 dataCount += insertSize
169                 placeholdersBefore = insert.placeholdersBefore
170 
171                 PagingDataEvent.Prepend(
172                     inserted = insert.pages.flatMap { it.data },
173                     newPlaceholdersBefore = placeholdersBefore,
174                     oldPlaceholdersBefore = oldPlaceholdersBefore
175                 )
176             }
177             APPEND -> {
178                 val oldPlaceholdersAfter = placeholdersAfter
179                 val oldDataCount = dataCount
180                 // update all states
181                 pages.addAll(pages.size, insert.pages)
182                 dataCount += insertSize
183                 placeholdersAfter = insert.placeholdersAfter
184 
185                 PagingDataEvent.Append(
186                     startIndex = placeholdersBefore + oldDataCount,
187                     inserted = insert.pages.flatMap { it.data },
188                     newPlaceholdersAfter = placeholdersAfter,
189                     oldPlaceholdersAfter = oldPlaceholdersAfter
190                 )
191             }
192         }
193     }
194 
195     /**
196      * @param pageOffsetsToDrop originalPageOffset of pages that were dropped
197      * @return The number of items dropped
198      */
dropPagesWithOffsetsnull199     private fun dropPagesWithOffsets(pageOffsetsToDrop: IntRange): Int {
200         var removeCount = 0
201         val pageIterator = pages.iterator()
202         while (pageIterator.hasNext()) {
203             val page = pageIterator.next()
204             if (page.originalPageOffsets.any { pageOffsetsToDrop.contains(it) }) {
205                 removeCount += page.data.size
206                 pageIterator.remove()
207             }
208         }
209 
210         return removeCount
211     }
212 
213     /**
214      * Helper which converts a [PageEvent.Drop] to a [PagingDataEvent] by dropping all pages that
215      * depend on the n-lowest or n-highest originalPageOffsets.
216      */
dropPagesnull217     private fun dropPages(drop: PageEvent.Drop<T>): PagingDataEvent<T> {
218         // update states
219         val itemDropCount = dropPagesWithOffsets(drop.minPageOffset..drop.maxPageOffset)
220         dataCount -= itemDropCount
221 
222         return if (drop.loadType == PREPEND) {
223             val oldPlaceholdersBefore = placeholdersBefore
224             placeholdersBefore = drop.placeholdersRemaining
225 
226             PagingDataEvent.DropPrepend(
227                 dropCount = itemDropCount,
228                 newPlaceholdersBefore = placeholdersBefore,
229                 oldPlaceholdersBefore = oldPlaceholdersBefore,
230             )
231         } else {
232             val oldPlaceholdersAfter = placeholdersAfter
233             placeholdersAfter = drop.placeholdersRemaining
234 
235             PagingDataEvent.DropAppend(
236                 startIndex = placeholdersBefore + dataCount,
237                 dropCount = itemDropCount,
238                 newPlaceholdersAfter = drop.placeholdersRemaining,
239                 oldPlaceholdersAfter = oldPlaceholdersAfter,
240             )
241         }
242     }
243 
244     internal companion object {
245         // TODO(b/205350267): Replace this with a static list that does not emit CombinedLoadStates.
246         private val INITIAL = PageStore(EMPTY_REFRESH_LOCAL)
247 
248         @Suppress("UNCHECKED_CAST", "SyntheticAccessor")
initialnull249         internal fun <T : Any> initial(event: PageEvent.Insert<T>?): PageStore<T> =
250             if (event != null) {
251                 PageStore(event)
252             } else {
253                 INITIAL as PageStore<T>
254             }
255     }
256 }
257