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.AnyThread;
20 import androidx.annotation.MainThread;
21 import androidx.annotation.NonNull;
22 import androidx.annotation.Nullable;
23 
24 import java.util.List;
25 import java.util.concurrent.Executor;
26 
27 class 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 
ContiguousPagedList( @onNull ContiguousDataSource<K, V> dataSource, @NonNull Executor mainThreadExecutor, @NonNull Executor backgroundThreadExecutor, @Nullable BoundaryCallback<V> boundaryCallback, @NonNull Config config, final @Nullable K key, int lastLoad)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
dispatchUpdatesSinceSnapshot( @onNull PagedList<V> pagedListSnapshot, @NonNull Callback callback)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
loadAroundInternal(int index)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
schedulePrepend()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
scheduleAppend()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
isContiguous()236     boolean isContiguous() {
237         return true;
238     }
239 
240     @NonNull
241     @Override
getDataSource()242     public DataSource<?, V> getDataSource() {
243         return mDataSource;
244     }
245 
246     @Nullable
247     @Override
getLastKey()248     public Object getLastKey() {
249         return mDataSource.getKey(mLastLoad, mLastItem);
250     }
251 
252     @MainThread
253     @Override
onInitialized(int count)254     public void onInitialized(int count) {
255         notifyInserted(0, count);
256     }
257 
258     @MainThread
259     @Override
onPagePrepended(int leadingNulls, int changedCount, int addedCount)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
onPageAppended(int endPosition, int changedCount, int addedCount)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
onPagePlaceholderInserted(int pageIndex)295     public void onPagePlaceholderInserted(int pageIndex) {
296         throw new IllegalStateException("Tiled callback on ContiguousPagedList");
297     }
298 
299     @MainThread
300     @Override
onPageInserted(int start, int count)301     public void onPageInserted(int start, int count) {
302         throw new IllegalStateException("Tiled callback on ContiguousPagedList");
303     }
304 }
305