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