/* * 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); 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(); } } }