blob: d8907c3b730fbd68dc0ad91b9383743ff02642a3 [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.arch.paging;
import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
import android.support.annotation.WorkerThread;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class ContiguousPagedList<T> extends NullPaddedList<T> {
private final ContiguousDataSource<?, T> mDataSource;
private final Executor mMainThreadExecutor;
private final Executor mBackgroundThreadExecutor;
private final Config mConfig;
private boolean mPrependWorkerRunning = false;
private boolean mAppendWorkerRunning = false;
private int mPrependItemsRequested = 0;
private int mAppendItemsRequested = 0;
private int mLastLoad = 0;
private T mLastItem = null;
private AtomicBoolean mDetached = new AtomicBoolean(false);
private ArrayList<WeakReference<Callback>> mCallbacks = new ArrayList<>();
@WorkerThread
<K> ContiguousPagedList(@NonNull ContiguousDataSource<K, T> dataSource,
@NonNull Executor mainThreadExecutor,
@NonNull Executor backgroundThreadExecutor,
Config config,
@Nullable K key) {
super();
mDataSource = dataSource;
mMainThreadExecutor = mainThreadExecutor;
mBackgroundThreadExecutor = backgroundThreadExecutor;
mConfig = config;
NullPaddedList<T> initialState = dataSource.loadInitial(
key, config.mInitialLoadSizeHint, config.mEnablePlaceholders);
if (initialState != null) {
mPositionOffset = initialState.getPositionOffset();
mLeadingNullCount = initialState.getLeadingNullCount();
mList = new ArrayList<>(initialState.mList);
mTrailingNullCount = initialState.getTrailingNullCount();
if (initialState.getLeadingNullCount() == 0
&& initialState.getTrailingNullCount() == 0
&& config.mPrefetchDistance < 1) {
throw new IllegalArgumentException("Null padding is required to support the 0"
+ " prefetch case - require either null items or prefetching to fetch"
+ " beyond initial load.");
}
if (initialState.size() != 0) {
mLastLoad = mLeadingNullCount + mList.size() / 2;
mLastItem = mList.get(mList.size() / 2);
}
} else {
mList = new ArrayList<>();
detach();
}
if (mList.size() == 0) {
// Empty initial state, so don't try and fetch data.
mPrependWorkerRunning = true;
mAppendWorkerRunning = true;
}
}
@Override
public T get(int index) {
T item = super.get(index);
if (item != null) {
mLastItem = item;
}
return item;
}
@Override
public void loadAround(int index) {
mLastLoad = index + mPositionOffset;
int prependItems = mConfig.mPrefetchDistance - (index - mLeadingNullCount);
int appendItems = index + mConfig.mPrefetchDistance - (mLeadingNullCount + mList.size());
mPrependItemsRequested = Math.max(prependItems, mPrependItemsRequested);
if (mPrependItemsRequested > 0) {
schedulePrepend();
}
mAppendItemsRequested = Math.max(appendItems, mAppendItemsRequested);
if (mAppendItemsRequested > 0) {
scheduleAppend();
}
}
@Override
public int getLoadedCount() {
return mList.size();
}
@Override
public int getLeadingNullCount() {
return mLeadingNullCount;
}
@Override
public int getTrailingNullCount() {
return mTrailingNullCount;
}
@MainThread
private void schedulePrepend() {
if (mPrependWorkerRunning) {
return;
}
mPrependWorkerRunning = true;
final int position = mLeadingNullCount + mPositionOffset;
final T item = mList.get(0);
mBackgroundThreadExecutor.execute(new Runnable() {
@Override
public void run() {
if (mDetached.get()) {
return;
}
final List<T> data = mDataSource.loadBefore(position, item, mConfig.mPageSize);
if (data != null) {
mMainThreadExecutor.execute(new Runnable() {
@Override
public void run() {
if (mDetached.get()) {
return;
}
prependImpl(data);
}
});
} else {
detach();
}
}
});
}
@MainThread
private void scheduleAppend() {
if (mAppendWorkerRunning) {
return;
}
mAppendWorkerRunning = true;
final int position = mLeadingNullCount + mList.size() - 1 + mPositionOffset;
final T item = mList.get(mList.size() - 1);
mBackgroundThreadExecutor.execute(new Runnable() {
@Override
public void run() {
if (mDetached.get()) {
return;
}
final List<T> data = mDataSource.loadAfter(position, item, mConfig.mPageSize);
if (data != null) {
mMainThreadExecutor.execute(new Runnable() {
@Override
public void run() {
if (mDetached.get()) {
return;
}
appendImpl(data);
}
});
} else {
detach();
}
}
});
}
@MainThread
private void prependImpl(List<T> before) {
final int count = before.size();
if (count == 0) {
// Nothing returned from source, stop loading in this direction
return;
}
Collections.reverse(before);
mList.addAll(0, before);
final int changedCount = Math.min(mLeadingNullCount, count);
final int addedCount = count - changedCount;
if (changedCount != 0) {
mLeadingNullCount -= changedCount;
}
mPositionOffset -= addedCount;
mNumberPrepended += count;
// only try to post more work after fully prepended (with offsets / null counts updated)
mPrependItemsRequested -= count;
mPrependWorkerRunning = false;
if (mPrependItemsRequested > 0) {
// not done prepending, keep going
schedulePrepend();
}
// finally dispatch callbacks, after prepend may have already been scheduled
for (WeakReference<Callback> weakRef : mCallbacks) {
Callback callback = weakRef.get();
if (callback != null) {
if (changedCount != 0) {
callback.onChanged(mLeadingNullCount, changedCount);
}
if (addedCount != 0) {
callback.onInserted(0, addedCount);
}
}
}
}
@MainThread
private void appendImpl(List<T> after) {
final int count = after.size();
if (count == 0) {
// Nothing returned from source, stop loading in this direction
return;
}
mList.addAll(after);
final int changedCount = Math.min(mTrailingNullCount, count);
final int addedCount = count - changedCount;
if (changedCount != 0) {
mTrailingNullCount -= changedCount;
}
mNumberAppended += count;
// only try to post more work after fully appended (with null counts updated)
mAppendItemsRequested -= count;
mAppendWorkerRunning = false;
if (mAppendItemsRequested > 0) {
// not done appending, keep going
scheduleAppend();
}
// finally dispatch callbacks, after append may have already been scheduled
for (WeakReference<Callback> weakRef : mCallbacks) {
Callback callback = weakRef.get();
if (callback != null) {
final int endPosition = mLeadingNullCount + mList.size() - count;
if (changedCount != 0) {
callback.onChanged(endPosition, changedCount);
}
if (addedCount != 0) {
callback.onInserted(endPosition + changedCount, addedCount);
}
}
}
}
@Override
public boolean isImmutable() {
// TODO: return true if had nulls, and now getLoadedCount() == size(). Is that safe?
// Currently we don't prevent DataSources from returning more items than their null counts
return isDetached();
}
@Override
public void addWeakCallback(@Nullable PagedList<T> previousSnapshot,
@NonNull Callback callback) {
NullPaddedList<T> snapshot = (NullPaddedList<T>) previousSnapshot;
if (snapshot != this && snapshot != null) {
final int newlyAppended = mNumberAppended - snapshot.getNumberAppended();
final int newlyPrepended = mNumberPrepended - snapshot.getNumberPrepended();
final int previousTrailing = snapshot.getTrailingNullCount();
final int previousLeading = snapshot.getLeadingNullCount();
// Validate that the snapshot looks like a previous version of this list - if it's not,
// we can't be sure we'll dispatch callbacks safely
if (newlyAppended < 0
|| newlyPrepended < 0
|| mTrailingNullCount != Math.max(previousTrailing - newlyAppended, 0)
|| mLeadingNullCount != Math.max(previousLeading - newlyPrepended, 0)
|| snapshot.getLoadedCount() + newlyAppended + newlyPrepended != mList.size()) {
throw new IllegalArgumentException("Invalid snapshot provided - doesn't appear"
+ " to be a snapshot of this list");
}
if (newlyAppended != 0) {
final int changedCount = Math.min(previousTrailing, newlyAppended);
final int addedCount = newlyAppended - changedCount;
final int endPosition =
snapshot.getLeadingNullCount() + snapshot.getLoadedCount();
if (changedCount != 0) {
callback.onChanged(endPosition, changedCount);
}
if (addedCount != 0) {
callback.onInserted(endPosition + changedCount, addedCount);
}
}
if (newlyPrepended != 0) {
final int changedCount = Math.min(previousLeading, newlyPrepended);
final int addedCount = newlyPrepended - changedCount;
if (changedCount != 0) {
callback.onChanged(previousLeading, changedCount);
}
if (addedCount != 0) {
callback.onInserted(0, addedCount);
}
}
}
mCallbacks.add(new WeakReference<>(callback));
}
@Override
public void removeWeakCallback(@NonNull Callback callback) {
for (int i = mCallbacks.size() - 1; i >= 0; i--) {
Callback currentCallback = mCallbacks.get(i).get();
if (currentCallback == null || currentCallback == callback) {
mCallbacks.remove(i);
}
}
}
@Override
public boolean isDetached() {
return mDetached.get();
}
@SuppressWarnings("WeakerAccess")
public void detach() {
mDetached.set(true);
}
@Nullable
@Override
public Object getLastKey() {
return mDataSource.getKey(mLastLoad, mLastItem);
}
}