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.AnyThread;
20import androidx.annotation.MainThread;
21import androidx.annotation.NonNull;
22import androidx.annotation.Nullable;
23
24import java.util.List;
25import java.util.concurrent.Executor;
26
27class ContiguousPagedList<K, V> extends PagedList<V> implements PagedStorage.Callback {
28    private final ContiguousDataSource<K, V> mDataSource;
29    private boolean mPrependWorkerRunning = false;
30    private boolean mAppendWorkerRunning = false;
31
32    private int mPrependItemsRequested = 0;
33    private int mAppendItemsRequested = 0;
34
35    private PageResult.Receiver<V> mReceiver = new PageResult.Receiver<V>() {
36        // Creation thread for initial synchronous load, otherwise main thread
37        // Safe to access main thread only state - no other thread has reference during construction
38        @AnyThread
39        @Override
40        public void onPageResult(@PageResult.ResultType int resultType,
41                @NonNull PageResult<V> pageResult) {
42            if (pageResult.isInvalid()) {
43                detach();
44                return;
45            }
46
47            if (isDetached()) {
48                // No op, have detached
49                return;
50            }
51
52            List<V> page = pageResult.page;
53            if (resultType == PageResult.INIT) {
54                mStorage.init(pageResult.leadingNulls, page, pageResult.trailingNulls,
55                        pageResult.positionOffset, ContiguousPagedList.this);
56                if (mLastLoad == LAST_LOAD_UNSPECIFIED) {
57                    // Because the ContiguousPagedList wasn't initialized with a last load position,
58                    // initialize it to the middle of the initial load
59                    mLastLoad =
60                            pageResult.leadingNulls + pageResult.positionOffset + page.size() / 2;
61                }
62            } else if (resultType == PageResult.APPEND) {
63                mStorage.appendPage(page, ContiguousPagedList.this);
64            } else if (resultType == PageResult.PREPEND) {
65                mStorage.prependPage(page, ContiguousPagedList.this);
66            } else {
67                throw new IllegalArgumentException("unexpected resultType " + resultType);
68            }
69
70
71            if (mBoundaryCallback != null) {
72                boolean deferEmpty = mStorage.size() == 0;
73                boolean deferBegin = !deferEmpty
74                        && resultType == PageResult.PREPEND
75                        && pageResult.page.size() == 0;
76                boolean deferEnd = !deferEmpty
77                        && resultType == PageResult.APPEND
78                        && pageResult.page.size() == 0;
79                deferBoundaryCallbacks(deferEmpty, deferBegin, deferEnd);
80            }
81        }
82    };
83
84    static final int LAST_LOAD_UNSPECIFIED = -1;
85
86    ContiguousPagedList(
87            @NonNull ContiguousDataSource<K, V> dataSource,
88            @NonNull Executor mainThreadExecutor,
89            @NonNull Executor backgroundThreadExecutor,
90            @Nullable BoundaryCallback<V> boundaryCallback,
91            @NonNull Config config,
92            final @Nullable K key,
93            int lastLoad) {
94        super(new PagedStorage<V>(), mainThreadExecutor, backgroundThreadExecutor,
95                boundaryCallback, config);
96        mDataSource = dataSource;
97        mLastLoad = lastLoad;
98
99        if (mDataSource.isInvalid()) {
100            detach();
101        } else {
102            mDataSource.dispatchLoadInitial(key,
103                    mConfig.initialLoadSizeHint,
104                    mConfig.pageSize,
105                    mConfig.enablePlaceholders,
106                    mMainThreadExecutor,
107                    mReceiver);
108        }
109    }
110
111    @MainThread
112    @Override
113    void dispatchUpdatesSinceSnapshot(
114            @NonNull PagedList<V> pagedListSnapshot, @NonNull Callback callback) {
115        final PagedStorage<V> snapshot = pagedListSnapshot.mStorage;
116
117        final int newlyAppended = mStorage.getNumberAppended() - snapshot.getNumberAppended();
118        final int newlyPrepended = mStorage.getNumberPrepended() - snapshot.getNumberPrepended();
119
120        final int previousTrailing = snapshot.getTrailingNullCount();
121        final int previousLeading = snapshot.getLeadingNullCount();
122
123        // Validate that the snapshot looks like a previous version of this list - if it's not,
124        // we can't be sure we'll dispatch callbacks safely
125        if (snapshot.isEmpty()
126                || newlyAppended < 0
127                || newlyPrepended < 0
128                || mStorage.getTrailingNullCount() != Math.max(previousTrailing - newlyAppended, 0)
129                || mStorage.getLeadingNullCount() != Math.max(previousLeading - newlyPrepended, 0)
130                || (mStorage.getStorageCount()
131                        != snapshot.getStorageCount() + newlyAppended + newlyPrepended)) {
132            throw new IllegalArgumentException("Invalid snapshot provided - doesn't appear"
133                    + " to be a snapshot of this PagedList");
134        }
135
136        if (newlyAppended != 0) {
137            final int changedCount = Math.min(previousTrailing, newlyAppended);
138            final int addedCount = newlyAppended - changedCount;
139
140            final int endPosition = snapshot.getLeadingNullCount() + snapshot.getStorageCount();
141            if (changedCount != 0) {
142                callback.onChanged(endPosition, changedCount);
143            }
144            if (addedCount != 0) {
145                callback.onInserted(endPosition + changedCount, addedCount);
146            }
147        }
148        if (newlyPrepended != 0) {
149            final int changedCount = Math.min(previousLeading, newlyPrepended);
150            final int addedCount = newlyPrepended - changedCount;
151
152            if (changedCount != 0) {
153                callback.onChanged(previousLeading, changedCount);
154            }
155            if (addedCount != 0) {
156                callback.onInserted(0, addedCount);
157            }
158        }
159    }
160
161    @MainThread
162    @Override
163    protected void loadAroundInternal(int index) {
164        int prependItems = mConfig.prefetchDistance - (index - mStorage.getLeadingNullCount());
165        int appendItems = index + mConfig.prefetchDistance
166                - (mStorage.getLeadingNullCount() + mStorage.getStorageCount());
167
168        mPrependItemsRequested = Math.max(prependItems, mPrependItemsRequested);
169        if (mPrependItemsRequested > 0) {
170            schedulePrepend();
171        }
172
173        mAppendItemsRequested = Math.max(appendItems, mAppendItemsRequested);
174        if (mAppendItemsRequested > 0) {
175            scheduleAppend();
176        }
177    }
178
179    @MainThread
180    private void schedulePrepend() {
181        if (mPrependWorkerRunning) {
182            return;
183        }
184        mPrependWorkerRunning = true;
185
186        final int position = mStorage.getLeadingNullCount() + mStorage.getPositionOffset();
187
188        // safe to access first item here - mStorage can't be empty if we're prepending
189        final V item = mStorage.getFirstLoadedItem();
190        mBackgroundThreadExecutor.execute(new Runnable() {
191            @Override
192            public void run() {
193                if (isDetached()) {
194                    return;
195                }
196                if (mDataSource.isInvalid()) {
197                    detach();
198                } else {
199                    mDataSource.dispatchLoadBefore(position, item, mConfig.pageSize,
200                            mMainThreadExecutor, mReceiver);
201                }
202
203            }
204        });
205    }
206
207    @MainThread
208    private void scheduleAppend() {
209        if (mAppendWorkerRunning) {
210            return;
211        }
212        mAppendWorkerRunning = true;
213
214        final int position = mStorage.getLeadingNullCount()
215                + mStorage.getStorageCount() - 1 + mStorage.getPositionOffset();
216
217        // safe to access first item here - mStorage can't be empty if we're appending
218        final V item = mStorage.getLastLoadedItem();
219        mBackgroundThreadExecutor.execute(new Runnable() {
220            @Override
221            public void run() {
222                if (isDetached()) {
223                    return;
224                }
225                if (mDataSource.isInvalid()) {
226                    detach();
227                } else {
228                    mDataSource.dispatchLoadAfter(position, item, mConfig.pageSize,
229                            mMainThreadExecutor, mReceiver);
230                }
231            }
232        });
233    }
234
235    @Override
236    boolean isContiguous() {
237        return true;
238    }
239
240    @NonNull
241    @Override
242    public DataSource<?, V> getDataSource() {
243        return mDataSource;
244    }
245
246    @Nullable
247    @Override
248    public Object getLastKey() {
249        return mDataSource.getKey(mLastLoad, mLastItem);
250    }
251
252    @MainThread
253    @Override
254    public void onInitialized(int count) {
255        notifyInserted(0, count);
256    }
257
258    @MainThread
259    @Override
260    public void onPagePrepended(int leadingNulls, int changedCount, int addedCount) {
261        // consider whether to post more work, now that a page is fully prepended
262        mPrependItemsRequested = mPrependItemsRequested - changedCount - addedCount;
263        mPrependWorkerRunning = false;
264        if (mPrependItemsRequested > 0) {
265            // not done prepending, keep going
266            schedulePrepend();
267        }
268
269        // finally dispatch callbacks, after prepend may have already been scheduled
270        notifyChanged(leadingNulls, changedCount);
271        notifyInserted(0, addedCount);
272
273        offsetBoundaryAccessIndices(addedCount);
274    }
275
276    @MainThread
277    @Override
278    public void onPageAppended(int endPosition, int changedCount, int addedCount) {
279        // consider whether to post more work, now that a page is fully appended
280
281        mAppendItemsRequested = mAppendItemsRequested - changedCount - addedCount;
282        mAppendWorkerRunning = false;
283        if (mAppendItemsRequested > 0) {
284            // not done appending, keep going
285            scheduleAppend();
286        }
287
288        // finally dispatch callbacks, after append may have already been scheduled
289        notifyChanged(endPosition, changedCount);
290        notifyInserted(endPosition + changedCount, addedCount);
291    }
292
293    @MainThread
294    @Override
295    public void onPagePlaceholderInserted(int pageIndex) {
296        throw new IllegalStateException("Tiled callback on ContiguousPagedList");
297    }
298
299    @MainThread
300    @Override
301    public void onPageInserted(int start, int count) {
302        throw new IllegalStateException("Tiled callback on ContiguousPagedList");
303    }
304}
305