blob: d4531d33fa224d9897f06149e8627903d2fb00c1 [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 java.util.AbstractList;
import java.util.ArrayList;
import java.util.List;
final class PagedStorage<T> extends AbstractList<T> {
/**
* Lists instances are compared (with instance equality) to PLACEHOLDER_LIST to check if an item
* in that position is already loading. We use a singleton placeholder list that is distinct
* from Collections.EMPTY_LIST for safety.
*/
@SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
private static final List PLACEHOLDER_LIST = new ArrayList();
// 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<List<T>> 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;
PagedStorage() {
mLeadingNullCount = 0;
mPages = new ArrayList<>();
mTrailingNullCount = 0;
mPositionOffset = 0;
mStorageCount = 0;
mPageSize = 1;
mNumberPrepended = 0;
mNumberAppended = 0;
}
PagedStorage(int leadingNulls, List<T> page, int trailingNulls) {
this();
init(leadingNulls, page, trailingNulls, 0);
}
private PagedStorage(PagedStorage<T> 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;
}
PagedStorage<T> snapshot() {
return new PagedStorage<>(this);
}
private void init(int leadingNulls, List<T> page, int trailingNulls, int positionOffset) {
mLeadingNullCount = leadingNulls;
mPages.clear();
mPages.add(page);
mTrailingNullCount = trailingNulls;
mPositionOffset = positionOffset;
mStorageCount = page.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.size();
mNumberPrepended = 0;
mNumberAppended = 0;
}
void init(int leadingNulls, @NonNull List<T> page, int trailingNulls, int positionOffset,
@NonNull Callback callback) {
init(leadingNulls, page, trailingNulls, positionOffset);
callback.onInitialized(size());
}
@Override
public T 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).size();
if (pageSize > pageInternalIndex) {
// stop, found the page
break;
}
pageInternalIndex -= pageSize;
}
}
List<T> page = mPages.get(localPageIndex);
if (page == null || page.size() == 0) {
// can only occur in tiled case, with untouched inner/placeholder pages
return null;
}
return page.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++) {
List page = mPages.get(i);
if (page != null && page != PLACEHOLDER_LIST) {
break;
}
total += mPageSize;
}
return total;
}
int computeTrailingNulls() {
int total = mTrailingNullCount;
for (int i = mPages.size() - 1; i >= 0; i--) {
List page = mPages.get(i);
if (page != null && page != PLACEHOLDER_LIST) {
break;
}
total += mPageSize;
}
return total;
}
// ---------------- Contiguous API -------------------
T getFirstLoadedItem() {
// 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).get(0);
}
T getLastLoadedItem() {
// 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
List<T> page = mPages.get(mPages.size() - 1);
return page.get(page.size() - 1);
}
void prependPage(@NonNull List<T> page, @NonNull Callback callback) {
final int count = page.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);
}
void appendPage(@NonNull List<T> page, @NonNull Callback callback) {
final int count = page.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).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) ----------------------
void initAndSplit(int leadingNulls, @NonNull List<T> multiPageList,
int trailingNulls, int positionOffset, int pageSize, @NonNull Callback callback) {
int pageCount = (multiPageList.size() + (pageSize - 1)) / pageSize;
for (int i = 0; i < pageCount; i++) {
int beginInclusive = i * pageSize;
int endExclusive = Math.min(multiPageList.size(), (i + 1) * pageSize);
List<T> sublist = multiPageList.subList(beginInclusive, endExclusive);
if (i == 0) {
// Trailing nulls for first page includes other pages in multiPageList
int initialTrailingNulls = trailingNulls + multiPageList.size() - sublist.size();
init(leadingNulls, sublist, initialTrailingNulls, positionOffset);
} else {
int insertPosition = leadingNulls + beginInclusive;
insertPage(insertPosition, sublist, null);
}
}
callback.onInitialized(size());
}
public void insertPage(int position, @NonNull List<T> page, @Nullable Callback callback) {
final int newPageSize = page.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;
List<T> oldPage = mPages.get(localPageIndex);
if (oldPage != null && oldPage != PLACEHOLDER_LIST) {
throw new IllegalArgumentException(
"Invalid position " + position + ": data already loaded");
}
mPages.set(localPageIndex, page);
if (callback != null) {
callback.onPageInserted(position, page.size());
}
}
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) {
//noinspection unchecked
mPages.set(localPageIndex, PLACEHOLDER_LIST);
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;
}
List<T> page = mPages.get(index - leadingNullPages);
return page != null && page != PLACEHOLDER_LIST;
}
@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();
}
}