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