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
17package androidx.paging;
18
19import androidx.annotation.NonNull;
20import androidx.annotation.Nullable;
21
22import java.util.AbstractList;
23import java.util.ArrayList;
24import java.util.List;
25
26final 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