1/* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.support.v7.widget; 18 19import org.hamcrest.CoreMatchers; 20import org.hamcrest.MatcherAssert; 21import org.junit.Test; 22import org.junit.runner.RunWith; 23 24import android.content.Context; 25import android.support.test.runner.AndroidJUnit4; 26import android.support.v7.util.AsyncListUtil; 27import android.test.suitebuilder.annotation.MediumTest; 28import android.view.View; 29import android.view.ViewGroup; 30import android.widget.TextView; 31 32import java.util.BitSet; 33import java.util.concurrent.CountDownLatch; 34import java.util.concurrent.TimeUnit; 35import static org.junit.Assert.*; 36 37import static java.util.concurrent.TimeUnit.SECONDS; 38 39@MediumTest 40@RunWith(AndroidJUnit4.class) 41public class AsyncListUtilLayoutTest extends BaseRecyclerViewInstrumentationTest { 42 43 private static final boolean DEBUG = false; 44 45 private static final String TAG = "AsyncListUtilLayoutTest"; 46 47 private static final int ITEM_COUNT = 1000; 48 private static final int TILE_SIZE = 5; 49 50 AsyncTestAdapter mAdapter; 51 52 WrappedLinearLayoutManager mLayoutManager; 53 54 private TestDataCallback mDataCallback; 55 private TestViewCallback mViewCallback; 56 private AsyncListUtil<String> mAsyncListUtil; 57 58 public int mStartPrefetch = 0; 59 public int mEndPrefetch = 0; 60 61 @Test 62 public void asyncListUtil() throws Throwable { 63 mRecyclerView = inflateWrappedRV(); 64 mRecyclerView.setHasFixedSize(true); 65 66 mAdapter = new AsyncTestAdapter(); 67 mRecyclerView.setAdapter(mAdapter); 68 69 mLayoutManager = new WrappedLinearLayoutManager( 70 getActivity(), LinearLayoutManager.VERTICAL, false); 71 mRecyclerView.setLayoutManager(mLayoutManager); 72 73 mLayoutManager.expectLayouts(1); 74 setRecyclerView(mRecyclerView); 75 mLayoutManager.waitForLayout(2); 76 77 int rangeStart = 0; 78 assertEquals(rangeStart, mLayoutManager.findFirstVisibleItemPosition()); 79 80 final int rangeSize = mLayoutManager.findLastVisibleItemPosition() + 1; 81 assertTrue("No visible items", rangeSize > 0); 82 83 assertEquals("All visible items must be empty at first", 84 rangeSize, getEmptyVisibleChildCount()); 85 86 mDataCallback = new TestDataCallback(); 87 mViewCallback = new TestViewCallback(); 88 89 mDataCallback.expectTilesInRange(rangeStart, rangeSize); 90 mAdapter.expectItemsInRange(rangeStart, rangeSize); 91 92 runTestOnUiThread(new Runnable() { 93 @Override 94 public void run() { 95 mAsyncListUtil = new AsyncListUtil<String>( 96 String.class, TILE_SIZE, mDataCallback, mViewCallback); 97 } 98 }); 99 100 mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { 101 @Override 102 public void onScrolled(RecyclerView recyclerView, int dx, int dy) { 103 mAsyncListUtil.onRangeChanged(); 104 } 105 }); 106 assertAllLoaded("First load"); 107 108 rangeStart = roundUp(rangeSize); 109 scrollAndAssert("Scroll with no prefetch", rangeStart, rangeSize); 110 111 rangeStart = roundUp(rangeStart + rangeSize); 112 mEndPrefetch = TILE_SIZE * 2; 113 scrollAndAssert("Scroll with prefetch", rangeStart, rangeSize); 114 115 rangeStart += mEndPrefetch; 116 mEndPrefetch = 0; 117 scrollAndAssert("Scroll a little down, no prefetch", rangeStart, 0); 118 119 rangeStart = ITEM_COUNT / 2; 120 mStartPrefetch = TILE_SIZE * 2; 121 mEndPrefetch = TILE_SIZE * 3; 122 scrollAndAssert("Scroll to middle, prefetch", rangeStart, rangeSize); 123 124 rangeStart -= mStartPrefetch; 125 mStartPrefetch = 0; 126 mEndPrefetch = 0; 127 scrollAndAssert("Scroll a little up, no prefetch", rangeStart, 0); 128 129 Thread.sleep(500); // Wait for possible spurious messages. 130 } 131 132 private void assertAllLoaded(String context) 133 throws InterruptedException { 134 assertTrue(context + ", timed out while waiting for items", mAdapter.waitForItems(2)); 135 assertTrue(context + ", timed out while waiting for tiles", mDataCallback.waitForTiles(2)); 136 assertEquals(context + ", empty child found", 0, getEmptyVisibleChildCount()); 137 } 138 139 private void scrollAndAssert(String context, int rangeStart, int rangeSize) throws Throwable { 140 if (rangeSize > 0) { 141 mDataCallback.expectTilesInRange(rangeStart, rangeSize); 142 } else { 143 mDataCallback.expectNoNewTilesLoaded(); 144 } 145 mAdapter.expectItemsInRange(rangeStart, rangeSize); 146 mLayoutManager.expectLayouts(1); 147 scrollToPositionWithOffset(rangeStart, 0); 148 mLayoutManager.waitForLayout(1); 149 assertAllLoaded(context); 150 } 151 152 void scrollToPositionWithOffset(final int position, final int offset) throws Throwable { 153 runTestOnUiThread(new Runnable() { 154 @Override 155 public void run() { 156 mLayoutManager.scrollToPositionWithOffset(position, offset); 157 } 158 }); 159 } 160 161 private int roundUp(int value) { 162 return value - value % TILE_SIZE + TILE_SIZE; 163 } 164 165 private int getTileCount(int start, int size) { 166 return ((start + size - 1) / TILE_SIZE) - (start / TILE_SIZE) + 1; 167 } 168 169 private int getEmptyVisibleChildCount() { 170 int emptyChildCount = 0; 171 int firstVisible = mLayoutManager.findFirstVisibleItemPosition(); 172 int lastVisible = mLayoutManager.findLastVisibleItemPosition(); 173 for (int i = firstVisible; i <= lastVisible; i++) { 174 View child = mLayoutManager.findViewByPosition(i); 175 assertTrue(child instanceof TextView); 176 if (((TextView) child).getText() == "") { 177 emptyChildCount++; 178 } 179 } 180 return emptyChildCount; 181 } 182 183 private class TestDataCallback extends AsyncListUtil.DataCallback<String> { 184 185 private CountDownLatch mTilesLatch; 186 187 @Override 188 public void fillData(String[] data, int startPosition, int itemCount) { 189 assertTrue("Unexpected tile load @" + startPosition, mTilesLatch.getCount() > 0); 190 try { 191 Thread.sleep(100); 192 } catch (InterruptedException e) { 193 } 194 for (int i = 0; i < itemCount; i++) { 195 data[i] = "Item #" + (startPosition + i); 196 } 197 mTilesLatch.countDown(); 198 } 199 200 @Override 201 public int refreshData() { 202 return ITEM_COUNT; 203 } 204 205 private void expectTiles(int count) { 206 mTilesLatch = new CountDownLatch(count); 207 } 208 209 public void expectTilesInRange(int rangeStart, int rangeSize) { 210 expectTiles(getTileCount(rangeStart - mStartPrefetch, 211 rangeSize + mStartPrefetch + mEndPrefetch)); 212 } 213 214 public void expectNoNewTilesLoaded() { 215 expectTiles(0); 216 } 217 218 public boolean waitForTiles(long timeoutInSeconds) throws InterruptedException { 219 return mTilesLatch.await(timeoutInSeconds, TimeUnit.SECONDS); 220 } 221 } 222 223 private class TestViewCallback extends AsyncListUtil.ViewCallback { 224 @Override 225 public void getItemRangeInto(int[] outRange) { 226 outRange[0] = mLayoutManager.findFirstVisibleItemPosition(); 227 outRange[1] = mLayoutManager.findLastVisibleItemPosition(); 228 } 229 230 @Override 231 public void extendRangeInto(int[] range, int[] outRange, int scrollHint) { 232 outRange[0] = range[0] - mStartPrefetch; 233 outRange[1] = range[1] + mEndPrefetch; 234 } 235 236 @Override 237 public void onDataRefresh() { 238 mRecyclerView.getAdapter().notifyDataSetChanged(); 239 } 240 241 @Override 242 public void onItemLoaded(int position) { 243 mRecyclerView.getAdapter().notifyItemChanged(position); 244 } 245 } 246 247 private static class SimpleViewHolder extends RecyclerView.ViewHolder { 248 249 public SimpleViewHolder(Context context) { 250 super(new TextView(context)); 251 } 252 } 253 254 private class AsyncTestAdapter extends RecyclerView.Adapter<SimpleViewHolder> { 255 256 private BitSet mLoadedPositions; 257 private BitSet mExpectedPositions; 258 259 private CountDownLatch mItemsLatch; 260 public AsyncTestAdapter() { 261 mLoadedPositions = new BitSet(ITEM_COUNT); 262 } 263 264 @Override 265 public SimpleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 266 return new SimpleViewHolder(parent.getContext()); 267 } 268 269 @Override 270 public void onBindViewHolder(SimpleViewHolder holder, int position) { 271 final String item = mAsyncListUtil == null ? null : mAsyncListUtil.getItem(position); 272 ((TextView) (holder.itemView)).setText(item == null ? "" : item); 273 274 if (item != null) { 275 mLoadedPositions.set(position); 276 if (mExpectedPositions.get(position)) { 277 mExpectedPositions.clear(position); 278 if (mExpectedPositions.cardinality() == 0) { 279 mItemsLatch.countDown(); 280 } 281 } 282 } 283 } 284 285 @Override 286 public int getItemCount() { 287 return ITEM_COUNT; 288 } 289 290 private void expectItemsInRange(int rangeStart, int rangeSize) { 291 mExpectedPositions = new BitSet(rangeStart + rangeSize); 292 for (int i = 0; i < rangeSize; i++) { 293 if (!mLoadedPositions.get(rangeStart + i)) { 294 mExpectedPositions.set(rangeStart + i); 295 } 296 } 297 mItemsLatch = new CountDownLatch(1); 298 if (mExpectedPositions.cardinality() == 0) { 299 mItemsLatch.countDown(); 300 } 301 } 302 303 public boolean waitForItems(long timeoutInSeconds) throws InterruptedException { 304 return mItemsLatch.await(timeoutInSeconds, TimeUnit.SECONDS); 305 } 306 } 307 308 class WrappedLinearLayoutManager extends LinearLayoutManager { 309 310 CountDownLatch mLayoutLatch; 311 312 public WrappedLinearLayoutManager(Context context, int orientation, boolean reverseLayout) { 313 super(context, orientation, reverseLayout); 314 } 315 316 public void expectLayouts(int count) { 317 mLayoutLatch = new CountDownLatch(count); 318 } 319 320 public void waitForLayout(int seconds) throws Throwable { 321 mLayoutLatch.await(seconds * (DEBUG ? 100 : 1), SECONDS); 322 checkForMainThreadException(); 323 MatcherAssert.assertThat("all layouts should complete on time", 324 mLayoutLatch.getCount(), CoreMatchers.is(0L)); 325 // use a runnable to ensure RV layout is finished 326 getInstrumentation().runOnMainSync(new Runnable() { 327 @Override 328 public void run() { 329 } 330 }); 331 } 332 333 @Override 334 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 335 try { 336 super.onLayoutChildren(recycler, state); 337 } catch (Throwable t) { 338 postExceptionToInstrumentation(t); 339 } 340 mLayoutLatch.countDown(); 341 } 342 } 343} 344