1 /* 2 * Copyright 2018 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.NonNull; 20 import androidx.annotation.Nullable; 21 22 import java.util.AbstractList; 23 import java.util.ArrayList; 24 import java.util.List; 25 26 final class PagedStorage<T> extends AbstractList<T> { 27 /** 28 * Lists instances are compared (with instance equality) to PLACEHOLDER_LIST to check if an item 29 * in that position is already loading. We use a singleton placeholder list that is distinct 30 * from Collections.EMPTY_LIST for safety. 31 */ 32 @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") 33 private static final List PLACEHOLDER_LIST = new ArrayList(); 34 35 // Always set 36 private int mLeadingNullCount; 37 /** 38 * List of pages in storage. 39 * 40 * Two storage modes: 41 * 42 * Contiguous - all content in mPages is valid and loaded, but may return false from isTiled(). 43 * Safe to access any item in any page. 44 * 45 * Non-contiguous - mPages may have nulls or a placeholder page, isTiled() always returns true. 46 * mPages may have nulls, or placeholder (empty) pages while content is loading. 47 */ 48 private final ArrayList<List<T>> mPages; 49 private int mTrailingNullCount; 50 51 private int mPositionOffset; 52 /** 53 * Number of items represented by {@link #mPages}. If tiling is enabled, unloaded items in 54 * {@link #mPages} may be null, but this value still counts them. 55 */ 56 private int mStorageCount; 57 58 // If mPageSize > 0, tiling is enabled, 'mPages' may have gaps, and leadingPages is set 59 private int mPageSize; 60 61 private int mNumberPrepended; 62 private int mNumberAppended; 63 PagedStorage()64 PagedStorage() { 65 mLeadingNullCount = 0; 66 mPages = new ArrayList<>(); 67 mTrailingNullCount = 0; 68 mPositionOffset = 0; 69 mStorageCount = 0; 70 mPageSize = 1; 71 mNumberPrepended = 0; 72 mNumberAppended = 0; 73 } 74 PagedStorage(int leadingNulls, List<T> page, int trailingNulls)75 PagedStorage(int leadingNulls, List<T> page, int trailingNulls) { 76 this(); 77 init(leadingNulls, page, trailingNulls, 0); 78 } 79 PagedStorage(PagedStorage<T> other)80 private PagedStorage(PagedStorage<T> other) { 81 mLeadingNullCount = other.mLeadingNullCount; 82 mPages = new ArrayList<>(other.mPages); 83 mTrailingNullCount = other.mTrailingNullCount; 84 mPositionOffset = other.mPositionOffset; 85 mStorageCount = other.mStorageCount; 86 mPageSize = other.mPageSize; 87 mNumberPrepended = other.mNumberPrepended; 88 mNumberAppended = other.mNumberAppended; 89 } 90 snapshot()91 PagedStorage<T> snapshot() { 92 return new PagedStorage<>(this); 93 } 94 init(int leadingNulls, List<T> page, int trailingNulls, int positionOffset)95 private void init(int leadingNulls, List<T> page, int trailingNulls, int positionOffset) { 96 mLeadingNullCount = leadingNulls; 97 mPages.clear(); 98 mPages.add(page); 99 mTrailingNullCount = trailingNulls; 100 101 mPositionOffset = positionOffset; 102 mStorageCount = page.size(); 103 104 // initialized as tiled. There may be 3 nulls, 2 items, but we still call this tiled 105 // even if it will break if nulls convert. 106 mPageSize = page.size(); 107 108 mNumberPrepended = 0; 109 mNumberAppended = 0; 110 } 111 init(int leadingNulls, @NonNull List<T> page, int trailingNulls, int positionOffset, @NonNull Callback callback)112 void init(int leadingNulls, @NonNull List<T> page, int trailingNulls, int positionOffset, 113 @NonNull Callback callback) { 114 init(leadingNulls, page, trailingNulls, positionOffset); 115 callback.onInitialized(size()); 116 } 117 118 @Override get(int i)119 public T get(int i) { 120 if (i < 0 || i >= size()) { 121 throw new IndexOutOfBoundsException("Index: " + i + ", Size: " + size()); 122 } 123 124 // is it definitely outside 'mPages'? 125 int localIndex = i - mLeadingNullCount; 126 if (localIndex < 0 || localIndex >= mStorageCount) { 127 return null; 128 } 129 130 int localPageIndex; 131 int pageInternalIndex; 132 133 if (isTiled()) { 134 // it's inside mPages, and we're tiled. Jump to correct tile. 135 localPageIndex = localIndex / mPageSize; 136 pageInternalIndex = localIndex % mPageSize; 137 } else { 138 // it's inside mPages, but page sizes aren't regular. Walk to correct tile. 139 // Pages can only be null while tiled, so accessing page count is safe. 140 pageInternalIndex = localIndex; 141 final int localPageCount = mPages.size(); 142 for (localPageIndex = 0; localPageIndex < localPageCount; localPageIndex++) { 143 int pageSize = mPages.get(localPageIndex).size(); 144 if (pageSize > pageInternalIndex) { 145 // stop, found the page 146 break; 147 } 148 pageInternalIndex -= pageSize; 149 } 150 } 151 152 List<T> page = mPages.get(localPageIndex); 153 if (page == null || page.size() == 0) { 154 // can only occur in tiled case, with untouched inner/placeholder pages 155 return null; 156 } 157 return page.get(pageInternalIndex); 158 } 159 160 /** 161 * Returns true if all pages are the same size, except for the last, which may be smaller 162 */ isTiled()163 boolean isTiled() { 164 return mPageSize > 0; 165 } 166 getLeadingNullCount()167 int getLeadingNullCount() { 168 return mLeadingNullCount; 169 } 170 getTrailingNullCount()171 int getTrailingNullCount() { 172 return mTrailingNullCount; 173 } 174 getStorageCount()175 int getStorageCount() { 176 return mStorageCount; 177 } 178 getNumberAppended()179 int getNumberAppended() { 180 return mNumberAppended; 181 } 182 getNumberPrepended()183 int getNumberPrepended() { 184 return mNumberPrepended; 185 } 186 getPageCount()187 int getPageCount() { 188 return mPages.size(); 189 } 190 191 interface Callback { onInitialized(int count)192 void onInitialized(int count); onPagePrepended(int leadingNulls, int changed, int added)193 void onPagePrepended(int leadingNulls, int changed, int added); onPageAppended(int endPosition, int changed, int added)194 void onPageAppended(int endPosition, int changed, int added); onPagePlaceholderInserted(int pageIndex)195 void onPagePlaceholderInserted(int pageIndex); onPageInserted(int start, int count)196 void onPageInserted(int start, int count); 197 } 198 getPositionOffset()199 int getPositionOffset() { 200 return mPositionOffset; 201 } 202 203 @Override size()204 public int size() { 205 return mLeadingNullCount + mStorageCount + mTrailingNullCount; 206 } 207 computeLeadingNulls()208 int computeLeadingNulls() { 209 int total = mLeadingNullCount; 210 final int pageCount = mPages.size(); 211 for (int i = 0; i < pageCount; i++) { 212 List page = mPages.get(i); 213 if (page != null && page != PLACEHOLDER_LIST) { 214 break; 215 } 216 total += mPageSize; 217 } 218 return total; 219 } 220 computeTrailingNulls()221 int computeTrailingNulls() { 222 int total = mTrailingNullCount; 223 for (int i = mPages.size() - 1; i >= 0; i--) { 224 List page = mPages.get(i); 225 if (page != null && page != PLACEHOLDER_LIST) { 226 break; 227 } 228 total += mPageSize; 229 } 230 return total; 231 } 232 233 // ---------------- Contiguous API ------------------- 234 getFirstLoadedItem()235 T getFirstLoadedItem() { 236 // safe to access first page's first item here: 237 // If contiguous, mPages can't be empty, can't hold null Pages, and items can't be empty 238 return mPages.get(0).get(0); 239 } 240 getLastLoadedItem()241 T getLastLoadedItem() { 242 // safe to access last page's last item here: 243 // If contiguous, mPages can't be empty, can't hold null Pages, and items can't be empty 244 List<T> page = mPages.get(mPages.size() - 1); 245 return page.get(page.size() - 1); 246 } 247 prependPage(@onNull List<T> page, @NonNull Callback callback)248 void prependPage(@NonNull List<T> page, @NonNull Callback callback) { 249 final int count = page.size(); 250 if (count == 0) { 251 // Nothing returned from source, stop loading in this direction 252 return; 253 } 254 if (mPageSize > 0 && count != mPageSize) { 255 if (mPages.size() == 1 && count > mPageSize) { 256 // prepending to a single item - update current page size to that of 'inner' page 257 mPageSize = count; 258 } else { 259 // no longer tiled 260 mPageSize = -1; 261 } 262 } 263 264 mPages.add(0, page); 265 mStorageCount += count; 266 267 final int changedCount = Math.min(mLeadingNullCount, count); 268 final int addedCount = count - changedCount; 269 270 if (changedCount != 0) { 271 mLeadingNullCount -= changedCount; 272 } 273 mPositionOffset -= addedCount; 274 mNumberPrepended += count; 275 276 callback.onPagePrepended(mLeadingNullCount, changedCount, addedCount); 277 } 278 appendPage(@onNull List<T> page, @NonNull Callback callback)279 void appendPage(@NonNull List<T> page, @NonNull Callback callback) { 280 final int count = page.size(); 281 if (count == 0) { 282 // Nothing returned from source, stop loading in this direction 283 return; 284 } 285 286 if (mPageSize > 0) { 287 // if the previous page was smaller than mPageSize, 288 // or if this page is larger than the previous, disable tiling 289 if (mPages.get(mPages.size() - 1).size() != mPageSize 290 || count > mPageSize) { 291 mPageSize = -1; 292 } 293 } 294 295 mPages.add(page); 296 mStorageCount += count; 297 298 final int changedCount = Math.min(mTrailingNullCount, count); 299 final int addedCount = count - changedCount; 300 301 if (changedCount != 0) { 302 mTrailingNullCount -= changedCount; 303 } 304 mNumberAppended += count; 305 callback.onPageAppended(mLeadingNullCount + mStorageCount - count, 306 changedCount, addedCount); 307 } 308 309 // ------------------ Non-Contiguous API (tiling required) ---------------------- 310 initAndSplit(int leadingNulls, @NonNull List<T> multiPageList, int trailingNulls, int positionOffset, int pageSize, @NonNull Callback callback)311 void initAndSplit(int leadingNulls, @NonNull List<T> multiPageList, 312 int trailingNulls, int positionOffset, int pageSize, @NonNull Callback callback) { 313 314 int pageCount = (multiPageList.size() + (pageSize - 1)) / pageSize; 315 for (int i = 0; i < pageCount; i++) { 316 int beginInclusive = i * pageSize; 317 int endExclusive = Math.min(multiPageList.size(), (i + 1) * pageSize); 318 319 List<T> sublist = multiPageList.subList(beginInclusive, endExclusive); 320 321 if (i == 0) { 322 // Trailing nulls for first page includes other pages in multiPageList 323 int initialTrailingNulls = trailingNulls + multiPageList.size() - sublist.size(); 324 init(leadingNulls, sublist, initialTrailingNulls, positionOffset); 325 } else { 326 int insertPosition = leadingNulls + beginInclusive; 327 insertPage(insertPosition, sublist, null); 328 } 329 } 330 callback.onInitialized(size()); 331 } 332 insertPage(int position, @NonNull List<T> page, @Nullable Callback callback)333 public void insertPage(int position, @NonNull List<T> page, @Nullable Callback callback) { 334 final int newPageSize = page.size(); 335 if (newPageSize != mPageSize) { 336 // differing page size is OK in 2 cases, when the page is being added: 337 // 1) to the end (in which case, ignore new smaller size) 338 // 2) only the last page has been added so far (in which case, adopt new bigger size) 339 340 int size = size(); 341 boolean addingLastPage = position == (size - size % mPageSize) 342 && newPageSize < mPageSize; 343 boolean onlyEndPagePresent = mTrailingNullCount == 0 && mPages.size() == 1 344 && newPageSize > mPageSize; 345 346 // OK only if existing single page, and it's the last one 347 if (!onlyEndPagePresent && !addingLastPage) { 348 throw new IllegalArgumentException("page introduces incorrect tiling"); 349 } 350 if (onlyEndPagePresent) { 351 mPageSize = newPageSize; 352 } 353 } 354 355 int pageIndex = position / mPageSize; 356 357 allocatePageRange(pageIndex, pageIndex); 358 359 int localPageIndex = pageIndex - mLeadingNullCount / mPageSize; 360 361 List<T> oldPage = mPages.get(localPageIndex); 362 if (oldPage != null && oldPage != PLACEHOLDER_LIST) { 363 throw new IllegalArgumentException( 364 "Invalid position " + position + ": data already loaded"); 365 } 366 mPages.set(localPageIndex, page); 367 if (callback != null) { 368 callback.onPageInserted(position, page.size()); 369 } 370 } 371 allocatePageRange(final int minimumPage, final int maximumPage)372 private void allocatePageRange(final int minimumPage, final int maximumPage) { 373 int leadingNullPages = mLeadingNullCount / mPageSize; 374 375 if (minimumPage < leadingNullPages) { 376 for (int i = 0; i < leadingNullPages - minimumPage; i++) { 377 mPages.add(0, null); 378 } 379 int newStorageAllocated = (leadingNullPages - minimumPage) * mPageSize; 380 mStorageCount += newStorageAllocated; 381 mLeadingNullCount -= newStorageAllocated; 382 383 leadingNullPages = minimumPage; 384 } 385 if (maximumPage >= leadingNullPages + mPages.size()) { 386 int newStorageAllocated = Math.min(mTrailingNullCount, 387 (maximumPage + 1 - (leadingNullPages + mPages.size())) * mPageSize); 388 for (int i = mPages.size(); i <= maximumPage - leadingNullPages; i++) { 389 mPages.add(mPages.size(), null); 390 } 391 mStorageCount += newStorageAllocated; 392 mTrailingNullCount -= newStorageAllocated; 393 } 394 } 395 allocatePlaceholders(int index, int prefetchDistance, int pageSize, Callback callback)396 public void allocatePlaceholders(int index, int prefetchDistance, 397 int pageSize, Callback callback) { 398 if (pageSize != mPageSize) { 399 if (pageSize < mPageSize) { 400 throw new IllegalArgumentException("Page size cannot be reduced"); 401 } 402 if (mPages.size() != 1 || mTrailingNullCount != 0) { 403 // not in single, last page allocated case - can't change page size 404 throw new IllegalArgumentException( 405 "Page size can change only if last page is only one present"); 406 } 407 mPageSize = pageSize; 408 } 409 410 final int maxPageCount = (size() + mPageSize - 1) / mPageSize; 411 int minimumPage = Math.max((index - prefetchDistance) / mPageSize, 0); 412 int maximumPage = Math.min((index + prefetchDistance) / mPageSize, maxPageCount - 1); 413 414 allocatePageRange(minimumPage, maximumPage); 415 int leadingNullPages = mLeadingNullCount / mPageSize; 416 for (int pageIndex = minimumPage; pageIndex <= maximumPage; pageIndex++) { 417 int localPageIndex = pageIndex - leadingNullPages; 418 if (mPages.get(localPageIndex) == null) { 419 //noinspection unchecked 420 mPages.set(localPageIndex, PLACEHOLDER_LIST); 421 callback.onPagePlaceholderInserted(pageIndex); 422 } 423 } 424 } 425 hasPage(int pageSize, int index)426 public boolean hasPage(int pageSize, int index) { 427 // NOTE: we pass pageSize here to avoid in case mPageSize 428 // not fully initialized (when last page only one loaded) 429 int leadingNullPages = mLeadingNullCount / pageSize; 430 431 if (index < leadingNullPages || index >= leadingNullPages + mPages.size()) { 432 return false; 433 } 434 435 List<T> page = mPages.get(index - leadingNullPages); 436 437 return page != null && page != PLACEHOLDER_LIST; 438 } 439 440 @Override toString()441 public String toString() { 442 StringBuilder ret = new StringBuilder("leading " + mLeadingNullCount 443 + ", storage " + mStorageCount 444 + ", trailing " + getTrailingNullCount()); 445 446 for (int i = 0; i < mPages.size(); i++) { 447 ret.append(" ").append(mPages.get(i)); 448 } 449 return ret.toString(); 450 } 451 } 452