blob: b8f6788aa3152b0caa2832b36fb328e4d5fc0208 [file] [log] [blame]
/*
* Copyright (C) 2015 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.support.v7.widget;
import android.content.Context;
import android.support.v7.util.AsyncListUtil;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import java.util.BitSet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class AsyncListUtilLayoutTest extends BaseRecyclerViewInstrumentationTest {
private static final boolean DEBUG = false;
private static final String TAG = "AsyncListUtilLayoutTest";
private static final int ITEM_COUNT = 1000;
private static final int TILE_SIZE = 5;
AsyncTestAdapter mAdapter;
WrappedLinearLayoutManager mLayoutManager;
private TestDataCallback mDataCallback;
private TestViewCallback mViewCallback;
private AsyncListUtil<String> mAsyncListUtil;
public int mStartPrefetch = 0;
public int mEndPrefetch = 0;
public void testAsyncListUtil() throws Throwable {
mRecyclerView = inflateWrappedRV();
mRecyclerView.setHasFixedSize(true);
mAdapter = new AsyncTestAdapter();
mRecyclerView.setAdapter(mAdapter);
mLayoutManager = new WrappedLinearLayoutManager(
getActivity(), LinearLayoutManager.VERTICAL, false);
mRecyclerView.setLayoutManager(mLayoutManager);
mLayoutManager.expectLayouts(1);
setRecyclerView(mRecyclerView);
mLayoutManager.waitForLayout(2);
int rangeStart = 0;
assertEquals(rangeStart, mLayoutManager.findFirstVisibleItemPosition());
final int rangeSize = mLayoutManager.findLastVisibleItemPosition() + 1;
assertTrue("No visible items", rangeSize > 0);
assertEquals("All visible items must be empty at first",
rangeSize, getEmptyVisibleChildCount());
mDataCallback = new TestDataCallback();
mViewCallback = new TestViewCallback();
mDataCallback.expectTilesInRange(rangeStart, rangeSize);
mAdapter.expectItemsInRange(rangeStart, rangeSize);
runTestOnUiThread(new Runnable() {
@Override
public void run() {
mAsyncListUtil = new AsyncListUtil<String>(
String.class, TILE_SIZE, mDataCallback, mViewCallback);
}
});
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
mAsyncListUtil.onRangeChanged();
}
});
assertAllLoaded("First load");
rangeStart = roundUp(rangeSize);
scrollAndAssert("Scroll with no prefetch", rangeStart, rangeSize);
rangeStart = roundUp(rangeStart + rangeSize);
mEndPrefetch = TILE_SIZE * 2;
scrollAndAssert("Scroll with prefetch", rangeStart, rangeSize);
rangeStart += mEndPrefetch;
mEndPrefetch = 0;
scrollAndAssert("Scroll a little down, no prefetch", rangeStart, 0);
rangeStart = ITEM_COUNT / 2;
mStartPrefetch = TILE_SIZE * 2;
mEndPrefetch = TILE_SIZE * 3;
scrollAndAssert("Scroll to middle, prefetch", rangeStart, rangeSize);
rangeStart -= mStartPrefetch;
mStartPrefetch = 0;
mEndPrefetch = 0;
scrollAndAssert("Scroll a little up, no prefetch", rangeStart, 0);
Thread.sleep(500); // Wait for possible spurious messages.
}
private void assertAllLoaded(String context)
throws InterruptedException {
assertTrue(context + ", timed out while waiting for items", mAdapter.waitForItems(2));
assertTrue(context + ", timed out while waiting for tiles", mDataCallback.waitForTiles(2));
assertEquals(context + ", empty child found", 0, getEmptyVisibleChildCount());
}
private void scrollAndAssert(String context, int rangeStart, int rangeSize) throws Throwable {
if (rangeSize > 0) {
mDataCallback.expectTilesInRange(rangeStart, rangeSize);
} else {
mDataCallback.expectNoNewTilesLoaded();
}
mAdapter.expectItemsInRange(rangeStart, rangeSize);
mLayoutManager.expectLayouts(1);
scrollToPositionWithOffset(rangeStart, 0);
mLayoutManager.waitForLayout(1);
assertAllLoaded(context);
}
void scrollToPositionWithOffset(final int position, final int offset) throws Throwable {
runTestOnUiThread(new Runnable() {
@Override
public void run() {
mLayoutManager.scrollToPositionWithOffset(position, offset);
}
});
}
private int roundUp(int value) {
return value - value % TILE_SIZE + TILE_SIZE;
}
private int getTileCount(int start, int size) {
return ((start + size - 1) / TILE_SIZE) - (start / TILE_SIZE) + 1;
}
private int getEmptyVisibleChildCount() {
int emptyChildCount = 0;
int firstVisible = mLayoutManager.findFirstVisibleItemPosition();
int lastVisible = mLayoutManager.findLastVisibleItemPosition();
for (int i = firstVisible; i <= lastVisible; i++) {
View child = mLayoutManager.findViewByPosition(i);
assertTrue(child instanceof TextView);
if (((TextView) child).getText() == "") {
emptyChildCount++;
}
}
return emptyChildCount;
}
private class TestDataCallback extends AsyncListUtil.DataCallback<String> {
private CountDownLatch mTilesLatch;
@Override
public void fillData(String[] data, int startPosition, int itemCount) {
assertTrue("Unexpected tile load @" + startPosition, mTilesLatch.getCount() > 0);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
for (int i = 0; i < itemCount; i++) {
data[i] = "Item #" + (startPosition + i);
}
mTilesLatch.countDown();
}
@Override
public int refreshData() {
return ITEM_COUNT;
}
private void expectTiles(int count) {
mTilesLatch = new CountDownLatch(count);
}
public void expectTilesInRange(int rangeStart, int rangeSize) {
expectTiles(getTileCount(rangeStart - mStartPrefetch,
rangeSize + mStartPrefetch + mEndPrefetch));
}
public void expectNoNewTilesLoaded() {
expectTiles(0);
}
public boolean waitForTiles(long timeoutInSeconds) throws InterruptedException {
return mTilesLatch.await(timeoutInSeconds, TimeUnit.SECONDS);
}
}
private class TestViewCallback extends AsyncListUtil.ViewCallback {
@Override
public void getItemRangeInto(int[] outRange) {
outRange[0] = mLayoutManager.findFirstVisibleItemPosition();
outRange[1] = mLayoutManager.findLastVisibleItemPosition();
}
@Override
public void extendRangeInto(int[] range, int[] outRange, int scrollHint) {
outRange[0] = range[0] - mStartPrefetch;
outRange[1] = range[1] + mEndPrefetch;
}
@Override
public void onDataRefresh() {
mRecyclerView.getAdapter().notifyDataSetChanged();
}
@Override
public void onItemLoaded(int position) {
mRecyclerView.getAdapter().notifyItemChanged(position);
}
}
private static class SimpleViewHolder extends RecyclerView.ViewHolder {
public SimpleViewHolder(Context context) {
super(new TextView(context));
}
}
private class AsyncTestAdapter extends RecyclerView.Adapter<SimpleViewHolder> {
private BitSet mLoadedPositions;
private BitSet mExpectedPositions;
private CountDownLatch mItemsLatch;
public AsyncTestAdapter() {
mLoadedPositions = new BitSet(ITEM_COUNT);
}
@Override
public SimpleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new SimpleViewHolder(parent.getContext());
}
@Override
public void onBindViewHolder(SimpleViewHolder holder, int position) {
final String item = mAsyncListUtil == null ? null : mAsyncListUtil.getItem(position);
((TextView) (holder.itemView)).setText(item == null ? "" : item);
if (item != null) {
mLoadedPositions.set(position);
if (mExpectedPositions.get(position)) {
mExpectedPositions.clear(position);
if (mExpectedPositions.cardinality() == 0) {
mItemsLatch.countDown();
}
}
}
}
@Override
public int getItemCount() {
return ITEM_COUNT;
}
private void expectItemsInRange(int rangeStart, int rangeSize) {
mExpectedPositions = new BitSet(rangeStart + rangeSize);
for (int i = 0; i < rangeSize; i++) {
if (!mLoadedPositions.get(rangeStart + i)) {
mExpectedPositions.set(rangeStart + i);
}
}
mItemsLatch = new CountDownLatch(1);
if (mExpectedPositions.cardinality() == 0) {
mItemsLatch.countDown();
}
}
public boolean waitForItems(long timeoutInSeconds) throws InterruptedException {
return mItemsLatch.await(timeoutInSeconds, TimeUnit.SECONDS);
}
}
class WrappedLinearLayoutManager extends LinearLayoutManager {
CountDownLatch mLayoutLatch;
public WrappedLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
}
public void expectLayouts(int count) {
mLayoutLatch = new CountDownLatch(count);
}
public void waitForLayout(long timeout) throws InterruptedException {
mLayoutLatch.await(timeout * (DEBUG ? 100 : 1), TimeUnit.SECONDS);
assertEquals("all expected layouts should be executed at the expected time",
0, mLayoutLatch.getCount());
getInstrumentation().waitForIdleSync();
}
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
try {
super.onLayoutChildren(recycler, state);
} catch (Throwable t) {
postExceptionToInstrumentation(t);
}
mLayoutLatch.countDown();
}
}
}