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