Home | History | Annotate | Download | only in paging
      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 
     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 
     75     PagedStorage(int leadingNulls, List<T> page, int trailingNulls) {
     76         this();
     77         init(leadingNulls, page, trailingNulls, 0);
     78     }
     79 
     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 
     91     PagedStorage<T> snapshot() {
     92         return new PagedStorage<>(this);
     93     }
     94 
     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 
    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
    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      */
    163     boolean isTiled() {
    164         return mPageSize > 0;
    165     }
    166 
    167     int getLeadingNullCount() {
    168         return mLeadingNullCount;
    169     }
    170 
    171     int getTrailingNullCount() {
    172         return mTrailingNullCount;
    173     }
    174 
    175     int getStorageCount() {
    176         return mStorageCount;
    177     }
    178 
    179     int getNumberAppended() {
    180         return mNumberAppended;
    181     }
    182 
    183     int getNumberPrepended() {
    184         return mNumberPrepended;
    185     }
    186 
    187     int getPageCount() {
    188         return mPages.size();
    189     }
    190 
    191     interface Callback {
    192         void onInitialized(int count);
    193         void onPagePrepended(int leadingNulls, int changed, int added);
    194         void onPageAppended(int endPosition, int changed, int added);
    195         void onPagePlaceholderInserted(int pageIndex);
    196         void onPageInserted(int start, int count);
    197     }
    198 
    199     int getPositionOffset() {
    200         return mPositionOffset;
    201     }
    202 
    203     @Override
    204     public int size() {
    205         return mLeadingNullCount + mStorageCount + mTrailingNullCount;
    206     }
    207 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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
    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