Merge "PagedList storage and data access refactor" into oc-mr1-support-27.0-dev
diff --git a/paging/common/src/main/java/android/arch/paging/BoundedDataSource.java b/paging/common/src/main/java/android/arch/paging/BoundedDataSource.java
index 664ab16..0656490 100644
--- a/paging/common/src/main/java/android/arch/paging/BoundedDataSource.java
+++ b/paging/common/src/main/java/android/arch/paging/BoundedDataSource.java
@@ -21,7 +21,6 @@
import android.support.annotation.WorkerThread;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
/**
@@ -75,7 +74,6 @@
if (result.size() != loadSize) {
throw new IllegalStateException("invalid number of items returned.");
}
- Collections.reverse(result);
}
return result;
}
diff --git a/paging/common/src/main/java/android/arch/paging/ContiguousDataSource.java b/paging/common/src/main/java/android/arch/paging/ContiguousDataSource.java
index afcc208..be9da20 100644
--- a/paging/common/src/main/java/android/arch/paging/ContiguousDataSource.java
+++ b/paging/common/src/main/java/android/arch/paging/ContiguousDataSource.java
@@ -26,21 +26,65 @@
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public abstract class ContiguousDataSource<Key, Value> extends DataSource<Key, Value> {
- /**
- * Number of items that this DataSource can provide in total, or COUNT_UNDEFINED.
- *
- * @return number of items that this DataSource can provide in total, or COUNT_UNDEFINED
- * if difficult or undesired to compute.
- */
- public int countItems() {
- return COUNT_UNDEFINED;
- }
-
@Override
boolean isContiguous() {
return true;
}
+ void loadInitial(Key key, int pageSize, boolean enablePlaceholders,
+ PageResult.Receiver<Key, Value> receiver) {
+ NullPaddedList<Value> initial = loadInitial(key, pageSize, enablePlaceholders);
+ if (initial != null) {
+ receiver.onPageResult(new PageResult<>(
+ PageResult.INIT,
+ new Page<Key, Value>(initial.mList),
+ initial.getLeadingNullCount(),
+ initial.getTrailingNullCount(),
+ initial.getPositionOffset()));
+ } else {
+ receiver.onPageResult(new PageResult<Key, Value>(
+ PageResult.INIT, null, 0, 0, 0));
+ }
+ }
+
+ void loadAfter(int currentEndIndex, @NonNull Value currentEndItem, int pageSize,
+ PageResult.Receiver<Key, Value> receiver) {
+ List<Value> list = loadAfter(currentEndIndex, currentEndItem, pageSize);
+
+ Page<Key, Value> page = list != null
+ ? new Page<Key, Value>(list) : null;
+
+ receiver.postOnPageResult(new PageResult<>(
+ PageResult.APPEND, page, 0, 0, 0));
+ }
+
+ void loadBefore(int currentBeginIndex, @NonNull Value currentBeginItem, int pageSize,
+ PageResult.Receiver<Key, Value> receiver) {
+ List<Value> list = loadBefore(currentBeginIndex, currentBeginItem, pageSize);
+
+ Page<Key, Value> page = list != null
+ ? new Page<Key, Value>(list) : null;
+
+ receiver.postOnPageResult(new PageResult<>(
+ PageResult.PREPEND, page, 0, 0, 0));
+ }
+
+ /**
+ * Get the key from either the position, or item, or null if position/item invalid.
+ * <p>
+ * Position may not match passed item's position - if trying to query the key from a position
+ * that isn't yet loaded, a fallback item (last loaded item accessed) will be passed.
+ */
+ abstract Key getKey(int position, Value item);
+
+ @Nullable
+ abstract List<Value> loadAfterImpl(int currentEndIndex,
+ @NonNull Value currentEndItem, int pageSize);
+
+ @Nullable
+ abstract List<Value> loadBeforeImpl(int currentBeginIndex,
+ @NonNull Value currentBeginItem, int pageSize);
+
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@WorkerThread
@@ -48,21 +92,7 @@
public abstract NullPaddedList<Value> loadInitial(
Key key, int initialLoadSize, boolean enablePlaceholders);
- /**
- * Load data after the given position / item.
- * <p>
- * It's valid to return a different list size than the page size, if it's easier for this data
- * source. It is generally safer to increase number loaded than reduce.
- *
- * @param currentEndIndex Load items after this index, starting with currentEndIndex + 1.
- * @param currentEndItem Load items after this item, can be used for precise querying based on
- * item contents.
- * @param pageSize Suggested number of items to load.
- * @return List of items, starting at position currentEndIndex + 1. Null if the data source is
- * no longer valid, and should not be queried again.
- *
- * @hide
- */
+ /** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@WorkerThread
@Nullable
@@ -78,24 +108,7 @@
return list;
}
- @Nullable
- abstract List<Value> loadAfterImpl(int currentEndIndex,
- @NonNull Value currentEndItem, int pageSize);
-
- /**
- * Load data before the given position / item.
- * <p>
- * It's valid to return a different list size than the page size, if it's easier for this data
- * source. It is generally safer to increase number loaded than reduce.
- *
- * @param currentBeginIndex Load items before this index, starting with currentBeginIndex - 1.
- * @param currentBeginItem Load items after this item, can be used for precise querying based
- * on item contents.
- * @param pageSize Suggested number of items to load.
- * @return List of items, in descending order, starting at position currentBeginIndex - 1.
- *
- * @hide
- */
+ /** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@WorkerThread
@Nullable
@@ -111,15 +124,4 @@
return list;
}
-
- @Nullable
- abstract List<Value> loadBeforeImpl(int currentBeginIndex,
- @NonNull Value currentBeginItem, int pageSize);
-
- /**
- * Get the key from either the position, or item. Position may not match passed item's position,
- * if trying to query the key from a position that isn't yet loaded, so a fallback item must be
- * used.
- */
- abstract Key getKey(int position, Value item);
}
diff --git a/paging/common/src/main/java/android/arch/paging/ContiguousPagedList.java b/paging/common/src/main/java/android/arch/paging/ContiguousPagedList.java
index d8907c3..7835dbe 100644
--- a/paging/common/src/main/java/android/arch/paging/ContiguousPagedList.java
+++ b/paging/common/src/main/java/android/arch/paging/ContiguousPagedList.java
@@ -16,101 +16,136 @@
package android.arch.paging;
+import android.support.annotation.AnyThread;
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;
-
+class ContiguousPagedList<K, V> extends PagedList<V> implements PagedStorage.Callback {
+ private final ContiguousDataSource<K, V> mDataSource;
private boolean mPrependWorkerRunning = false;
private boolean mAppendWorkerRunning = false;
private int mPrependItemsRequested = 0;
private int mAppendItemsRequested = 0;
- private int mLastLoad = 0;
- private T mLastItem = null;
+ @SuppressWarnings("unchecked")
+ private final PagedStorage<K, V> mKeyedStorage = (PagedStorage<K, V>) mStorage;
- private AtomicBoolean mDetached = new AtomicBoolean(false);
+ private final PageResult.Receiver<K, V> mReceiver = new PageResult.Receiver<K, V>() {
+ @AnyThread
+ @Override
+ public void postOnPageResult(@NonNull final PageResult<K, V> pageResult) {
+ // NOTE: if we're already on main thread, this can delay page receive by a frame
+ mMainThreadExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ onPageResult(pageResult);
+ }
+ });
+ }
- private ArrayList<WeakReference<Callback>> mCallbacks = new ArrayList<>();
+ @MainThread
+ @Override
+ public void onPageResult(@NonNull PageResult<K, V> pageResult) {
+ if (pageResult.page == null) {
+ detach();
+ return;
+ }
- @WorkerThread
- <K> ContiguousPagedList(@NonNull ContiguousDataSource<K, T> dataSource,
+ if (isDetached()) {
+ // No op, have detached
+ return;
+ }
+
+ Page<K, V> page = pageResult.page;
+ if (pageResult.type == PageResult.INIT) {
+ mKeyedStorage.init(pageResult.leadingNulls, page, pageResult.trailingNulls,
+ pageResult.positionOffset, ContiguousPagedList.this);
+ notifyInserted(0, mKeyedStorage.size());
+ } else if (pageResult.type == PageResult.APPEND) {
+ mKeyedStorage.appendPage(page, ContiguousPagedList.this);
+ } else if (pageResult.type == PageResult.PREPEND) {
+ mKeyedStorage.prependPage(page, ContiguousPagedList.this);
+ }
+ }
+ };
+
+ ContiguousPagedList(
+ @NonNull ContiguousDataSource<K, V> dataSource,
@NonNull Executor mainThreadExecutor,
@NonNull Executor backgroundThreadExecutor,
- Config config,
- @Nullable K key) {
- super();
-
+ @NonNull Config config,
+ final @Nullable K key) {
+ super(new PagedStorage<K, V>(), mainThreadExecutor, backgroundThreadExecutor, config);
mDataSource = dataSource;
- mMainThreadExecutor = mainThreadExecutor;
- mBackgroundThreadExecutor = backgroundThreadExecutor;
- mConfig = config;
- NullPaddedList<T> initialState = dataSource.loadInitial(
- key, config.mInitialLoadSizeHint, config.mEnablePlaceholders);
- if (initialState != null) {
- mPositionOffset = initialState.getPositionOffset();
+ // blocking init just triggers the initial load on the construction thread -
+ // Could still be posted with callback, if desired.
+ mDataSource.loadInitial(key,
+ mConfig.mInitialLoadSizeHint,
+ mConfig.mEnablePlaceholders,
+ mReceiver);
+ }
- mLeadingNullCount = initialState.getLeadingNullCount();
- mList = new ArrayList<>(initialState.mList);
- mTrailingNullCount = initialState.getTrailingNullCount();
+ @MainThread
+ @Override
+ void dispatchUpdatesSinceSnapshot(
+ @NonNull PagedList<V> pagedListSnapshot, @NonNull Callback callback) {
- 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.");
- }
+ final PagedStorage<?, V> snapshot = pagedListSnapshot.mStorage;
- if (initialState.size() != 0) {
- mLastLoad = mLeadingNullCount + mList.size() / 2;
- mLastItem = mList.get(mList.size() / 2);
- }
- } else {
- mList = new ArrayList<>();
- detach();
+ final int newlyAppended = mStorage.getNumberAppended() - snapshot.getNumberAppended();
+ final int newlyPrepended = mStorage.getNumberPrepended() - 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
+ || mStorage.getTrailingNullCount() != Math.max(previousTrailing - newlyAppended, 0)
+ || mStorage.getLeadingNullCount() != Math.max(previousLeading - newlyPrepended, 0)
+ || (mStorage.getStorageCount()
+ != snapshot.getStorageCount() + newlyAppended + newlyPrepended)) {
+ throw new IllegalArgumentException("Invalid snapshot provided - doesn't appear"
+ + " to be a snapshot of this PagedList");
}
- if (mList.size() == 0) {
- // Empty initial state, so don't try and fetch data.
- mPrependWorkerRunning = true;
- mAppendWorkerRunning = true;
+
+ if (newlyAppended != 0) {
+ final int changedCount = Math.min(previousTrailing, newlyAppended);
+ final int addedCount = newlyAppended - changedCount;
+
+ final int endPosition = snapshot.getLeadingNullCount() + snapshot.getStorageCount();
+ 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);
+ }
}
}
+ @MainThread
@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());
+ protected void loadAroundInternal(int index) {
+ int prependItems = mConfig.mPrefetchDistance - (index - mStorage.getLeadingNullCount());
+ int appendItems = index + mConfig.mPrefetchDistance
+ - (mStorage.getLeadingNullCount() + mStorage.getStorageCount());
mPrependItemsRequested = Math.max(prependItems, mPrependItemsRequested);
if (mPrependItemsRequested > 0) {
@@ -123,21 +158,6 @@
}
}
- @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) {
@@ -145,29 +165,17 @@
}
mPrependWorkerRunning = true;
- final int position = mLeadingNullCount + mPositionOffset;
- final T item = mList.get(0);
+ final int position = mStorage.getLeadingNullCount() + mStorage.getPositionOffset();
+
+ // safe to access first item here - mStorage can't be empty if we're prepending
+ final V item = mStorage.getFirstContiguousItem();
mBackgroundThreadExecutor.execute(new Runnable() {
@Override
public void run() {
- if (mDetached.get()) {
+ if (isDetached()) {
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();
- }
+ mDataSource.loadBefore(position, item, mConfig.mPageSize, mReceiver);
}
});
}
@@ -179,56 +187,44 @@
}
mAppendWorkerRunning = true;
- final int position = mLeadingNullCount + mList.size() - 1 + mPositionOffset;
- final T item = mList.get(mList.size() - 1);
+ final int position = mStorage.getLeadingNullCount()
+ + mStorage.getStorageCount() - 1 + mStorage.getPositionOffset();
+
+ // safe to access first item here - mStorage can't be empty if we're appending
+ final V item = mStorage.getLastContiguousItem();
mBackgroundThreadExecutor.execute(new Runnable() {
@Override
public void run() {
- if (mDetached.get()) {
+ if (isDetached()) {
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();
- }
+ mDataSource.loadAfter(position, item, mConfig.mPageSize, mReceiver);
}
});
}
+ @Override
+ boolean isContiguous() {
+ return true;
+ }
+
+ @Nullable
+ @Override
+ public Object getLastKey() {
+ return mDataSource.getKey(mLastLoad, mLastItem);
+ }
+
@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;
- }
+ @Override
+ public void onInitialized(int count) {
+ notifyInserted(0, count);
+ }
- 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;
+ @MainThread
+ @Override
+ public void onPagePrepended(int leadingNulls, int changedCount, int addedCount) {
+ // consider whether to post more work, now that a page is fully prepended
+ mPrependItemsRequested = mPrependItemsRequested - changedCount - addedCount;
mPrependWorkerRunning = false;
if (mPrependItemsRequested > 0) {
// not done prepending, keep going
@@ -236,39 +232,16 @@
}
// 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);
- }
- }
- }
+ notifyChanged(leadingNulls, changedCount);
+ notifyInserted(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;
- }
+ @Override
+ public void onPageAppended(int endPosition, int changedCount, int addedCount) {
+ // consider whether to post more work, now that a page is fully appended
- 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;
+ mAppendItemsRequested = mAppendItemsRequested - changedCount - addedCount;
mAppendWorkerRunning = false;
if (mAppendItemsRequested > 0) {
// not done appending, keep going
@@ -276,100 +249,19 @@
}
// 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);
- }
- }
- }
+ notifyChanged(endPosition, changedCount);
+ notifyInserted(endPosition + changedCount, addedCount);
}
+ @MainThread
@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();
+ public void onPagePlaceholderInserted(int pageIndex) {
+ throw new IllegalStateException("Tiled callback on ContiguousPagedList");
}
+ @MainThread
@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);
+ public void onPageInserted(int start, int count) {
+ throw new IllegalStateException("Tiled callback on ContiguousPagedList");
}
}
diff --git a/paging/common/src/main/java/android/arch/paging/DataSource.java b/paging/common/src/main/java/android/arch/paging/DataSource.java
index 48fbec5..524e570 100644
--- a/paging/common/src/main/java/android/arch/paging/DataSource.java
+++ b/paging/common/src/main/java/android/arch/paging/DataSource.java
@@ -17,6 +17,7 @@
package android.arch.paging;
import android.support.annotation.AnyThread;
+import android.support.annotation.NonNull;
import android.support.annotation.WorkerThread;
import java.util.concurrent.CopyOnWriteArrayList;
@@ -60,15 +61,6 @@
public static int COUNT_UNDEFINED = -1;
/**
- * Number of items that this DataSource can provide in total, or {@link #COUNT_UNDEFINED}.
- *
- * @return number of items that this DataSource can provide in total, or
- * {@link #COUNT_UNDEFINED} if expensive or undesired to compute.
- */
- @WorkerThread
- public abstract int countItems();
-
- /**
* Returns true if the data source guaranteed to produce a contiguous set of items,
* never producing gaps.
*/
@@ -111,7 +103,7 @@
*/
@AnyThread
@SuppressWarnings("WeakerAccess")
- public void addInvalidatedCallback(InvalidatedCallback onInvalidatedCallback) {
+ public void addInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
mOnInvalidatedCallbacks.add(onInvalidatedCallback);
}
@@ -122,7 +114,7 @@
*/
@AnyThread
@SuppressWarnings("WeakerAccess")
- public void removeInvalidatedCallback(InvalidatedCallback onInvalidatedCallback) {
+ public void removeInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
mOnInvalidatedCallbacks.remove(onInvalidatedCallback);
}
diff --git a/paging/common/src/main/java/android/arch/paging/KeyedDataSource.java b/paging/common/src/main/java/android/arch/paging/KeyedDataSource.java
index 8cf6829..0d45294 100644
--- a/paging/common/src/main/java/android/arch/paging/KeyedDataSource.java
+++ b/paging/common/src/main/java/android/arch/paging/KeyedDataSource.java
@@ -103,10 +103,6 @@
* @param <Value> Type of items being loaded by the DataSource.
*/
public abstract class KeyedDataSource<Key, Value> extends ContiguousDataSource<Key, Value> {
- @Override
- public final int countItems() {
- return 0; // method not called, can't be overridden
- }
@Nullable
@Override
@@ -118,7 +114,14 @@
@Override
List<Value> loadBeforeImpl(
int currentBeginIndex, @NonNull Value currentBeginItem, int pageSize) {
- return loadBefore(getKey(currentBeginItem), pageSize);
+ List<Value> list = loadBefore(getKey(currentBeginItem), pageSize);
+
+ if (list != null && list.size() > 1) {
+ // TODO: move out of keyed entirely, into the DB DataSource.
+ list = new ArrayList<>(list);
+ Collections.reverse(list);
+ }
+ return list;
}
@Nullable
@@ -191,6 +194,8 @@
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @WorkerThread
+ @Override
public NullPaddedList<Value> loadInitial(
@Nullable Key key, int initialLoadSize, boolean enablePlaceholders) {
if (isInvalid()) {
diff --git a/paging/common/src/main/java/android/arch/paging/NullPaddedList.java b/paging/common/src/main/java/android/arch/paging/NullPaddedList.java
index 4300030..c7b0b23 100644
--- a/paging/common/src/main/java/android/arch/paging/NullPaddedList.java
+++ b/paging/common/src/main/java/android/arch/paging/NullPaddedList.java
@@ -16,11 +16,9 @@
package android.arch.paging;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
-import java.util.ArrayList;
+import java.util.AbstractList;
import java.util.List;
/**
@@ -31,18 +29,11 @@
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class NullPaddedList<Type> extends PagedList<Type> {
+public class NullPaddedList<Type> extends AbstractList<Type> {
List<Type> mList;
- int mTrailingNullCount;
- int mLeadingNullCount;
- int mPositionOffset;
-
- // track the items prepended/appended since the PagedList was initialized
- int mNumberPrepended;
- int mNumberAppended;
-
- NullPaddedList() {
- }
+ private int mTrailingNullCount;
+ private int mLeadingNullCount;
+ private int mPositionOffset;
@Override
public String toString() {
@@ -91,20 +82,6 @@
mPositionOffset = positionOffset;
}
- /**
- * Create a copy of the passed NullPaddedList.
- *
- * @param other Other list to copy.
- */
- NullPaddedList(NullPaddedList<Type> other) {
- mLeadingNullCount = other.getLeadingNullCount();
- mList = other.isImmutable() ? other.mList : new ArrayList<>(other.mList);
- mTrailingNullCount = other.getTrailingNullCount();
-
- mNumberPrepended = other.getNumberPrepended();
- mNumberAppended = other.getNumberAppended();
- }
-
// --------------- PagedList API ---------------
@Override
@@ -124,46 +101,12 @@
}
@Override
- public void loadAround(int index) {
- // do nothing - immutable, so no fetching will be done
- }
-
- @Override
public final int size() {
return getLoadedCount() + getLeadingNullCount() + getTrailingNullCount();
}
- public boolean isImmutable() {
- return true;
- }
-
- @Override
- public PagedList<Type> snapshot() {
- if (isImmutable()) {
- return this;
- }
- return new NullPaddedList<>(this);
- }
-
- @Override
- boolean isContiguous() {
- return true;
- }
-
- @Override
- public void addWeakCallback(@Nullable PagedList<Type> previousSnapshot,
- @NonNull Callback callback) {
- // no op, immutable
- }
-
- @Override
- public void removeWeakCallback(Callback callback) {
- // no op, immutable
- }
-
// --------------- Contiguous API ---------------
- @Override
public int getPositionOffset() {
return mPositionOffset;
}
@@ -194,12 +137,4 @@
public int getTrailingNullCount() {
return mTrailingNullCount;
}
-
- int getNumberPrepended() {
- return mNumberPrepended;
- }
-
- int getNumberAppended() {
- return mNumberAppended;
- }
}
diff --git a/paging/common/src/main/java/android/arch/paging/Page.java b/paging/common/src/main/java/android/arch/paging/Page.java
new file mode 100644
index 0000000..e9890ed
--- /dev/null
+++ b/paging/common/src/main/java/android/arch/paging/Page.java
@@ -0,0 +1,51 @@
+/*
+ * 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.NonNull;
+import android.support.annotation.Nullable;
+
+import java.util.List;
+
+/**
+ * Immutable class representing a page of data loaded from a DataSource.
+ * <p>
+ * Optionally stores before/after keys for cases where they cannot be computed, but the DataSource
+ * can provide them as part of loading a page.
+ * <p>
+ * A page's list must never be modified.
+ */
+class Page<K, V> {
+ @SuppressWarnings("WeakerAccess")
+ @Nullable
+ public final K beforeKey;
+ @NonNull
+ public final List<V> items;
+ @SuppressWarnings("WeakerAccess")
+ @Nullable
+ public K afterKey;
+
+ Page(@NonNull List<V> items) {
+ this(null, items, null);
+ }
+
+ Page(@Nullable K beforeKey, @NonNull List<V> items, @Nullable K afterKey) {
+ this.beforeKey = beforeKey;
+ this.items = items;
+ this.afterKey = afterKey;
+ }
+}
diff --git a/paging/common/src/main/java/android/arch/paging/PageArrayList.java b/paging/common/src/main/java/android/arch/paging/PageArrayList.java
deleted file mode 100644
index b90d055..0000000
--- a/paging/common/src/main/java/android/arch/paging/PageArrayList.java
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * 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.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/** @hide */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-class PageArrayList<T> extends PagedList<T> {
- // partial list of pages, doesn't include pages below the lowest accessed, or above the highest
- final ArrayList<List<T>> mPages;
-
- // to access page at index N, do mPages.get(N - mPageIndexOffset), but do bounds checking first!
- int mPageIndexOffset;
-
- final int mPageSize;
- final int mCount;
- final int mMaxPageCount;
-
- PageArrayList(int pageSize, int count) {
- mPages = new ArrayList<>();
- mPageSize = pageSize;
- mCount = count;
- mMaxPageCount = (mCount + mPageSize - 1) / mPageSize;
- }
-
- private PageArrayList(PageArrayList<T> other) {
- mPages = other.isImmutable() ? other.mPages : new ArrayList<>(other.mPages);
- mPageIndexOffset = other.mPageIndexOffset;
- mPageSize = other.mPageSize;
- mCount = other.size();
- mMaxPageCount = other.mMaxPageCount;
- }
-
- @Override
- public T get(int index) {
- if (index < 0 || index >= mCount) {
- throw new IllegalArgumentException();
- }
-
- int localPageIndex = getLocalPageIndex(index);
-
- List<T> page = getPage(localPageIndex);
-
- if (page == null) {
- // page empty
- return null;
- }
-
- return page.get(index % mPageSize);
- }
-
- @Nullable
- private List<T> getPage(int localPageIndex) {
- if (localPageIndex < 0 || localPageIndex >= mPages.size()) {
- // page not present
- return null;
- }
-
- return mPages.get(localPageIndex);
- }
-
- private int getLocalPageIndex(int index) {
- return index / mPageSize - mPageIndexOffset;
- }
-
- @Override
- public void loadAround(int index) {
- // do nothing - immutable, so no fetching will be done
- }
-
- @Override
- public int size() {
- return mCount;
- }
-
- @Override
- public boolean isImmutable() {
- return true;
- }
-
- boolean hasPage(int pageIndex) {
- final int localPageIndex = pageIndex - mPageIndexOffset;
- List<T> page = getPage(localPageIndex);
- return page != null && page.size() != 0;
- }
-
- @Override
- public PagedList<T> snapshot() {
- if (isImmutable()) {
- return this;
- }
- return new PageArrayList<>(this);
- }
-
- @Override
- boolean isContiguous() {
- return false;
- }
-
- @Override
- public void addWeakCallback(@Nullable PagedList<T> previousSnapshot,
- @NonNull Callback callback) {
- // no op, immutable
- }
-
- @Override
- public void removeWeakCallback(Callback callback) {
- // no op, immutable
- }
-}
diff --git a/paging/common/src/main/java/android/arch/paging/PageResult.java b/paging/common/src/main/java/android/arch/paging/PageResult.java
new file mode 100644
index 0000000..a4090f6
--- /dev/null
+++ b/paging/common/src/main/java/android/arch/paging/PageResult.java
@@ -0,0 +1,56 @@
+/*
+ * 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.AnyThread;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+
+class PageResult<K, V> {
+ static final int INIT = 0;
+
+ // contiguous results
+ static final int APPEND = 1;
+ static final int PREPEND = 2;
+
+ // non-contiguous, tile result
+ static final int TILE = 3;
+
+ public final int type;
+ public final Page<K, V> page;
+ @SuppressWarnings("WeakerAccess")
+ public final int leadingNulls;
+ @SuppressWarnings("WeakerAccess")
+ public final int trailingNulls;
+ @SuppressWarnings("WeakerAccess")
+ public final int positionOffset;
+
+ PageResult(int type, Page<K, V> page, int leadingNulls, int trailingNulls, int positionOffset) {
+ this.type = type;
+ this.page = page;
+ this.leadingNulls = leadingNulls;
+ this.trailingNulls = trailingNulls;
+ this.positionOffset = positionOffset;
+ }
+
+ interface Receiver<K, V> {
+ @AnyThread
+ void postOnPageResult(@NonNull PageResult<K, V> pageResult);
+ @MainThread
+ void onPageResult(@NonNull PageResult<K, V> pageResult);
+ }
+}
diff --git a/paging/common/src/main/java/android/arch/paging/PagedList.java b/paging/common/src/main/java/android/arch/paging/PagedList.java
index 6a31b68..1f07bfa 100644
--- a/paging/common/src/main/java/android/arch/paging/PagedList.java
+++ b/paging/common/src/main/java/android/arch/paging/PagedList.java
@@ -20,9 +20,12 @@
import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread;
+import java.lang.ref.WeakReference;
import java.util.AbstractList;
+import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
/**
* Lazy loading list that pages in content from a {@link DataSource}.
@@ -90,9 +93,28 @@
* @param <T> The type of the entries in the list.
*/
public abstract class PagedList<T> extends AbstractList<T> {
- // Since we currently rely on implementation details of two implementations,
- // prevent external subclassing
- PagedList() {
+ final Executor mMainThreadExecutor;
+ final Executor mBackgroundThreadExecutor;
+ final Config mConfig;
+
+ @NonNull
+ final PagedStorage<?, T> mStorage;
+
+ int mLastLoad = 0;
+ T mLastItem = null;
+
+ private final AtomicBoolean mDetached = new AtomicBoolean(false);
+
+ protected final ArrayList<WeakReference<Callback>> mCallbacks = new ArrayList<>();
+
+ PagedList(@NonNull PagedStorage<?, T> storage,
+ @NonNull Executor mainThreadExecutor,
+ @NonNull Executor backgroundThreadExecutor,
+ @NonNull Config config) {
+ mStorage = storage;
+ mMainThreadExecutor = mainThreadExecutor;
+ mBackgroundThreadExecutor = backgroundThreadExecutor;
+ mConfig = config;
}
/**
@@ -280,7 +302,13 @@
*/
@Override
@Nullable
- public abstract T get(int index);
+ public T get(int index) {
+ T item = mStorage.get(index);
+ if (item != null) {
+ mLastItem = item;
+ }
+ return item;
+ }
/**
@@ -288,7 +316,10 @@
*
* @param index Index at which to load.
*/
- public abstract void loadAround(int index);
+ public void loadAround(int index) {
+ mLastLoad = index + getPositionOffset();
+ loadAroundInternal(index);
+ }
/**
@@ -297,7 +328,9 @@
* @return Current total size of the list.
*/
@Override
- public abstract int size();
+ public int size() {
+ return mStorage.size();
+ }
/**
* Returns whether the list is immutable. Immutable lists may not become mutable again, and may
@@ -305,15 +338,25 @@
*
* @return True if the PagedList is immutable.
*/
- public abstract boolean isImmutable();
+ @SuppressWarnings("WeakerAccess")
+ public boolean isImmutable() {
+ return isDetached();
+ }
/**
* Returns an immutable snapshot of the PagedList. If this PagedList is already
* immutable, it will be returned.
*
- * @return Immutable snapshot of PagedList, which may be the PagedList itself.
+ * @return Immutable snapshot of PagedList data.
*/
- public abstract List<T> snapshot();
+ @NonNull
+ public List<T> snapshot() {
+ if (isImmutable()) {
+ return this;
+ }
+
+ return new SnapshotPagedList<>(this);
+ }
abstract boolean isContiguous();
@@ -328,9 +371,7 @@
* @return Key of position most recently passed to {@link #loadAround(int)}.
*/
@Nullable
- public Object getLastKey() {
- return null;
- }
+ public abstract Object getLastKey();
/**
* True if the PagedList has detached the DataSource it was loading from, and will no longer
@@ -338,8 +379,9 @@
*
* @return True if the data source is detached.
*/
+ @SuppressWarnings("WeakerAccess")
public boolean isDetached() {
- return true;
+ return mDetached.get();
}
/**
@@ -349,7 +391,9 @@
* signal to stop loading. The PagedList will continue to present existing data, but will not
* initiate new loads.
*/
+ @SuppressWarnings("WeakerAccess")
public void detach() {
+ mDetached.set(true);
}
/**
@@ -361,7 +405,7 @@
* If the DataSource is a {@link KeyedDataSource}, and thus doesn't use positions, returns 0.
*/
public int getPositionOffset() {
- return 0;
+ return mStorage.getPositionOffset();
}
/**
@@ -385,16 +429,68 @@
* @param callback Callback to dispatch to.
* @see #removeWeakCallback(Callback)
*/
- public abstract void addWeakCallback(@Nullable PagedList<T> previousSnapshot,
- @NonNull Callback callback);
+ @SuppressWarnings("WeakerAccess")
+ public void addWeakCallback(@Nullable List<T> previousSnapshot, @NonNull Callback callback) {
+ if (previousSnapshot != null && previousSnapshot != this) {
+ PagedList<T> storageSnapshot = (PagedList<T>) previousSnapshot;
+ //noinspection unchecked
+ dispatchUpdatesSinceSnapshot(storageSnapshot, callback);
+ }
+ // first, clean up any empty weak refs
+ for (int i = mCallbacks.size() - 1; i >= 0; i--) {
+ Callback currentCallback = mCallbacks.get(i).get();
+ if (currentCallback == null) {
+ mCallbacks.remove(i);
+ }
+ }
+
+ // then add the new one
+ mCallbacks.add(new WeakReference<>(callback));
+ }
/**
* Removes a previously added callback.
*
* @param callback Callback, previously added.
- * @see #addWeakCallback(PagedList, Callback)
+ * @see #addWeakCallback(List, Callback)
*/
- public abstract void removeWeakCallback(Callback callback);
+ @SuppressWarnings("WeakerAccess")
+ 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) {
+ // found callback, or empty weak ref
+ mCallbacks.remove(i);
+ }
+ }
+ }
+
+ void notifyInserted(int position, int count) {
+ if (count != 0) {
+ for (int i = mCallbacks.size() - 1; i >= 0; i--) {
+ Callback callback = mCallbacks.get(i).get();
+ if (callback != null) {
+ callback.onInserted(position, count);
+ }
+ }
+ }
+ }
+
+ void notifyChanged(int position, int count) {
+ if (count != 0) {
+ for (int i = mCallbacks.size() - 1; i >= 0; i--) {
+ Callback callback = mCallbacks.get(i).get();
+ if (callback != null) {
+ callback.onChanged(position, count);
+ }
+ }
+ }
+ }
+
+ abstract void dispatchUpdatesSinceSnapshot(@NonNull PagedList<T> snapshot,
+ @NonNull Callback callback);
+
+ abstract void loadAroundInternal(int index);
/**
* Callback signaling when content is loaded into the list.
@@ -545,10 +641,15 @@
* Defines how many items to load when first load occurs, if you are using a
* {@link KeyedDataSource}.
* <p>
- * If you are using an {@link TiledDataSource}, this value is currently ignored.
- * Otherwise, this value will be passed to
- * {@link KeyedDataSource#loadInitial(int)} to load a (typically) larger amount
- * of data on first load.
+ * This value is typically larger than page size, so on first load data there's a large
+ * enough range of content loaded to cover small scrolls.
+ * <p>
+ * If used with a {@link TiledDataSource}, this value is rounded to the nearest number
+ * of pages, with a minimum of two pages, and loaded with a single call to
+ * {@link TiledDataSource#loadRange(int, int)}.
+ * <p>
+ * If used with a {@link KeyedDataSource}, this value will be passed to
+ * {@link KeyedDataSource#loadInitial(int)}.
* <p>
* If not set, defaults to three times page size.
*
diff --git a/paging/common/src/main/java/android/arch/paging/PagedStorage.java b/paging/common/src/main/java/android/arch/paging/PagedStorage.java
new file mode 100644
index 0000000..7f91290
--- /dev/null
+++ b/paging/common/src/main/java/android/arch/paging/PagedStorage.java
@@ -0,0 +1,433 @@
+/*
+ * 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.NonNull;
+
+import java.util.AbstractList;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+final class PagedStorage<K, V> extends AbstractList<V> {
+ // Always set
+ private int mLeadingNullCount;
+ /**
+ * List of pages in storage.
+ *
+ * Two storage modes:
+ *
+ * Contiguous - all content in mPages is valid and loaded, but may return false from isTiled().
+ * Safe to access any item in any page.
+ *
+ * Non-contiguous - mPages may have nulls or a placeholder page, isTiled() always returns true.
+ * mPages may have nulls, or placeholder (empty) pages while content is loading.
+ */
+ private final ArrayList<Page<K, V>> mPages;
+ private int mTrailingNullCount;
+
+ private int mPositionOffset;
+ /**
+ * Number of items represented by {@link #mPages}. If tiling is enabled, unloaded items in
+ * {@link #mPages} may be null, but this value still counts them.
+ */
+ private int mStorageCount;
+
+ // If mPageSize > 0, tiling is enabled, 'mPages' may have gaps, and leadingPages is set
+ private int mPageSize;
+
+ private int mNumberPrepended;
+ private int mNumberAppended;
+
+ // only used in tiling case
+ private Page<K, V> mPlaceholderPage;
+
+ PagedStorage() {
+ mLeadingNullCount = 0;
+ mPages = new ArrayList<>();
+ mTrailingNullCount = 0;
+ mPositionOffset = 0;
+ mStorageCount = 0;
+ mPageSize = 1;
+ mNumberPrepended = 0;
+ mNumberAppended = 0;
+ }
+
+ PagedStorage(int leadingNulls, Page<K, V> page, int trailingNulls) {
+ this();
+ init(leadingNulls, page, trailingNulls, 0);
+ }
+
+ private PagedStorage(PagedStorage<K, V> other) {
+ mLeadingNullCount = other.mLeadingNullCount;
+ mPages = new ArrayList<>(other.mPages);
+ mTrailingNullCount = other.mTrailingNullCount;
+ mPositionOffset = other.mPositionOffset;
+ mStorageCount = other.mStorageCount;
+ mPageSize = other.mPageSize;
+ mNumberPrepended = other.mNumberPrepended;
+ mNumberAppended = other.mNumberAppended;
+
+ // preserve placeholder page so we can locate placeholder pages if needed later
+ mPlaceholderPage = other.mPlaceholderPage;
+ }
+
+ PagedStorage<K, V> snapshot() {
+ return new PagedStorage<>(this);
+ }
+
+ private void init(int leadingNulls, Page<K, V> page, int trailingNulls, int positionOffset) {
+ mLeadingNullCount = leadingNulls;
+ mPages.clear();
+ mPages.add(page);
+ mTrailingNullCount = trailingNulls;
+
+ mPositionOffset = positionOffset;
+ mStorageCount = page.items.size();
+
+ // initialized as tiled. There may be 3 nulls, 2 items, but we still call this tiled
+ // even if it will break if nulls convert.
+ mPageSize = page.items.size();
+
+ mNumberPrepended = 0;
+ mNumberAppended = 0;
+ }
+
+ void init(int leadingNulls, Page<K, V> page, int trailingNulls, int positionOffset,
+ @NonNull Callback callback) {
+ init(leadingNulls, page, trailingNulls, positionOffset);
+ callback.onInitialized(size());
+ }
+
+ @Override
+ public V get(int i) {
+ if (i < 0 || i >= size()) {
+ throw new IndexOutOfBoundsException("Index: " + i + ", Size: " + size());
+ }
+
+ // is it definitely outside 'mPages'?
+ int localIndex = i - mLeadingNullCount;
+ if (localIndex < 0 || localIndex >= mStorageCount) {
+ return null;
+ }
+
+ int localPageIndex;
+ int pageInternalIndex;
+
+ if (isTiled()) {
+ // it's inside mPages, and we're tiled. Jump to correct tile.
+ localPageIndex = localIndex / mPageSize;
+ pageInternalIndex = localIndex % mPageSize;
+ } else {
+ // it's inside mPages, but page sizes aren't regular. Walk to correct tile.
+ // Pages can only be null while tiled, so accessing page count is safe.
+ pageInternalIndex = localIndex;
+ final int localPageCount = mPages.size();
+ for (localPageIndex = 0; localPageIndex < localPageCount; localPageIndex++) {
+ int pageSize = mPages.get(localPageIndex).items.size();
+ if (pageSize > pageInternalIndex) {
+ // stop, found the page
+ break;
+ }
+ pageInternalIndex -= pageSize;
+ }
+ }
+
+ Page<?, V> page = mPages.get(localPageIndex);
+ if (page == null || page.items.size() == 0) {
+ // can only occur in tiled case, with untouched inner/placeholder pages
+ return null;
+ }
+ return page.items.get(pageInternalIndex);
+ }
+
+ /**
+ * Returns true if all pages are the same size, except for the last, which may be smaller
+ */
+ boolean isTiled() {
+ return mPageSize > 0;
+ }
+
+ int getLeadingNullCount() {
+ return mLeadingNullCount;
+ }
+
+ int getTrailingNullCount() {
+ return mTrailingNullCount;
+ }
+
+ int getStorageCount() {
+ return mStorageCount;
+ }
+
+ int getNumberAppended() {
+ return mNumberAppended;
+ }
+
+ int getNumberPrepended() {
+ return mNumberPrepended;
+ }
+
+ int getPageCount() {
+ return mPages.size();
+ }
+
+ interface Callback {
+ void onInitialized(int count);
+ void onPagePrepended(int leadingNulls, int changed, int added);
+ void onPageAppended(int endPosition, int changed, int added);
+ void onPagePlaceholderInserted(int pageIndex);
+ void onPageInserted(int start, int count);
+ }
+
+ int getPositionOffset() {
+ return mPositionOffset;
+ }
+
+ @Override
+ public int size() {
+ return mLeadingNullCount + mStorageCount + mTrailingNullCount;
+ }
+
+ int computeLeadingNulls() {
+ int total = mLeadingNullCount;
+ final int pageCount = mPages.size();
+ for (int i = 0; i < pageCount; i++) {
+ Page page = mPages.get(i);
+ if (page != null && page != mPlaceholderPage) {
+ break;
+ }
+ total += mPageSize;
+ }
+ return total;
+ }
+
+ int computeTrailingNulls() {
+ int total = mTrailingNullCount;
+ for (int i = mPages.size() - 1; i >= 0; i--) {
+ Page page = mPages.get(i);
+ if (page != null && page != mPlaceholderPage) {
+ break;
+ }
+ total += mPageSize;
+ }
+ return total;
+ }
+
+ // ---------------- Contiguous API -------------------
+
+ V getFirstContiguousItem() {
+ // safe to access first page's first item here:
+ // If contiguous, mPages can't be empty, can't hold null Pages, and items can't be empty
+ return mPages.get(0).items.get(0);
+ }
+
+ V getLastContiguousItem() {
+ // safe to access last page's last item here:
+ // If contiguous, mPages can't be empty, can't hold null Pages, and items can't be empty
+ Page<K, V> page = mPages.get(mPages.size() - 1);
+ return page.items.get(page.items.size() - 1);
+ }
+
+ public void prependPage(@NonNull Page<K, V> page, @NonNull Callback callback) {
+ final int count = page.items.size();
+ if (count == 0) {
+ // Nothing returned from source, stop loading in this direction
+ return;
+ }
+ if (mPageSize > 0 && count != mPageSize) {
+ if (mPages.size() == 1 && count > mPageSize) {
+ // prepending to a single item - update current page size to that of 'inner' page
+ mPageSize = count;
+ } else {
+ // no longer tiled
+ mPageSize = -1;
+ }
+ }
+
+ mPages.add(0, page);
+ mStorageCount += count;
+
+ final int changedCount = Math.min(mLeadingNullCount, count);
+ final int addedCount = count - changedCount;
+
+ if (changedCount != 0) {
+ mLeadingNullCount -= changedCount;
+ }
+ mPositionOffset -= addedCount;
+ mNumberPrepended += count;
+
+ callback.onPagePrepended(mLeadingNullCount, changedCount, addedCount);
+ }
+
+ public void appendPage(@NonNull Page<K, V> page, @NonNull Callback callback) {
+ final int count = page.items.size();
+ if (count == 0) {
+ // Nothing returned from source, stop loading in this direction
+ return;
+ }
+
+ if (mPageSize > 0) {
+ // if the previous page was smaller than mPageSize,
+ // or if this page is larger than the previous, disable tiling
+ if (mPages.get(mPages.size() - 1).items.size() != mPageSize
+ || count > mPageSize) {
+ mPageSize = -1;
+ }
+ }
+
+ mPages.add(page);
+ mStorageCount += count;
+
+ final int changedCount = Math.min(mTrailingNullCount, count);
+ final int addedCount = count - changedCount;
+
+ if (changedCount != 0) {
+ mTrailingNullCount -= changedCount;
+ }
+ mNumberAppended += count;
+ callback.onPageAppended(mLeadingNullCount + mStorageCount - count,
+ changedCount, addedCount);
+ }
+
+ // ------------------ Non-Contiguous API (tiling required) ----------------------
+
+ public void insertPage(int position, @NonNull Page<K, V> page, Callback callback) {
+ final int newPageSize = page.items.size();
+ if (newPageSize != mPageSize) {
+ // differing page size is OK in 2 cases, when the page is being added:
+ // 1) to the end (in which case, ignore new smaller size)
+ // 2) only the last page has been added so far (in which case, adopt new bigger size)
+
+ int size = size();
+ boolean addingLastPage = position == (size - size % mPageSize)
+ && newPageSize < mPageSize;
+ boolean onlyEndPagePresent = mTrailingNullCount == 0 && mPages.size() == 1
+ && newPageSize > mPageSize;
+
+ // OK only if existing single page, and it's the last one
+ if (!onlyEndPagePresent && !addingLastPage) {
+ throw new IllegalArgumentException("page introduces incorrect tiling");
+ }
+ if (onlyEndPagePresent) {
+ mPageSize = newPageSize;
+ }
+ }
+
+ int pageIndex = position / mPageSize;
+
+ allocatePageRange(pageIndex, pageIndex);
+
+ int localPageIndex = pageIndex - mLeadingNullCount / mPageSize;
+
+ Page<K, V> oldPage = mPages.get(localPageIndex);
+ if (oldPage != null && oldPage != mPlaceholderPage) {
+ throw new IllegalArgumentException(
+ "Invalid position " + position + ": data already loaded");
+ }
+ mPages.set(localPageIndex, page);
+ callback.onPageInserted(position, page.items.size());
+ }
+
+ private Page<K, V> getPlaceholderPage() {
+ if (mPlaceholderPage == null) {
+ @SuppressWarnings("unchecked")
+ List<V> list = Collections.emptyList();
+ mPlaceholderPage = new Page<>(null, list, null);
+ }
+ return mPlaceholderPage;
+ }
+
+ private void allocatePageRange(final int minimumPage, final int maximumPage) {
+ int leadingNullPages = mLeadingNullCount / mPageSize;
+
+ if (minimumPage < leadingNullPages) {
+ for (int i = 0; i < leadingNullPages - minimumPage; i++) {
+ mPages.add(0, null);
+ }
+ int newStorageAllocated = (leadingNullPages - minimumPage) * mPageSize;
+ mStorageCount += newStorageAllocated;
+ mLeadingNullCount -= newStorageAllocated;
+
+ leadingNullPages = minimumPage;
+ }
+ if (maximumPage >= leadingNullPages + mPages.size()) {
+ int newStorageAllocated = Math.min(mTrailingNullCount,
+ (maximumPage + 1 - (leadingNullPages + mPages.size())) * mPageSize);
+ for (int i = mPages.size(); i <= maximumPage - leadingNullPages; i++) {
+ mPages.add(mPages.size(), null);
+ }
+ mStorageCount += newStorageAllocated;
+ mTrailingNullCount -= newStorageAllocated;
+ }
+ }
+
+ public void allocatePlaceholders(int index, int prefetchDistance,
+ int pageSize, Callback callback) {
+ if (pageSize != mPageSize) {
+ if (pageSize < mPageSize) {
+ throw new IllegalArgumentException("Page size cannot be reduced");
+ }
+ if (mPages.size() != 1 || mTrailingNullCount != 0) {
+ // not in single, last page allocated case - can't change page size
+ throw new IllegalArgumentException(
+ "Page size can change only if last page is only one present");
+ }
+ mPageSize = pageSize;
+ }
+
+ final int maxPageCount = (size() + mPageSize - 1) / mPageSize;
+ int minimumPage = Math.max((index - prefetchDistance) / mPageSize, 0);
+ int maximumPage = Math.min((index + prefetchDistance) / mPageSize, maxPageCount - 1);
+
+ allocatePageRange(minimumPage, maximumPage);
+ int leadingNullPages = mLeadingNullCount / mPageSize;
+ for (int pageIndex = minimumPage; pageIndex <= maximumPage; pageIndex++) {
+ int localPageIndex = pageIndex - leadingNullPages;
+ if (mPages.get(localPageIndex) == null) {
+ mPages.set(localPageIndex, getPlaceholderPage());
+ callback.onPagePlaceholderInserted(pageIndex);
+ }
+ }
+ }
+
+ public boolean hasPage(int pageSize, int index) {
+ // NOTE: we pass pageSize here to avoid in case mPageSize
+ // not fully initialized (when last page only one loaded)
+ int leadingNullPages = mLeadingNullCount / pageSize;
+
+ if (index < leadingNullPages || index >= leadingNullPages + mPages.size()) {
+ return false;
+ }
+
+ Page<K, V> page = mPages.get(index - leadingNullPages);
+
+ return page != null && page != mPlaceholderPage;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder ret = new StringBuilder("leading " + mLeadingNullCount
+ + ", storage " + mStorageCount
+ + ", trailing " + getTrailingNullCount());
+
+ for (int i = 0; i < mPages.size(); i++) {
+ ret.append(" ").append(mPages.get(i));
+ }
+ return ret.toString();
+ }
+}
diff --git a/paging/common/src/main/java/android/arch/paging/PositionalDataSource.java b/paging/common/src/main/java/android/arch/paging/PositionalDataSource.java
index deb51e9..c538cb6 100644
--- a/paging/common/src/main/java/android/arch/paging/PositionalDataSource.java
+++ b/paging/common/src/main/java/android/arch/paging/PositionalDataSource.java
@@ -42,6 +42,17 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public abstract class PositionalDataSource<Value> extends ContiguousDataSource<Integer, Value> {
+
+ /**
+ * Number of items that this DataSource can provide in total, or COUNT_UNDEFINED.
+ *
+ * @return number of items that this DataSource can provide in total, or COUNT_UNDEFINED
+ * if difficult or undesired to compute.
+ */
+ public int countItems() {
+ return COUNT_UNDEFINED;
+ }
+
@Nullable
@Override
List<Value> loadAfterImpl(int currentEndIndex, @NonNull Value currentEndItem, int pageSize) {
@@ -55,16 +66,7 @@
return loadBefore(currentBeginIndex - 1, pageSize);
}
-
- /**
- * Load initial data, starting after the passed position.
- *
- * @param position Index just before the data to be loaded.
- * @param initialLoadSize Suggested number of items to load.
- * @return List of initial items, representing data starting at position. Null if the
- * DataSource is no longer valid, and should not be queried again.
- * @hide
- */
+ /** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@WorkerThread
@Nullable
@@ -118,6 +120,9 @@
@Override
Integer getKey(int position, Value item) {
+ if (position < 0) {
+ return null;
+ }
return position;
}
}
diff --git a/paging/common/src/main/java/android/arch/paging/SnapshotPagedList.java b/paging/common/src/main/java/android/arch/paging/SnapshotPagedList.java
new file mode 100644
index 0000000..7e965a0
--- /dev/null
+++ b/paging/common/src/main/java/android/arch/paging/SnapshotPagedList.java
@@ -0,0 +1,64 @@
+/*
+ * 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.NonNull;
+import android.support.annotation.Nullable;
+
+class SnapshotPagedList<T> extends PagedList<T> {
+ private final boolean mContiguous;
+ private final Object mLastKey;
+
+ SnapshotPagedList(@NonNull PagedList<T> pagedList) {
+ super(pagedList.mStorage.snapshot(),
+ pagedList.mMainThreadExecutor,
+ pagedList.mBackgroundThreadExecutor,
+ pagedList.mConfig);
+ mContiguous = pagedList.isContiguous();
+ mLastKey = pagedList.getLastKey();
+ }
+
+ @Override
+ public boolean isImmutable() {
+ return true;
+ }
+
+ @Override
+ public boolean isDetached() {
+ return true;
+ }
+
+ @Override
+ boolean isContiguous() {
+ return mContiguous;
+ }
+
+ @Nullable
+ @Override
+ public Object getLastKey() {
+ return mLastKey;
+ }
+
+ @Override
+ void dispatchUpdatesSinceSnapshot(@NonNull PagedList<T> storageSnapshot,
+ @NonNull Callback callback) {
+ }
+
+ @Override
+ void loadAroundInternal(int index) {
+ }
+}
diff --git a/paging/common/src/main/java/android/arch/paging/TiledDataSource.java b/paging/common/src/main/java/android/arch/paging/TiledDataSource.java
index 36be423..61dead3 100644
--- a/paging/common/src/main/java/android/arch/paging/TiledDataSource.java
+++ b/paging/common/src/main/java/android/arch/paging/TiledDataSource.java
@@ -19,6 +19,7 @@
import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread;
+import java.util.Collections;
import java.util.List;
/**
@@ -92,7 +93,6 @@
* @return Number of items this DataSource can provide. Must be <code>0</code> or greater.
*/
@WorkerThread
- @Override
public abstract int countItems();
@Override
@@ -118,7 +118,61 @@
@WorkerThread
public abstract List<Type> loadRange(int startPosition, int count);
- final List<Type> loadRangeWrapper(int startPosition, int count) {
+ /**
+ * blocking, and splits pages
+ */
+ void loadRangeInitial(int startPosition, int count, int pageSize, int itemCount,
+ PageResult.Receiver<Integer, Type> receiver) {
+
+ if (itemCount == 0) {
+ // no data to load, just immediately return empty
+ receiver.onPageResult(new PageResult<>(
+ PageResult.INIT, new Page<Integer, Type>(Collections.<Type>emptyList()),
+ 0, 0, startPosition));
+ return;
+ }
+
+
+ List<Type> list = loadRangeWrapper(startPosition, count);
+
+ count = Math.min(count, itemCount - startPosition);
+
+ if (list == null) {
+ // invalid data, pass to receiver
+ receiver.onPageResult(new PageResult<Integer, Type>(
+ PageResult.INIT, null, 0, 0, startPosition));
+ return;
+ }
+
+ if (list.size() != count) {
+ throw new IllegalStateException("Invalid list, requested size: " + count
+ + ", returned size: " + list.size());
+ }
+
+ // emit the results as multiple pages
+ int pageCount = (count + (pageSize - 1)) / pageSize;
+ for (int i = 0; i < pageCount; i++) {
+ int beginInclusive = i * pageSize;
+ int endExclusive = Math.min(count, (i + 1) * pageSize);
+
+ Page<Integer, Type> page = new Page<>(list.subList(beginInclusive, endExclusive));
+
+ int leadingNulls = startPosition + beginInclusive;
+ int trailingNulls = itemCount - leadingNulls - page.items.size();
+ receiver.onPageResult(new PageResult<>(
+ PageResult.INIT, page, leadingNulls, trailingNulls, 0));
+ }
+ }
+
+ void loadRange(int startPosition, int count, PageResult.Receiver<Integer, Type> receiver) {
+ List<Type> list = loadRangeWrapper(startPosition, count);
+
+ Page<Integer, Type> page = list != null ? new Page<Integer, Type>(list) : null;
+ receiver.postOnPageResult(new PageResult<>(
+ PageResult.TILE, page, startPosition, 0, 0));
+ }
+
+ private List<Type> loadRangeWrapper(int startPosition, int count) {
if (isInvalid()) {
return null;
}
diff --git a/paging/common/src/main/java/android/arch/paging/TiledPagedList.java b/paging/common/src/main/java/android/arch/paging/TiledPagedList.java
index a2fc064..c45d029 100644
--- a/paging/common/src/main/java/android/arch/paging/TiledPagedList.java
+++ b/paging/common/src/main/java/android/arch/paging/TiledPagedList.java
@@ -16,214 +16,100 @@
package android.arch.paging;
+import android.support.annotation.AnyThread;
+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.AbstractList;
-import java.util.ArrayList;
-import java.util.List;
import java.util.concurrent.Executor;
-import java.util.concurrent.atomic.AtomicBoolean;
-/** @hide */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-class TiledPagedList<T> extends PageArrayList<T> {
+class TiledPagedList<T> extends PagedList<T>
+ implements PagedStorage.Callback {
private final TiledDataSource<T> mDataSource;
- private final Executor mMainThreadExecutor;
- private final Executor mBackgroundThreadExecutor;
- private final Config mConfig;
- @SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
- private final List<T> mLoadingPlaceholder = new AbstractList<T>() {
+ @SuppressWarnings("unchecked")
+ private final PagedStorage<Integer, T> mKeyedStorage = (PagedStorage<Integer, T>) mStorage;
+
+ private final PageResult.Receiver<Integer, T> mReceiver =
+ new PageResult.Receiver<Integer, T>() {
+ @AnyThread
@Override
- public T get(int i) {
- return null;
+ public void postOnPageResult(@NonNull final PageResult<Integer, T> pageResult) {
+ // NOTE: if we're already on main thread, this can delay page receive by a frame
+ mMainThreadExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ onPageResult(pageResult);
+ }
+ });
}
+ @MainThread
@Override
- public int size() {
- return 0;
+ public void onPageResult(@NonNull PageResult<Integer, T> pageResult) {
+ if (pageResult.page == null) {
+ detach();
+ return;
+ }
+
+ if (isDetached()) {
+ // No op, have detached
+ return;
+ }
+
+ if (mStorage.getPageCount() == 0) {
+ mKeyedStorage.init(
+ pageResult.leadingNulls, pageResult.page, pageResult.trailingNulls,
+ pageResult.positionOffset, TiledPagedList.this);
+ } else {
+ mKeyedStorage.insertPage(pageResult.leadingNulls, pageResult.page,
+ TiledPagedList.this);
+ }
}
};
- private int mLastLoad = -1;
-
- private AtomicBoolean mDetached = new AtomicBoolean(false);
-
- private ArrayList<WeakReference<Callback>> mCallbacks = new ArrayList<>();
-
@WorkerThread
TiledPagedList(@NonNull TiledDataSource<T> dataSource,
@NonNull Executor mainThreadExecutor,
@NonNull Executor backgroundThreadExecutor,
- Config config,
+ @NonNull Config config,
int position) {
- super(config.mPageSize, dataSource.countItems());
-
+ super(new PagedStorage<Integer, T>(),
+ mainThreadExecutor, backgroundThreadExecutor, config);
mDataSource = dataSource;
- mMainThreadExecutor = mainThreadExecutor;
- mBackgroundThreadExecutor = backgroundThreadExecutor;
- mConfig = config;
- position = Math.min(Math.max(0, position), mCount);
+ final int pageSize = mConfig.mPageSize;
- int firstPage = position / mPageSize;
- List<T> firstPageData = dataSource.loadRangeWrapper(firstPage * mPageSize, mPageSize);
- if (firstPageData != null) {
- mPageIndexOffset = firstPage;
- mPages.add(firstPageData);
- mLastLoad = position;
- } else {
- detach();
- return;
- }
+ final int itemCount = mDataSource.countItems();
+ final int firstLoadSize = Math.min(itemCount,
+ (Math.max(mConfig.mInitialLoadSizeHint / pageSize, 2)) * pageSize);
+ final int firstLoadPosition = computeFirstLoadPosition(
+ position, firstLoadSize, pageSize, itemCount);
- int secondPage = (position % mPageSize < mPageSize / 2) ? firstPage - 1 : firstPage + 1;
- if (secondPage < 0 || secondPage > mMaxPageCount) {
- // no second page to load
- return;
- }
- List<T> secondPageData = dataSource.loadRangeWrapper(secondPage * mPageSize, mPageSize);
- if (secondPageData != null) {
- boolean before = secondPage < firstPage;
- mPages.add(before ? 0 : 1, secondPageData);
- if (before) {
- mPageIndexOffset--;
- }
- return;
- }
- detach();
+ mDataSource.loadRangeInitial(firstLoadPosition, firstLoadSize, pageSize,
+ itemCount, mReceiver);
+ }
+
+ static int computeFirstLoadPosition(int position, int firstLoadSize, int pageSize, int size) {
+ int idealStart = position - firstLoadSize / 2;
+
+ int roundedPageStart = Math.round(idealStart / pageSize) * pageSize;
+
+ // minimum start position is 0
+ roundedPageStart = Math.max(0, roundedPageStart);
+
+ // maximum start pos is that which will encompass end of list
+ int maximumLoadPage = ((size - firstLoadSize + pageSize - 1) / pageSize) * pageSize;
+ roundedPageStart = Math.min(maximumLoadPage, roundedPageStart);
+
+ return roundedPageStart;
}
@Override
- public void loadAround(int index) {
- mLastLoad = index;
-
- int minimumPage = Math.max((index - mConfig.mPrefetchDistance) / mPageSize, 0);
- int maximumPage = Math.min((index + mConfig.mPrefetchDistance) / mPageSize,
- mMaxPageCount - 1);
-
- if (minimumPage < mPageIndexOffset) {
- for (int i = 0; i < mPageIndexOffset - minimumPage; i++) {
- mPages.add(0, null);
- }
- mPageIndexOffset = minimumPage;
- }
- if (maximumPage >= mPageIndexOffset + mPages.size()) {
- for (int i = mPages.size(); i <= maximumPage - mPageIndexOffset; i++) {
- mPages.add(mPages.size(), null);
- }
- }
- for (int i = minimumPage; i <= maximumPage; i++) {
- scheduleLoadPage(i);
- }
- }
-
- private void scheduleLoadPage(final int pageIndex) {
- final int localPageIndex = pageIndex - mPageIndexOffset;
-
- if (mPages.get(localPageIndex) != null) {
- // page is present in list, and non-null - don't need to load
- return;
- }
- mPages.set(localPageIndex, mLoadingPlaceholder);
-
- mBackgroundThreadExecutor.execute(new Runnable() {
- @Override
- public void run() {
- if (mDetached.get()) {
- return;
- }
- final List<T> data = mDataSource.loadRangeWrapper(
- pageIndex * mPageSize, mPageSize);
- if (data != null) {
- mMainThreadExecutor.execute(new Runnable() {
- @Override
- public void run() {
- if (mDetached.get()) {
- return;
- }
- loadPageImpl(pageIndex, data);
- }
- });
- } else {
- detach();
- }
- }
- });
-
- }
-
- private void loadPageImpl(int pageIndex, List<T> data) {
- int localPageIndex = pageIndex - mPageIndexOffset;
-
- if (mPages.get(localPageIndex) != mLoadingPlaceholder) {
- throw new IllegalStateException("Data inserted before requested.");
- }
- mPages.set(localPageIndex, data);
- for (WeakReference<Callback> weakRef : mCallbacks) {
- Callback callback = weakRef.get();
- if (callback != null) {
- callback.onChanged(pageIndex * mPageSize, data.size());
- }
- }
- }
-
- @Override
- public boolean isImmutable() {
- // TODO: consider counting loaded pages, return true if mLoadedPages == mMaxPageCount
- // Note: could at some point want to support growing past max count, or grow dynamically
- return isDetached();
- }
-
- @Override
- public void addWeakCallback(@Nullable PagedList<T> previousSnapshot,
- @NonNull Callback callback) {
- PageArrayList<T> snapshot = (PageArrayList<T>) previousSnapshot;
- if (snapshot != this && snapshot != null) {
- // loop through each page and signal the callback for any pages that are present now,
- // but not in the snapshot.
- for (int i = 0; i < mPages.size(); i++) {
- int pageIndex = i + mPageIndexOffset;
- int pageCount = 0;
- // count number of consecutive pages that were added since the snapshot...
- while (pageCount < mPages.size()
- && hasPage(pageIndex + pageCount)
- && !snapshot.hasPage(pageIndex + pageCount)) {
- pageCount++;
- }
- // and signal them all at once to the callback
- if (pageCount > 0) {
- callback.onChanged(pageIndex * mPageSize, mPageSize * pageCount);
- i += pageCount - 1;
- }
- }
- }
- 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();
- }
-
- @Override
- public void detach() {
- mDetached.set(true);
+ boolean isContiguous() {
+ return false;
}
@Nullable
@@ -231,4 +117,72 @@
public Object getLastKey() {
return mLastLoad;
}
+
+ @Override
+ protected void dispatchUpdatesSinceSnapshot(@NonNull PagedList<T> pagedListSnapshot,
+ @NonNull Callback callback) {
+ //noinspection UnnecessaryLocalVariable
+ final PagedStorage<?, T> snapshot = pagedListSnapshot.mStorage;
+
+ // loop through each page and signal the callback for any pages that are present now,
+ // but not in the snapshot.
+ final int pageSize = mConfig.mPageSize;
+ final int leadingNullPages = mStorage.getLeadingNullCount() / pageSize;
+ final int pageCount = mStorage.getPageCount();
+ for (int i = 0; i < pageCount; i++) {
+ int pageIndex = i + leadingNullPages;
+ int updatedPages = 0;
+ // count number of consecutive pages that were added since the snapshot...
+ while (updatedPages < mStorage.getPageCount()
+ && mStorage.hasPage(pageSize, pageIndex + updatedPages)
+ && !snapshot.hasPage(pageSize, pageIndex + updatedPages)) {
+ updatedPages++;
+ }
+ // and signal them all at once to the callback
+ if (updatedPages > 0) {
+ callback.onChanged(pageIndex * pageSize, pageSize * updatedPages);
+ i += updatedPages - 1;
+ }
+ }
+ }
+
+ @Override
+ protected void loadAroundInternal(int index) {
+ mStorage.allocatePlaceholders(index, mConfig.mPrefetchDistance, mConfig.mPageSize, this);
+ }
+
+ @Override
+ public void onInitialized(int count) {
+ notifyInserted(0, count);
+ }
+
+ @Override
+ public void onPagePrepended(int leadingNulls, int changed, int added) {
+ throw new IllegalStateException("Contiguous callback on TiledPagedList");
+ }
+
+ @Override
+ public void onPageAppended(int endPosition, int changed, int added) {
+ throw new IllegalStateException("Contiguous callback on TiledPagedList");
+ }
+
+ @Override
+ public void onPagePlaceholderInserted(final int pageIndex) {
+ // placeholder means initialize a load
+ mBackgroundThreadExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ if (isDetached()) {
+ return;
+ }
+ final int pageSize = mConfig.mPageSize;
+ mDataSource.loadRange(pageIndex * pageSize, pageSize, mReceiver);
+ }
+ });
+ }
+
+ @Override
+ public void onPageInserted(int start, int count) {
+ notifyChanged(start, count);
+ }
}
diff --git a/paging/common/src/test/java/android/arch/paging/ContiguousPagedListTest.java b/paging/common/src/test/java/android/arch/paging/ContiguousPagedListTest.java
index ee7ea6a..43f556a 100644
--- a/paging/common/src/test/java/android/arch/paging/ContiguousPagedListTest.java
+++ b/paging/common/src/test/java/android/arch/paging/ContiguousPagedListTest.java
@@ -16,6 +16,7 @@
package android.arch.paging;
+import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.mock;
@@ -109,6 +110,7 @@
private void verifyRange(int start, int count, NullPaddedList<Item> actual) {
if (mCounted) {
+ //noinspection UnnecessaryLocalVariable
int expectedLeading = start;
int expectedTrailing = ITEMS.size() - start - count;
assertEquals(ITEMS.size(), actual.size());
@@ -132,6 +134,38 @@
}
}
+ @SuppressWarnings("SuspiciousSystemArraycopy")
+ private void verifyRange(int start, int count, PagedStorage<?, Item> actual) {
+ if (mCounted) {
+ Item[] expected = new Item[ITEMS.size()];
+ System.arraycopy(ITEMS.toArray(), start, expected, start, count);
+ assertArrayEquals(expected, actual.toArray());
+
+ //noinspection UnnecessaryLocalVariable
+ int expectedLeading = start;
+ int expectedTrailing = ITEMS.size() - start - count;
+ assertEquals(ITEMS.size(), actual.size());
+ assertEquals(ITEMS.size() - expectedLeading - expectedTrailing,
+ actual.getStorageCount());
+ assertEquals(expectedLeading, actual.getLeadingNullCount());
+ assertEquals(expectedTrailing, actual.getTrailingNullCount());
+
+ } else {
+ Item[] expected = new Item[count];
+ System.arraycopy(ITEMS.toArray(), start, expected, 0, count);
+ assertArrayEquals(expected, actual.toArray());
+
+ assertEquals(count, actual.size());
+ assertEquals(actual.size(), actual.getStorageCount());
+ assertEquals(0, actual.getLeadingNullCount());
+ assertEquals(0, actual.getTrailingNullCount());
+ }
+ }
+
+ private void verifyRange(int start, int count, PagedList<Item> actual) {
+ verifyRange(start, count, actual.mStorage);
+ }
+
private void verifyCallback(PagedList.Callback callback, int countedPosition,
int uncountedPosition) {
if (mCounted) {
@@ -154,7 +188,7 @@
}
- private ContiguousPagedList<Item> createCountedPagedList(
+ private ContiguousPagedList<Integer, Item> createCountedPagedList(
PagedList.Config config, int initialPosition) {
TestSource source = new TestSource();
return new ContiguousPagedList<>(
@@ -163,7 +197,7 @@
initialPosition);
}
- private ContiguousPagedList<Item> createCountedPagedList(int initialPosition) {
+ private ContiguousPagedList<Integer, Item> createCountedPagedList(int initialPosition) {
return createCountedPagedList(
new PagedList.Config.Builder()
.setInitialLoadSizeHint(40)
@@ -174,8 +208,14 @@
}
@Test
+ public void construct() {
+ ContiguousPagedList<Integer, Item> pagedList = createCountedPagedList(0);
+ verifyRange(0, 40, pagedList);
+ }
+
+ @Test
public void append() {
- ContiguousPagedList<Item> pagedList = createCountedPagedList(0);
+ ContiguousPagedList<Integer, Item> pagedList = createCountedPagedList(0);
PagedList.Callback callback = mock(PagedList.Callback.class);
pagedList.addWeakCallback(null, callback);
verifyRange(0, 40, pagedList);
@@ -192,7 +232,7 @@
@Test
public void prepend() {
- ContiguousPagedList<Item> pagedList = createCountedPagedList(80);
+ ContiguousPagedList<Integer, Item> pagedList = createCountedPagedList(80);
PagedList.Callback callback = mock(PagedList.Callback.class);
pagedList.addWeakCallback(null, callback);
verifyRange(60, 40, pagedList);
@@ -208,7 +248,7 @@
@Test
public void outwards() {
- ContiguousPagedList<Item> pagedList = createCountedPagedList(50);
+ ContiguousPagedList<Integer, Item> pagedList = createCountedPagedList(50);
PagedList.Callback callback = mock(PagedList.Callback.class);
pagedList.addWeakCallback(null, callback);
verifyRange(30, 40, pagedList);
@@ -231,7 +271,7 @@
@Test
public void multiAppend() {
- ContiguousPagedList<Item> pagedList = createCountedPagedList(0);
+ ContiguousPagedList<Integer, Item> pagedList = createCountedPagedList(0);
PagedList.Callback callback = mock(PagedList.Callback.class);
pagedList.addWeakCallback(null, callback);
verifyRange(0, 40, pagedList);
@@ -248,7 +288,7 @@
@Test
public void distantPrefetch() {
- ContiguousPagedList<Item> pagedList = createCountedPagedList(
+ ContiguousPagedList<Integer, Item> pagedList = createCountedPagedList(
new PagedList.Config.Builder()
.setInitialLoadSizeHint(10)
.setPageSize(10)
@@ -274,7 +314,7 @@
@Test
public void appendCallbackAddedLate() {
- ContiguousPagedList<Item> pagedList = createCountedPagedList(0);
+ ContiguousPagedList<Integer, Item> pagedList = createCountedPagedList(0);
verifyRange(0, 40, pagedList);
pagedList.loadAround(35);
@@ -282,7 +322,7 @@
verifyRange(0, 60, pagedList);
// snapshot at 60 items
- NullPaddedList<Item> snapshot = (NullPaddedList<Item>) pagedList.snapshot();
+ PagedList<Item> snapshot = (PagedList<Item>) pagedList.snapshot();
verifyRange(0, 60, snapshot);
@@ -300,7 +340,7 @@
@Test
public void prependCallbackAddedLate() {
- ContiguousPagedList<Item> pagedList = createCountedPagedList(80);
+ ContiguousPagedList<Integer, Item> pagedList = createCountedPagedList(80);
verifyRange(60, 40, pagedList);
pagedList.loadAround(mCounted ? 65 : 5);
@@ -308,7 +348,7 @@
verifyRange(40, 60, pagedList);
// snapshot at 60 items
- NullPaddedList<Item> snapshot = (NullPaddedList<Item>) pagedList.snapshot();
+ PagedList<Item> snapshot = (PagedList<Item>) pagedList.snapshot();
verifyRange(40, 60, snapshot);
diff --git a/paging/common/src/test/java/android/arch/paging/KeyedDataSourceTest.kt b/paging/common/src/test/java/android/arch/paging/KeyedDataSourceTest.kt
index 92ab3c5..0625694 100644
--- a/paging/common/src/test/java/android/arch/paging/KeyedDataSourceTest.kt
+++ b/paging/common/src/test/java/android/arch/paging/KeyedDataSourceTest.kt
@@ -1,3 +1,19 @@
+/*
+ * 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 org.junit.Assert.assertEquals
@@ -5,6 +21,8 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito
@RunWith(JUnit4::class)
class KeyedDataSourceTest {
@@ -156,6 +174,29 @@
assertEquals(0, initialLoad.trailingNullCount)
}
+ // ----- Other behavior -----
+
+ @Test
+ fun loadBefore() {
+ val dataSource = ItemDataSource()
+ @Suppress("UNCHECKED_CAST")
+ val receiver = Mockito.mock(PageResult.Receiver::class.java)
+ as PageResult.Receiver<Key, Item>
+
+ dataSource.loadBefore(5, ITEMS_BY_NAME_ID[5], 5, receiver)
+
+ @Suppress("UNCHECKED_CAST")
+ val argument = ArgumentCaptor.forClass(PageResult::class.java)
+ as ArgumentCaptor<PageResult<Key, Item>>
+ Mockito.verify(receiver).postOnPageResult(argument.capture())
+ Mockito.verifyNoMoreInteractions(receiver)
+
+ val observed = argument.value
+
+ assertEquals(PageResult.PREPEND, observed.type)
+ assertEquals(ITEMS_BY_NAME_ID.subList(0, 5), observed.page.items)
+ }
+
internal data class Key(val name: String, val id: Int)
internal data class Item(
diff --git a/paging/common/src/test/java/android/arch/paging/PageArrayListTest.java b/paging/common/src/test/java/android/arch/paging/PageArrayListTest.java
deleted file mode 100644
index 135e640..0000000
--- a/paging/common/src/test/java/android/arch/paging/PageArrayListTest.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * 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 static org.junit.Assert.assertEquals;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.util.Arrays;
-import java.util.List;
-
-@RunWith(JUnit4.class)
-public class PageArrayListTest {
- @Test
- public void simple() {
- List<String> data = Arrays.asList("A", "B", "C", "D", "E", "F");
- PageArrayList<String> list = new PageArrayList<>(2, data.size());
-
- assertEquals(2, list.mPageSize);
- assertEquals(data.size(), list.size());
- assertEquals(3, list.mMaxPageCount);
-
- for (int i = 0; i < data.size(); i++) {
- assertEquals(null, list.get(i));
- }
- for (int i = 0; i < data.size(); i += list.mPageSize) {
- list.mPages.add(data.subList(i, i + 2));
- }
- for (int i = 0; i < data.size(); i++) {
- assertEquals(data.get(i), list.get(i));
- }
- }
-}
diff --git a/paging/common/src/test/java/android/arch/paging/PagedStorageTest.kt b/paging/common/src/test/java/android/arch/paging/PagedStorageTest.kt
new file mode 100644
index 0000000..92b6c87
--- /dev/null
+++ b/paging/common/src/test/java/android/arch/paging/PagedStorageTest.kt
@@ -0,0 +1,409 @@
+/*
+ * 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 org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+
+@RunWith(JUnit4::class)
+class PagedStorageTest {
+ private fun createPage(vararg strings: String): Page<Int, String> {
+ return Page(strings.asList())
+ }
+
+ @Test
+ fun construct() {
+ val storage = PagedStorage(2, createPage("a", "b"), 2)
+
+ assertArrayEquals(arrayOf(null, null, "a", "b", null, null), storage.toArray())
+ assertEquals(6, storage.size)
+ }
+
+ @Test
+ fun appendFill() {
+ val callback = mock(PagedStorage.Callback::class.java)
+
+ val storage = PagedStorage(2, createPage("a", "b"), 2)
+ storage.appendPage(createPage("c", "d"), callback)
+
+
+ assertArrayEquals(arrayOf(null, null, "a", "b", "c", "d"), storage.toArray())
+ verify(callback).onPageAppended(4, 2, 0)
+ verifyNoMoreInteractions(callback)
+ }
+
+ @Test
+ fun appendAdd() {
+ val callback = mock(PagedStorage.Callback::class.java)
+
+ val storage = PagedStorage(2, createPage("a", "b"), 0)
+ storage.appendPage(createPage("c", "d"), callback)
+
+ assertArrayEquals(arrayOf(null, null, "a", "b", "c", "d"), storage.toArray())
+ verify(callback).onPageAppended(4, 0, 2)
+ verifyNoMoreInteractions(callback)
+ }
+
+ @Test
+ fun appendFillAdd() {
+ val callback = mock(PagedStorage.Callback::class.java)
+
+ val storage = PagedStorage(2, createPage("a", "b"), 2)
+
+ // change 2 nulls into c, d
+ storage.appendPage(createPage("c", "d"), callback)
+
+ assertArrayEquals(arrayOf(null, null, "a", "b", "c", "d"), storage.toArray())
+ verify(callback).onPageAppended(4, 2, 0)
+ verifyNoMoreInteractions(callback)
+
+ // append e, f
+ storage.appendPage(createPage("e", "f"), callback)
+
+ assertArrayEquals(arrayOf(null, null, "a", "b", "c", "d", "e", "f"), storage.toArray())
+ verify(callback).onPageAppended(6, 0, 2)
+ verifyNoMoreInteractions(callback)
+ }
+
+ @Test
+ fun prependFill() {
+ val callback = mock(PagedStorage.Callback::class.java)
+
+ val storage = PagedStorage(2, createPage("c", "d"), 2)
+ storage.prependPage(createPage("a", "b"), callback)
+
+ assertArrayEquals(arrayOf("a", "b", "c", "d", null, null), storage.toArray())
+ verify(callback).onPagePrepended(0, 2, 0)
+ verifyNoMoreInteractions(callback)
+ }
+
+ @Test
+ fun prependAdd() {
+ val callback = mock(PagedStorage.Callback::class.java)
+
+ val storage = PagedStorage(0, createPage("c", "d"), 2)
+ storage.prependPage(createPage("a", "b"), callback)
+
+
+ assertArrayEquals(arrayOf("a", "b", "c", "d", null, null), storage.toArray())
+ verify(callback).onPagePrepended(0, 0, 2)
+ verifyNoMoreInteractions(callback)
+ }
+
+ @Test
+ fun prependFillAdd() {
+ val callback = mock(PagedStorage.Callback::class.java)
+
+ val storage = PagedStorage(2, createPage("e", "f"), 2)
+
+ // change 2 nulls into c, d
+ storage.prependPage(createPage("c", "d"), callback)
+
+ assertArrayEquals(arrayOf("c", "d", "e", "f", null, null), storage.toArray())
+ verify(callback).onPagePrepended(0, 2, 0)
+ verifyNoMoreInteractions(callback)
+
+ // prepend a, b
+ storage.prependPage(createPage("a", "b"), callback)
+
+ assertArrayEquals(arrayOf("a", "b", "c", "d", "e", "f", null, null), storage.toArray())
+ verify(callback).onPagePrepended(0, 0, 2)
+ verifyNoMoreInteractions(callback)
+ }
+
+ @Test
+ fun isTiled_addend_smallerPageIsNotLast() {
+ val callback = mock(PagedStorage.Callback::class.java)
+ val storage = PagedStorage(0, createPage("a", "a"), 0)
+ assertTrue(storage.isTiled)
+
+ storage.appendPage(createPage("a", "a"), callback)
+ assertTrue(storage.isTiled)
+
+ storage.appendPage(createPage("a"), callback)
+ assertTrue(storage.isTiled)
+
+ // no matter what we append here, we're no longer tiled
+ storage.appendPage(createPage("a", "a"), callback)
+ assertFalse(storage.isTiled)
+ }
+
+ @Test
+ fun isTiled_append_growingSizeDisable() {
+ val callback = mock(PagedStorage.Callback::class.java)
+ val storage = PagedStorage(0, createPage("a", "a"), 0)
+ assertTrue(storage.isTiled)
+
+ // page size can't grow from append
+ storage.appendPage(createPage("a", "a", "a"), callback)
+ assertFalse(storage.isTiled)
+ }
+
+ @Test
+ fun isTiled_prepend_smallerPage() {
+ val callback = mock(PagedStorage.Callback::class.java)
+ val storage = PagedStorage(0, createPage("a"), 0)
+ assertTrue(storage.isTiled)
+
+ storage.prependPage(createPage("a", "a"), callback)
+ assertTrue(storage.isTiled)
+
+ storage.prependPage(createPage("a", "a"), callback)
+ assertTrue(storage.isTiled)
+
+ storage.prependPage(createPage("a"), callback)
+ assertFalse(storage.isTiled)
+ }
+
+ @Test
+ fun isTiled_prepend_smallerThanInitialPage() {
+ val callback = mock(PagedStorage.Callback::class.java)
+ val storage = PagedStorage(0, createPage("a", "a"), 0)
+ assertTrue(storage.isTiled)
+
+ storage.prependPage(createPage("a"), callback)
+ assertFalse(storage.isTiled)
+ }
+
+ @Test
+ fun get_tiled() {
+ val callback = mock(PagedStorage.Callback::class.java)
+ val storage = PagedStorage(1, createPage("a", "b"), 5)
+ assertTrue(storage.isTiled)
+
+ storage.appendPage(createPage("c", "d"), callback)
+ storage.appendPage(createPage("e", "f"), callback)
+
+ assertTrue(storage.isTiled)
+ assertArrayEquals(arrayOf(null, "a", "b", "c", "d", "e", "f", null), storage.toArray())
+ }
+
+ @Test
+ fun get_nonTiled() {
+ val callback = mock(PagedStorage.Callback::class.java)
+ val storage = PagedStorage(1, createPage("a"), 6)
+ assertTrue(storage.isTiled)
+
+ storage.appendPage(createPage("b", "c"), callback)
+ storage.appendPage(createPage("d", "e", "f"), callback)
+
+ assertFalse(storage.isTiled)
+ assertArrayEquals(arrayOf(null, "a", "b", "c", "d", "e", "f", null), storage.toArray())
+ }
+
+ @Test
+ fun insertOne() {
+ val callback = mock(PagedStorage.Callback::class.java)
+ val storage = PagedStorage<Int, String>()
+
+ storage.init(2, createPage("c", "d"), 3, 0, callback)
+
+ assertEquals(7, storage.size)
+ assertArrayEquals(arrayOf(null, null, "c", "d", null, null, null), storage.toArray())
+ verify(callback).onInitialized(7)
+ verifyNoMoreInteractions(callback)
+
+ storage.insertPage(4, createPage("e", "f"), callback)
+
+ assertEquals(7, storage.size)
+ assertArrayEquals(arrayOf(null, null, "c", "d", "e", "f", null), storage.toArray())
+ verify(callback).onPageInserted(4, 2)
+ verifyNoMoreInteractions(callback)
+ }
+
+ @Test
+ fun insertThree() {
+ val callback = mock(PagedStorage.Callback::class.java)
+ val storage = PagedStorage<Int, String>()
+
+ storage.init(2, createPage("c", "d"), 3, 0, callback)
+
+ assertEquals(7, storage.size)
+ assertArrayEquals(arrayOf(null, null, "c", "d", null, null, null), storage.toArray())
+ verify(callback).onInitialized(7)
+ verifyNoMoreInteractions(callback)
+
+ // first, insert 1st page
+ storage.insertPage(0, createPage("a", "b"), callback)
+
+ assertEquals(7, storage.size)
+ assertArrayEquals(arrayOf("a", "b", "c", "d", null, null, null), storage.toArray())
+ verify(callback).onPageInserted(0, 2)
+ verifyNoMoreInteractions(callback)
+
+ // then 3rd page
+ storage.insertPage(4, createPage("e", "f"), callback)
+
+ assertEquals(7, storage.size)
+ assertArrayEquals(arrayOf("a", "b", "c", "d", "e", "f", null), storage.toArray())
+ verify(callback).onPageInserted(4, 2)
+ verifyNoMoreInteractions(callback)
+
+ // then last, small page
+ storage.insertPage(6, createPage("g"), callback)
+
+ assertEquals(7, storage.size)
+ assertArrayEquals(arrayOf("a", "b", "c", "d", "e", "f", "g"), storage.toArray())
+ verify(callback).onPageInserted(6, 1)
+ verifyNoMoreInteractions(callback)
+ }
+
+ @Test
+ fun insertLastFirst() {
+ val callback = mock(PagedStorage.Callback::class.java)
+ val storage = PagedStorage<Int, String>()
+
+ storage.init(6, createPage("g"), 0, 0, callback)
+
+ assertEquals(7, storage.size)
+ assertArrayEquals(arrayOf(null, null, null, null, null, null, "g"), storage.toArray())
+ verify(callback).onInitialized(7)
+ verifyNoMoreInteractions(callback)
+
+ // insert 1st page
+ storage.insertPage(0, createPage("a", "b"), callback)
+
+ assertEquals(7, storage.size)
+ assertArrayEquals(arrayOf("a", "b", null, null, null, null, "g"), storage.toArray())
+ verify(callback).onPageInserted(0, 2)
+ verifyNoMoreInteractions(callback)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun insertFailure_decreaseLast() {
+ val callback = mock(PagedStorage.Callback::class.java)
+ val storage = PagedStorage<Int, String>()
+
+ storage.init(2, createPage("c", "d"), 0, 0, callback)
+
+ // should throw, page too small
+ storage.insertPage(0, createPage("a"), callback)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun insertFailure_increase() {
+ val callback = mock(PagedStorage.Callback::class.java)
+ val storage = PagedStorage<Int, String>()
+
+ storage.init(0, createPage("a", "b"), 3, 0, callback)
+
+ // should throw, page too big
+ storage.insertPage(2, createPage("c", "d", "e"), callback)
+ }
+
+ @Test
+ fun allocatePlaceholders_simple() {
+ val callback = mock(PagedStorage.Callback::class.java)
+ val storage = PagedStorage<Int, String>()
+
+ storage.init(2, createPage("c"), 2, 0, callback)
+
+ verify(callback).onInitialized(5)
+
+ storage.allocatePlaceholders(2, 1, 1, callback)
+
+ verify(callback).onPagePlaceholderInserted(1)
+ verify(callback).onPagePlaceholderInserted(3)
+ verifyNoMoreInteractions(callback)
+
+ assertArrayEquals(arrayOf(null, null, "c", null, null), storage.toArray())
+ }
+
+ @Test
+ fun allocatePlaceholders_adoptPageSize() {
+ val callback = mock(PagedStorage.Callback::class.java)
+ val storage = PagedStorage<Int, String>()
+
+ storage.init(4, createPage("e"), 0, 0, callback)
+
+ verify(callback).onInitialized(5)
+
+ storage.allocatePlaceholders(0, 2, 2, callback)
+
+ verify(callback).onPagePlaceholderInserted(0)
+ verify(callback).onPagePlaceholderInserted(1)
+ verifyNoMoreInteractions(callback)
+
+ assertArrayEquals(arrayOf(null, null, null, null, "e"), storage.toArray())
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun allocatePlaceholders_cannotShrinkPageSize() {
+ val callback = mock(PagedStorage.Callback::class.java)
+ val storage = PagedStorage<Int, String>()
+
+ storage.init(4, createPage("e", "f"), 0, 0, callback)
+
+ verify(callback).onInitialized(6)
+
+ storage.allocatePlaceholders(0, 2, 1, callback)
+ }
+
+
+ @Test(expected = IllegalArgumentException::class)
+ fun allocatePlaceholders_cannotAdoptPageSize() {
+ val callback = mock(PagedStorage.Callback::class.java)
+ val storage = PagedStorage<Int, String>()
+
+ storage.init(2, createPage("c", "d"), 2, 0, callback)
+
+ verify(callback).onInitialized(6)
+
+ storage.allocatePlaceholders(0, 2, 3, callback)
+ }
+
+ @Test
+ fun get_placeholdersMulti() {
+ val callback = mock(PagedStorage.Callback::class.java)
+ val storage = PagedStorage<Int, String>()
+
+ storage.init(2, createPage("c", "d"), 3, 0, callback)
+
+ assertArrayEquals(arrayOf(null, null, "c", "d", null, null, null), storage.toArray())
+
+ storage.allocatePlaceholders(0, 10, 2, callback)
+
+ // allocating placeholders shouldn't affect result of get
+ assertArrayEquals(arrayOf(null, null, "c", "d", null, null, null), storage.toArray())
+ }
+
+ @Test
+ fun hasPage() {
+ val callback = mock(PagedStorage.Callback::class.java)
+ val storage = PagedStorage<Int, String>()
+
+ storage.init(4, createPage("e"), 0, 0, callback)
+
+ assertFalse(storage.hasPage(1, 0))
+ assertFalse(storage.hasPage(1, 1))
+ assertFalse(storage.hasPage(1, 2))
+ assertFalse(storage.hasPage(1, 3))
+ assertTrue(storage.hasPage(1, 4))
+
+ assertFalse(storage.hasPage(2, 0))
+ assertFalse(storage.hasPage(2, 1))
+ assertTrue(storage.hasPage(2, 2))
+ }
+}
diff --git a/paging/common/src/test/java/android/arch/paging/TiledDataSourceTest.kt b/paging/common/src/test/java/android/arch/paging/TiledDataSourceTest.kt
new file mode 100644
index 0000000..593ccd0
--- /dev/null
+++ b/paging/common/src/test/java/android/arch/paging/TiledDataSourceTest.kt
@@ -0,0 +1,46 @@
+package android.arch.paging
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import java.util.Collections
+
+
+@RunWith(JUnit4::class)
+class TiledDataSourceTest {
+ @Test
+ fun loadInitialEmpty() {
+ @Suppress("UNCHECKED_CAST")
+ val receiver = mock(PageResult.Receiver::class.java) as PageResult.Receiver<Int, String>
+ val dataSource = EmptyDataSource()
+ dataSource.loadRangeInitial(0, 0, 1, 0, receiver)
+
+ @Suppress("UNCHECKED_CAST")
+ val argument = ArgumentCaptor.forClass(PageResult::class.java)
+ as ArgumentCaptor<PageResult<Int, String>>
+ verify(receiver).onPageResult(argument.capture())
+ verifyNoMoreInteractions(receiver)
+
+ val observed = argument.value
+
+ assertEquals(PageResult.INIT, observed.type)
+ assertEquals(Collections.EMPTY_LIST, observed.page.items)
+ }
+
+ class EmptyDataSource : TiledDataSource<String>() {
+ override fun countItems(): Int {
+ return 0
+ }
+
+ override fun loadRange(startPosition: Int, count: Int): List<String> {
+ @Suppress("UNCHECKED_CAST")
+ return Collections.EMPTY_LIST as List<String>
+ }
+ }
+}
\ No newline at end of file
diff --git a/paging/common/src/test/java/android/arch/paging/TiledPagedListTest.java b/paging/common/src/test/java/android/arch/paging/TiledPagedListTest.java
index 4ad02e1..22bfd1f 100644
--- a/paging/common/src/test/java/android/arch/paging/TiledPagedListTest.java
+++ b/paging/common/src/test/java/android/arch/paging/TiledPagedListTest.java
@@ -78,75 +78,107 @@
}
}
- private void verifyRange(PageArrayList<Item> list, Integer... loadedPages) {
+ private void verifyRange(List<Item> list, Integer... loadedPages) {
List<Integer> loadedPageList = Arrays.asList(loadedPages);
assertEquals(ITEMS.size(), list.size());
for (int i = 0; i < list.size(); i++) {
if (loadedPageList.contains(i / PAGE_SIZE)) {
- assertSame(ITEMS.get(i), list.get(i));
+ assertSame("Index " + i, ITEMS.get(i), list.get(i));
} else {
- assertEquals(null, list.get(i));
+ assertEquals("Index " + i, null, list.get(i));
}
}
}
- private TiledPagedList<Item> createTiledPagedList(int loadPosition) {
- return createTiledPagedList(loadPosition, PAGE_SIZE);
+ private TiledPagedList<Item> createTiledPagedList(int loadPosition, int initPages) {
+ return createTiledPagedList(loadPosition, initPages, PAGE_SIZE);
}
- private TiledPagedList<Item> createTiledPagedList(int loadPosition, int prefetchDistance) {
+ private TiledPagedList<Item> createTiledPagedList(int loadPosition, int initPages,
+ int prefetchDistance) {
TestTiledSource source = new TestTiledSource();
return new TiledPagedList<>(
source, mMainThread, mBackgroundThread,
new PagedList.Config.Builder()
.setPageSize(PAGE_SIZE)
+ .setInitialLoadSizeHint(PAGE_SIZE * initPages)
.setPrefetchDistance(prefetchDistance)
.build(),
loadPosition);
}
@Test
- public void initialLoad() {
- TiledPagedList<Item> pagedList = createTiledPagedList(0);
- verifyRange(pagedList, 0);
+ public void computeFirstLoadPosition_zero() {
+ assertEquals(0, TiledPagedList.computeFirstLoadPosition(0, 30, 10, 100));
+ }
+
+ @Test
+ public void computeFirstLoadPosition_requestedPositionIncluded() {
+ assertEquals(0, TiledPagedList.computeFirstLoadPosition(10, 10, 10, 100));
+ }
+
+ @Test
+ public void computeFirstLoadPosition_endAdjusted() {
+ assertEquals(70, TiledPagedList.computeFirstLoadPosition(99, 30, 10, 100));
+ }
+
+ @Test
+ public void initialLoad_onePage() {
+ TiledPagedList<Item> pagedList = createTiledPagedList(0, 1);
+ verifyRange(pagedList, 0, 1);
+ }
+
+ @Test
+ public void initialLoad_onePageOffset() {
+ TiledPagedList<Item> pagedList = createTiledPagedList(10, 1);
+ verifyRange(pagedList, 0, 1);
+ }
+
+ @Test
+ public void initialLoad_full() {
+ TiledPagedList<Item> pagedList = createTiledPagedList(0, 100);
+ verifyRange(pagedList, 0, 1, 2, 3, 4);
}
@Test
public void initialLoad_end() {
- TiledPagedList<Item> pagedList = createTiledPagedList(44);
+ TiledPagedList<Item> pagedList = createTiledPagedList(44, 2);
verifyRange(pagedList, 3, 4);
}
@Test
public void initialLoad_multiple() {
- TiledPagedList<Item> pagedList = createTiledPagedList(9);
+ TiledPagedList<Item> pagedList = createTiledPagedList(9, 2);
verifyRange(pagedList, 0, 1);
}
@Test
public void initialLoad_offset() {
- TiledPagedList<Item> pagedList = createTiledPagedList(41);
+ TiledPagedList<Item> pagedList = createTiledPagedList(41, 2);
verifyRange(pagedList, 3, 4);
}
@Test
public void append() {
- TiledPagedList<Item> pagedList = createTiledPagedList(0);
+ TiledPagedList<Item> pagedList = createTiledPagedList(0, 1);
PagedList.Callback callback = mock(PagedList.Callback.class);
pagedList.addWeakCallback(null, callback);
- verifyRange(pagedList, 0);
+ verifyRange(pagedList, 0, 1);
verifyZeroInteractions(callback);
- pagedList.loadAround(5);
- drain();
+ pagedList.loadAround(15);
verifyRange(pagedList, 0, 1);
- verify(callback).onChanged(10, 10);
+
+ drain();
+
+ verifyRange(pagedList, 0, 1, 2);
+ verify(callback).onChanged(20, 10);
verifyNoMoreInteractions(callback);
}
@Test
public void prepend() {
- TiledPagedList<Item> pagedList = createTiledPagedList(44);
+ TiledPagedList<Item> pagedList = createTiledPagedList(44, 2);
PagedList.Callback callback = mock(PagedList.Callback.class);
pagedList.addWeakCallback(null, callback);
verifyRange(pagedList, 3, 4);
@@ -162,16 +194,16 @@
@Test
public void loadWithGap() {
- TiledPagedList<Item> pagedList = createTiledPagedList(0);
+ TiledPagedList<Item> pagedList = createTiledPagedList(0, 1);
PagedList.Callback callback = mock(PagedList.Callback.class);
pagedList.addWeakCallback(null, callback);
- verifyRange(pagedList, 0);
+ verifyRange(pagedList, 0, 1);
verifyZeroInteractions(callback);
pagedList.loadAround(44);
drain();
- verifyRange(pagedList, 0, 3, 4);
+ verifyRange(pagedList, 0, 1, 3, 4);
verify(callback).onChanged(30, 10);
verify(callback).onChanged(40, 5);
verifyNoMoreInteractions(callback);
@@ -179,58 +211,56 @@
@Test
public void tinyPrefetchTest() {
- TiledPagedList<Item> pagedList = createTiledPagedList(0, 1);
+ TiledPagedList<Item> pagedList = createTiledPagedList(0, 1, 1);
PagedList.Callback callback = mock(PagedList.Callback.class);
pagedList.addWeakCallback(null, callback);
- verifyRange(pagedList, 0); // just 4 loaded
+ verifyRange(pagedList, 0, 1);
verifyZeroInteractions(callback);
- pagedList.loadAround(23);
+ pagedList.loadAround(33);
drain();
- verifyRange(pagedList, 0, 2);
- verify(callback).onChanged(20, 10);
+ verifyRange(pagedList, 0, 1, 3);
+ verify(callback).onChanged(30, 10);
verifyNoMoreInteractions(callback);
pagedList.loadAround(44);
drain();
- verifyRange(pagedList, 0, 2, 4);
+ verifyRange(pagedList, 0, 1, 3, 4);
verify(callback).onChanged(40, 5);
verifyNoMoreInteractions(callback);
}
@Test
public void appendCallbackAddedLate() {
- TiledPagedList<Item> pagedList = createTiledPagedList(0, 0);
- verifyRange(pagedList, 0);
-
- pagedList.loadAround(15);
- drain();
+ TiledPagedList<Item> pagedList = createTiledPagedList(0, 1, 0);
verifyRange(pagedList, 0, 1);
- // snapshot at 20 items
- PageArrayList<Item> snapshot = (PageArrayList<Item>) pagedList.snapshot();
- verifyRange(snapshot, 0, 1);
-
-
pagedList.loadAround(25);
- pagedList.loadAround(35);
drain();
- verifyRange(pagedList, 0, 1, 2, 3);
- verifyRange(snapshot, 0, 1);
+ verifyRange(pagedList, 0, 1, 2);
+
+ // snapshot at 30 items
+ List<Item> snapshot = pagedList.snapshot();
+ verifyRange(snapshot, 0, 1, 2);
+
+ pagedList.loadAround(35);
+ pagedList.loadAround(44);
+ drain();
+ verifyRange(pagedList, 0, 1, 2, 3, 4);
+ verifyRange(snapshot, 0, 1, 2);
PagedList.Callback callback = mock(
PagedList.Callback.class);
pagedList.addWeakCallback(snapshot, callback);
- verify(callback).onChanged(20, 20);
+ verify(callback).onChanged(30, 20);
verifyNoMoreInteractions(callback);
}
-
@Test
public void prependCallbackAddedLate() {
- TiledPagedList<Item> pagedList = createTiledPagedList(44, 0);
+ TiledPagedList<Item> pagedList = createTiledPagedList(44, 2, 0);
verifyRange(pagedList, 3, 4);
pagedList.loadAround(25);
@@ -238,10 +268,9 @@
verifyRange(pagedList, 2, 3, 4);
// snapshot at 30 items
- PageArrayList<Item> snapshot = (PageArrayList<Item>) pagedList.snapshot();
+ List<Item> snapshot = pagedList.snapshot();
verifyRange(snapshot, 2, 3, 4);
-
pagedList.loadAround(15);
pagedList.loadAround(5);
drain();
@@ -272,10 +301,11 @@
assertTrue(pagedList.isContiguous());
- ContiguousPagedList<Item> contiguousPagedList = (ContiguousPagedList<Item>) pagedList;
- assertEquals(0, contiguousPagedList.getLeadingNullCount());
- assertEquals(PAGE_SIZE, contiguousPagedList.mList.size());
- assertEquals(0, contiguousPagedList.getTrailingNullCount());
+ ContiguousPagedList<Integer, Item> contiguousPagedList =
+ (ContiguousPagedList<Integer, Item>) pagedList;
+ assertEquals(0, contiguousPagedList.mStorage.getLeadingNullCount());
+ assertEquals(PAGE_SIZE, contiguousPagedList.mStorage.getStorageCount());
+ assertEquals(0, contiguousPagedList.mStorage.getTrailingNullCount());
}
private void drain() {
diff --git a/paging/runtime/src/androidTest/java/android/arch/paging/ContiguousDiffHelperTest.java b/paging/runtime/src/androidTest/java/android/arch/paging/PagedStorageDiffHelperTest.java
similarity index 61%
rename from paging/runtime/src/androidTest/java/android/arch/paging/ContiguousDiffHelperTest.java
rename to paging/runtime/src/androidTest/java/android/arch/paging/PagedStorageDiffHelperTest.java
index 4f221b3..8cb9224 100644
--- a/paging/runtime/src/androidTest/java/android/arch/paging/ContiguousDiffHelperTest.java
+++ b/paging/runtime/src/androidTest/java/android/arch/paging/PagedStorageDiffHelperTest.java
@@ -16,6 +16,9 @@
package android.arch.paging;
+import static junit.framework.Assert.assertEquals;
+
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
@@ -29,11 +32,12 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
-import org.mockito.Mockito;
+
+import java.util.Arrays;
@SmallTest
@RunWith(JUnit4.class)
-public class ContiguousDiffHelperTest {
+public class PagedStorageDiffHelperTest {
private interface CallbackValidator {
void validate(ListUpdateCallback callback);
}
@@ -51,13 +55,18 @@
}
};
- private void validateTwoListDiff(StringPagedList oldList, StringPagedList newList,
- CallbackValidator callbackValidator) {
- DiffUtil.DiffResult diffResult = ContiguousDiffHelper.computeDiff(oldList, newList,
- DIFF_CALLBACK, false);
+ public static Page<Integer, String> createPage(String... items) {
+ return new Page<>(Arrays.asList(items));
+ }
- ListUpdateCallback listUpdateCallback = Mockito.mock(ListUpdateCallback.class);
- ContiguousDiffHelper.dispatchDiff(listUpdateCallback, oldList, newList, diffResult);
+ private static void validateTwoListDiff(PagedStorage<?, String> oldList,
+ PagedStorage<?, String> newList,
+ CallbackValidator callbackValidator) {
+ DiffUtil.DiffResult diffResult = PagedStorageDiffHelper.computeDiff(
+ oldList, newList, DIFF_CALLBACK);
+
+ ListUpdateCallback listUpdateCallback = mock(ListUpdateCallback.class);
+ PagedStorageDiffHelper.dispatchDiff(listUpdateCallback, oldList, newList, diffResult);
callbackValidator.validate(listUpdateCallback);
}
@@ -65,8 +74,35 @@
@Test
public void sameListNoUpdates() {
validateTwoListDiff(
- new StringPagedList(5, 5, "a", "b", "c"),
- new StringPagedList(5, 5, "a", "b", "c"),
+ new PagedStorage<>(5, createPage("a", "b", "c"), 5),
+ new PagedStorage<>(5, createPage("a", "b", "c"), 5),
+ new CallbackValidator() {
+ @Override
+ public void validate(ListUpdateCallback callback) {
+ verifyZeroInteractions(callback);
+ }
+ }
+ );
+ }
+
+ @Test
+ public void sameListNoUpdatesPlaceholder() {
+ PagedStorage<Integer, String> storageNoPlaceholder =
+ new PagedStorage<>(0, createPage("a", "b", "c"), 10);
+
+ PagedStorage<Integer, String> storageWithPlaceholder =
+ new PagedStorage<>(0, createPage("a", "b", "c"), 10);
+ storageWithPlaceholder.allocatePlaceholders(3, 0, 3,
+ /* ignored */ mock(PagedStorage.Callback.class));
+
+ // even though one has placeholders, and null counts are different...
+ assertEquals(10, storageNoPlaceholder.getTrailingNullCount());
+ assertEquals(7, storageWithPlaceholder.getTrailingNullCount());
+
+ // ... should be no interactions, since content still same
+ validateTwoListDiff(
+ storageNoPlaceholder,
+ storageWithPlaceholder,
new CallbackValidator() {
@Override
public void validate(ListUpdateCallback callback) {
@@ -79,8 +115,8 @@
@Test
public void appendFill() {
validateTwoListDiff(
- new StringPagedList(5, 5, "a", "b"),
- new StringPagedList(5, 4, "a", "b", "c"),
+ new PagedStorage<>(5, createPage("a", "b"), 5),
+ new PagedStorage<>(5, createPage("a", "b", "c"), 4),
new CallbackValidator() {
@Override
public void validate(ListUpdateCallback callback) {
@@ -96,8 +132,8 @@
@Test
public void prependFill() {
validateTwoListDiff(
- new StringPagedList(5, 5, "b", "c"),
- new StringPagedList(4, 5, "a", "b", "c"),
+ new PagedStorage<>(5, createPage("b", "c"), 5),
+ new PagedStorage<>(4, createPage("a", "b", "c"), 5),
new CallbackValidator() {
@Override
public void validate(ListUpdateCallback callback) {
@@ -113,8 +149,8 @@
@Test
public void change() {
validateTwoListDiff(
- new StringPagedList(5, 5, "a1", "b1", "c1"),
- new StringPagedList(5, 5, "a2", "b1", "c2"),
+ new PagedStorage<>(5, createPage("a1", "b1", "c1"), 5),
+ new PagedStorage<>(5, createPage("a2", "b1", "c2"), 5),
new CallbackValidator() {
@Override
public void validate(ListUpdateCallback callback) {
@@ -125,4 +161,5 @@
}
);
}
+
}
diff --git a/paging/runtime/src/androidTest/java/android/arch/paging/StringPagedList.java b/paging/runtime/src/androidTest/java/android/arch/paging/StringPagedList.java
index 5318d38..880d5e9 100644
--- a/paging/runtime/src/androidTest/java/android/arch/paging/StringPagedList.java
+++ b/paging/runtime/src/androidTest/java/android/arch/paging/StringPagedList.java
@@ -16,10 +16,60 @@
package android.arch.paging;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
import java.util.Arrays;
-public class StringPagedList extends NullPaddedList<String> {
- public StringPagedList(int leadingNulls, int trailingNulls, String... items) {
- super(leadingNulls, Arrays.asList(items), trailingNulls);
+public class StringPagedList extends PagedList<String> implements PagedStorage.Callback {
+ StringPagedList(int leadingNulls, int trailingNulls, String... items) {
+ super(new PagedStorage<Integer, String>(),
+ null, null, null);
+ PagedStorage<Integer, String> keyedStorage = (PagedStorage<Integer, String>) mStorage;
+ keyedStorage.init(leadingNulls,
+ new Page<Integer, String>(null, Arrays.asList(items), null),
+ trailingNulls,
+ 0,
+ this);
+ }
+
+ @Override
+ boolean isContiguous() {
+ return true;
+ }
+
+ @Nullable
+ @Override
+ public Object getLastKey() {
+ return null;
+ }
+
+ @Override
+ protected void dispatchUpdatesSinceSnapshot(@NonNull PagedList<String> storageSnapshot,
+ @NonNull Callback callback) {
+ }
+
+ @Override
+ protected void loadAroundInternal(int index) {
+ }
+
+ @Override
+ public void onInitialized(int count) {
+ }
+
+ @Override
+ public void onPagePrepended(int leadingNulls, int changed, int added) {
+ }
+
+ @Override
+ public void onPageAppended(int endPosition, int changed, int added) {
+ }
+
+ @Override
+ public void onPagePlaceholderInserted(int pageIndex) {
+ }
+
+ @Override
+ public void onPageInserted(int start, int count) {
}
}
diff --git a/paging/runtime/src/main/java/android/arch/paging/PagedListAdapterHelper.java b/paging/runtime/src/main/java/android/arch/paging/PagedListAdapterHelper.java
index c7b61d9..0007a2e 100644
--- a/paging/runtime/src/main/java/android/arch/paging/PagedListAdapterHelper.java
+++ b/paging/runtime/src/main/java/android/arch/paging/PagedListAdapterHelper.java
@@ -25,8 +25,6 @@
import android.support.v7.util.ListUpdateCallback;
import android.support.v7.widget.RecyclerView;
-import java.util.List;
-
/**
* Helper object for mapping a {@link PagedList} into a
* {@link android.support.v7.widget.RecyclerView.Adapter RecyclerView.Adapter}.
@@ -123,12 +121,10 @@
private final ListUpdateCallback mUpdateCallback;
private final ListAdapterConfig<T> mConfig;
- // true if our listener is detached from mList, because it's been snapshotted
- private boolean mUpdateScheduled;
-
private boolean mIsContiguous;
- private PagedList<T> mList;
+ private PagedList<T> mPagedList;
+ private PagedList<T> mSnapshot;
// Max generation of currently scheduled runnable
private int mMaxScheduledGeneration;
@@ -182,12 +178,17 @@
@SuppressWarnings("WeakerAccess")
@Nullable
public T getItem(int index) {
- if (mList == null) {
- throw new IndexOutOfBoundsException("Item count is zero, getItem() call is invalid");
+ if (mPagedList == null) {
+ if (mSnapshot == null) {
+ throw new IndexOutOfBoundsException(
+ "Item count is zero, getItem() call is invalid");
+ } else {
+ return mSnapshot.get(index);
+ }
}
- mList.loadAround(index);
- return mList.get(index);
+ mPagedList.loadAround(index);
+ return mPagedList.get(index);
}
/**
@@ -198,7 +199,11 @@
*/
@SuppressWarnings("WeakerAccess")
public int getItemCount() {
- return mList == null ? 0 : mList.size();
+ if (mPagedList != null) {
+ return mPagedList.size();
+ }
+
+ return mSnapshot == null ? 0 : mSnapshot.size();
}
/**
@@ -212,7 +217,7 @@
*/
public void setList(final PagedList<T> pagedList) {
if (pagedList != null) {
- if (mList == null) {
+ if (mPagedList == null && mSnapshot == null) {
mIsContiguous = pagedList.isContiguous();
} else {
if (pagedList.isContiguous() != mIsContiguous) {
@@ -222,7 +227,7 @@
}
}
- if (pagedList == mList) {
+ if (pagedList == mPagedList) {
// nothing to do
return;
}
@@ -231,49 +236,51 @@
final int runGeneration = ++mMaxScheduledGeneration;
if (pagedList == null) {
- mUpdateCallback.onRemoved(0, mList.size());
- mList.removeWeakCallback(mPagedListCallback);
- mList = null;
+ mUpdateCallback.onRemoved(0, getItemCount());
+ if (mPagedList != null) {
+ mPagedList.removeWeakCallback(mPagedListCallback);
+ mPagedList = null;
+ } else if (mSnapshot != null) {
+ mSnapshot = null;
+ }
return;
}
- if (mList == null) {
+ if (mPagedList == null && mSnapshot == null) {
// fast simple first insert
mUpdateCallback.onInserted(0, pagedList.size());
- mList = pagedList;
+ mPagedList = pagedList;
pagedList.addWeakCallback(null, mPagedListCallback);
return;
}
- if (!mList.isImmutable()) {
+ if (mPagedList != null) {
// first update scheduled on this list, so capture mPages as a snapshot, removing
// callbacks so we don't have resolve updates against a moving target
- mList.removeWeakCallback(mPagedListCallback);
- mList = (PagedList<T>) mList.snapshot();
+ mPagedList.removeWeakCallback(mPagedListCallback);
+ mSnapshot = (PagedList<T>) mPagedList.snapshot();
+ mPagedList = null;
}
- final PagedList<T> oldSnapshot = mList;
- final List<T> newSnapshot = pagedList.snapshot();
- mUpdateScheduled = true;
+ if (mSnapshot == null || mPagedList != null) {
+ throw new IllegalStateException("must be in snapshot state to diff");
+ }
+
+ final PagedList<T> oldSnapshot = mSnapshot;
+ final PagedList<T> newSnapshot = (PagedList<T>) pagedList.snapshot();
mConfig.getBackgroundThreadExecutor().execute(new Runnable() {
@Override
public void run() {
final DiffUtil.DiffResult result;
- if (mIsContiguous) {
- result = ContiguousDiffHelper.computeDiff(
- (NullPaddedList<T>) oldSnapshot, (NullPaddedList<T>) newSnapshot,
- mConfig.getDiffCallback(), true);
- } else {
- result = SparseDiffHelper.computeDiff(
- (PageArrayList<T>) oldSnapshot, (PageArrayList<T>) newSnapshot,
- mConfig.getDiffCallback(), true);
- }
+ result = PagedStorageDiffHelper.computeDiff(
+ oldSnapshot.mStorage,
+ newSnapshot.mStorage,
+ mConfig.getDiffCallback());
mConfig.getMainThreadExecutor().execute(new Runnable() {
@Override
public void run() {
if (mMaxScheduledGeneration == runGeneration) {
- mUpdateScheduled = false;
latchPagedList(pagedList, newSnapshot, result);
}
}
@@ -283,16 +290,17 @@
}
private void latchPagedList(
- PagedList<T> newList, List<T> diffSnapshot,
+ PagedList<T> newList, PagedList<T> diffSnapshot,
DiffUtil.DiffResult diffResult) {
- if (mIsContiguous) {
- ContiguousDiffHelper.dispatchDiff(mUpdateCallback,
- (NullPaddedList<T>) mList, (ContiguousPagedList<T>) newList, diffResult);
- } else {
- SparseDiffHelper.dispatchDiff(mUpdateCallback, diffResult);
+ if (mSnapshot == null || mPagedList != null) {
+ throw new IllegalStateException("must be in snapshot state to apply diff");
}
- mList = newList;
- newList.addWeakCallback((PagedList<T>) diffSnapshot, mPagedListCallback);
+
+ PagedStorageDiffHelper.dispatchDiff(mUpdateCallback,
+ mSnapshot.mStorage, newList.mStorage, diffResult);
+ mPagedList = newList;
+ mSnapshot = null;
+ newList.addWeakCallback(diffSnapshot, mPagedListCallback);
}
/**
@@ -307,6 +315,9 @@
@SuppressWarnings("WeakerAccess")
@Nullable
public PagedList<T> getCurrentList() {
- return mList;
+ if (mSnapshot != null) {
+ return mSnapshot;
+ }
+ return mPagedList;
}
}
diff --git a/paging/runtime/src/main/java/android/arch/paging/ContiguousDiffHelper.java b/paging/runtime/src/main/java/android/arch/paging/PagedStorageDiffHelper.java
similarity index 74%
rename from paging/runtime/src/main/java/android/arch/paging/ContiguousDiffHelper.java
rename to paging/runtime/src/main/java/android/arch/paging/PagedStorageDiffHelper.java
index 7dd194b..6fc7039 100644
--- a/paging/runtime/src/main/java/android/arch/paging/ContiguousDiffHelper.java
+++ b/paging/runtime/src/main/java/android/arch/paging/PagedStorageDiffHelper.java
@@ -16,36 +16,31 @@
package android.arch.paging;
-import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
import android.support.v7.recyclerview.extensions.DiffCallback;
import android.support.v7.util.DiffUtil;
import android.support.v7.util.ListUpdateCallback;
-/** @hide */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-class ContiguousDiffHelper {
- private ContiguousDiffHelper() {
+class PagedStorageDiffHelper {
+ private PagedStorageDiffHelper() {
}
- @NonNull
static <T> DiffUtil.DiffResult computeDiff(
- final NullPaddedList<T> oldList, final NullPaddedList<T> newList,
- final DiffCallback<T> diffCallback, boolean detectMoves) {
+ final PagedStorage<?, T> oldList,
+ final PagedStorage<?, T> newList,
+ final DiffCallback<T> diffCallback) {
+ final int oldOffset = oldList.computeLeadingNulls();
+ final int newOffset = newList.computeLeadingNulls();
- if (!oldList.isImmutable()) {
- throw new IllegalArgumentException("list must be immutable to safely perform diff");
- }
- if (!newList.isImmutable()) {
- throw new IllegalArgumentException("list must be immutable to safely perform diff");
- }
+ final int oldSize = oldList.size() - oldOffset - oldList.computeTrailingNulls();
+ final int newSize = newList.size() - newOffset - newList.computeTrailingNulls();
+
return DiffUtil.calculateDiff(new DiffUtil.Callback() {
@Nullable
@Override
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
- T oldItem = oldList.mList.get(oldItemPosition);
- T newItem = newList.mList.get(newItemPosition);
+ T oldItem = oldList.get(oldItemPosition + oldOffset);
+ T newItem = newList.get(newItemPosition + newList.getLeadingNullCount());
if (oldItem == null || newItem == null) {
return null;
}
@@ -54,21 +49,22 @@
@Override
public int getOldListSize() {
- return oldList.mList.size();
+ return oldSize;
}
@Override
public int getNewListSize() {
- return newList.mList.size();
+ return newSize;
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
- T oldItem = oldList.mList.get(oldItemPosition);
- T newItem = newList.mList.get(newItemPosition);
+ T oldItem = oldList.get(oldItemPosition + oldOffset);
+ T newItem = newList.get(newItemPosition + newList.getLeadingNullCount());
if (oldItem == newItem) {
return true;
}
+ //noinspection SimplifiableIfStatement
if (oldItem == null || newItem == null) {
return false;
}
@@ -77,18 +73,19 @@
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
- T oldItem = oldList.mList.get(oldItemPosition);
- T newItem = newList.mList.get(newItemPosition);
+ T oldItem = oldList.get(oldItemPosition + oldOffset);
+ T newItem = newList.get(newItemPosition + newList.getLeadingNullCount());
if (oldItem == newItem) {
return true;
}
+ //noinspection SimplifiableIfStatement
if (oldItem == null || newItem == null) {
return false;
}
return diffCallback.areContentsTheSame(oldItem, newItem);
}
- }, detectMoves);
+ }, true);
}
private static class OffsettingListUpdateCallback implements ListUpdateCallback {
@@ -134,21 +131,25 @@
* immediately after dispatching this diff.
*/
static <T> void dispatchDiff(ListUpdateCallback callback,
- final NullPaddedList<T> oldList, final NullPaddedList<T> newList,
+ final PagedStorage<?, T> oldList,
+ final PagedStorage<?, T> newList,
final DiffUtil.DiffResult diffResult) {
- if (oldList.getLeadingNullCount() == 0
- && oldList.getTrailingNullCount() == 0
- && newList.getLeadingNullCount() == 0
- && newList.getTrailingNullCount() == 0) {
+ final int trailingOld = oldList.computeTrailingNulls();
+ final int trailingNew = newList.computeTrailingNulls();
+ final int leadingOld = oldList.computeLeadingNulls();
+ final int leadingNew = newList.computeLeadingNulls();
+
+ if (trailingOld == 0
+ && trailingNew == 0
+ && leadingOld == 0
+ && leadingNew == 0) {
// Simple case, dispatch & return
diffResult.dispatchUpdatesTo(callback);
return;
}
// First, remove or insert trailing nulls
- final int trailingOld = oldList.getTrailingNullCount();
- final int trailingNew = newList.getTrailingNullCount();
if (trailingOld > trailingNew) {
int count = trailingOld - trailingNew;
callback.onRemoved(oldList.size() - count, count);
@@ -157,8 +158,6 @@
}
// Second, remove or insert leading nulls
- final int leadingOld = oldList.getLeadingNullCount();
- final int leadingNew = newList.getLeadingNullCount();
if (leadingOld > leadingNew) {
callback.onRemoved(0, leadingOld - leadingNew);
} else if (leadingOld < leadingNew) {
diff --git a/paging/runtime/src/main/java/android/arch/paging/SparseDiffHelper.java b/paging/runtime/src/main/java/android/arch/paging/SparseDiffHelper.java
deleted file mode 100644
index fe47897..0000000
--- a/paging/runtime/src/main/java/android/arch/paging/SparseDiffHelper.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * 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.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-import android.support.v7.recyclerview.extensions.DiffCallback;
-import android.support.v7.util.DiffUtil;
-import android.support.v7.util.ListUpdateCallback;
-
-/** @hide */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-class SparseDiffHelper {
- private SparseDiffHelper() {
- }
-
- @NonNull
- static <T> DiffUtil.DiffResult computeDiff(
- final PageArrayList<T> oldList, final PageArrayList<T> newList,
- final DiffCallback<T> diffCallback, boolean detectMoves) {
-
- if (!oldList.isImmutable()) {
- throw new IllegalArgumentException("list must be immutable to safely perform diff");
- }
- if (!newList.isImmutable()) {
- throw new IllegalArgumentException("list must be immutable to safely perform diff");
- }
- return DiffUtil.calculateDiff(new DiffUtil.Callback() {
- @Nullable
- @Override
- public Object getChangePayload(int oldItemPosition, int newItemPosition) {
- T oldItem = oldList.get(oldItemPosition);
- T newItem = newList.get(newItemPosition);
- if (oldItem == null || newItem == null) {
- return null;
- }
- return diffCallback.getChangePayload(oldItem, newItem);
- }
-
- @Override
- public int getOldListSize() {
- return oldList.size();
- }
-
- @Override
- public int getNewListSize() {
- return newList.size();
- }
-
- @Override
- public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
- T oldItem = oldList.get(oldItemPosition);
- T newItem = newList.get(newItemPosition);
- if (oldItem == newItem) {
- return true;
- }
- if (oldItem == null || newItem == null) {
- return false;
- }
- return diffCallback.areItemsTheSame(oldItem, newItem);
- }
-
- @Override
- public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
- T oldItem = oldList.get(oldItemPosition);
- T newItem = newList.get(newItemPosition);
- if (oldItem == newItem) {
- return true;
- }
- if (oldItem == null || newItem == null) {
- return false;
- }
-
- return diffCallback.areContentsTheSame(oldItem, newItem);
- }
- }, detectMoves);
- }
-
- static <T> void dispatchDiff(ListUpdateCallback callback,
- final DiffUtil.DiffResult diffResult) {
- // Simple case, dispatch & return
- diffResult.dispatchUpdatesTo(callback);
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/QueryDataSourceTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/QueryDataSourceTest.java
index e11117e..2735c05 100644
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/QueryDataSourceTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/QueryDataSourceTest.java
@@ -166,17 +166,13 @@
p = dataSource.loadBefore(15, list.get(0), 10);
assertNotNull(p);
- for (User u : p) {
- list.add(0, u);
- }
+ list.addAll(0, p);
assertArrayEquals(Arrays.copyOfRange(expected, 5, 35), list.toArray());
p = dataSource.loadBefore(5, list.get(0), 10);
assertNotNull(p);
- for (User u : p) {
- list.add(0, u);
- }
+ list.addAll(0, p);
assertArrayEquals(Arrays.copyOfRange(expected, 0, 35), list.toArray());
}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/QueryTransactionTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/QueryTransactionTest.java
index dcf98c9..854c862 100644
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/QueryTransactionTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/QueryTransactionTest.java
@@ -205,18 +205,18 @@
LiveData<PagedList<Entity1>> pagedList = mDao.pagedList().create(null, 10);
observeForever(pagedList);
drain();
- assertThat(sStartedTransactionCount.get(), is(mUseTransactionDao ? 1 : 0));
+ assertThat(sStartedTransactionCount.get(), is(mUseTransactionDao ? 0 : 0));
mDao.insert(new Entity1(1, "foo"));
drain();
//noinspection ConstantConditions
assertThat(pagedList.getValue().size(), is(1));
- assertTransactionCount(pagedList.getValue(), mUseTransactionDao ? 3 : 1);
+ assertTransactionCount(pagedList.getValue(), mUseTransactionDao ? 2 : 1);
mDao.insert(new Entity1(2, "bar"));
drain();
assertThat(pagedList.getValue().size(), is(2));
- assertTransactionCount(pagedList.getValue(), mUseTransactionDao ? 5 : 2);
+ assertTransactionCount(pagedList.getValue(), mUseTransactionDao ? 4 : 2);
}
@Test
diff --git a/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/RoomPagedListActivity.java b/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/RoomPagedListActivity.java
index 818c46b..cdd464e 100644
--- a/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/RoomPagedListActivity.java
+++ b/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/RoomPagedListActivity.java
@@ -86,6 +86,7 @@
@Override
protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
PagedList<Customer> list = mAdapter.getCurrentList();
if (list == null) {
// Can't find anything to restore
diff --git a/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/database/CustomerDao.java b/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/database/CustomerDao.java
index 9d40237..b5df914 100644
--- a/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/database/CustomerDao.java
+++ b/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/database/CustomerDao.java
@@ -59,7 +59,7 @@
// Keyed
- @Query("SELECT * from customer ORDER BY mLastName ASC LIMIT :limit")
+ @Query("SELECT * from customer ORDER BY mLastName DESC LIMIT :limit")
List<Customer> customerNameInitial(int limit);
@Query("SELECT * from customer WHERE mLastName < :key ORDER BY mLastName DESC LIMIT :limit")