blob: a2fc064b1ccc8874b80c8fb7b0d3dd1afc9c116d [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.arch.paging;
import android.support.annotation.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> {
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>() {
@Override
public T get(int i) {
return null;
}
@Override
public int size() {
return 0;
}
};
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,
int position) {
super(config.mPageSize, dataSource.countItems());
mDataSource = dataSource;
mMainThreadExecutor = mainThreadExecutor;
mBackgroundThreadExecutor = backgroundThreadExecutor;
mConfig = config;
position = Math.min(Math.max(0, position), mCount);
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;
}
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();
}
@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);
}
@Nullable
@Override
public Object getLastKey() {
return mLastLoad;
}
}