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