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.util; 18 19import org.junit.After; 20import org.junit.Before; 21import org.junit.Test; 22import org.junit.runner.RunWith; 23import org.junit.runners.JUnit4; 24 25import android.support.annotation.UiThread; 26import android.test.suitebuilder.annotation.MediumTest; 27import android.util.SparseBooleanArray; 28 29import java.util.concurrent.CountDownLatch; 30import java.util.concurrent.TimeUnit; 31import static org.junit.Assert.*; 32 33@MediumTest 34@RunWith(JUnit4.class) 35public class AsyncListUtilTest extends BaseThreadedTest { 36 37 private static final int TILE_SIZE = 10; 38 39 private TestDataCallback mDataCallback; 40 private TestViewCallback mViewCallback; 41 42 AsyncListUtil<String> mAsyncListUtil; 43 44 @Before 45 public final void setupCallbacks() throws Exception { 46 mDataCallback = new TestDataCallback(); 47 mViewCallback = new TestViewCallback(); 48 mDataCallback.expectTiles(0, 10, 20); 49 super.setUp(); 50 mDataCallback.waitForTiles("initial load"); 51 } 52 53 @Override 54 @UiThread 55 protected void setUpUi() { 56 mAsyncListUtil = new AsyncListUtil<String>( 57 String.class, TILE_SIZE, mDataCallback, mViewCallback); 58 } 59 60 @After 61 public void tearDown() throws Exception { 62 /// Wait a little extra to catch spurious messages. 63 new CountDownLatch(1).await(500, TimeUnit.MILLISECONDS); 64 } 65 66 @Test 67 public void withNoPreload() throws Throwable { 68 scrollAndExpectTiles(10, "scroll to 10", 30); 69 scrollAndExpectTiles(25, "scroll to 25", 40); 70 scrollAndExpectTiles(45, "scroll to 45", 50, 60); 71 scrollAndExpectTiles(70, "scroll to 70", 70, 80, 90); 72 } 73 74 @Test 75 public void withPreload() throws Throwable { 76 mViewCallback.mStartPreload = 5; 77 mViewCallback.mEndPreload = 15; 78 scrollAndExpectTiles(50, "scroll down a lot", 40, 50, 60, 70, 80); 79 80 mViewCallback.mStartPreload = 0; 81 mViewCallback.mEndPreload = 0; 82 scrollAndExpectTiles(60, "scroll down a little, no new tiles loaded"); 83 scrollAndExpectTiles(40, "scroll up a little, no new tiles loaded"); 84 } 85 86 @Test 87 public void tileCaching() throws Throwable { 88 scrollAndExpectTiles(25, "next screen", 30, 40); 89 90 scrollAndExpectTiles(0, "back at top, no new page loads"); 91 scrollAndExpectTiles(25, "next screen again, no new page loads"); 92 93 mDataCallback.mCacheSize = 3; 94 scrollAndExpectTiles(50, "scroll down more, all pages should load", 50, 60, 70); 95 scrollAndExpectTiles(0, "scroll back to top, all pages should reload", 0, 10, 20); 96 } 97 98 @Test 99 public void dataRefresh() throws Throwable { 100 mViewCallback.expectDataSetChanged(40); 101 mDataCallback.expectTiles(0, 10, 20); 102 refreshOnUiThread(); 103 mViewCallback.waitForDataSetChanged("increasing item count"); 104 mDataCallback.waitForTiles("increasing item count"); 105 106 mViewCallback.expectDataSetChanged(15); 107 mDataCallback.expectTiles(0, 10); 108 refreshOnUiThread(); 109 mViewCallback.waitForDataSetChanged("decreasing item count"); 110 mDataCallback.waitForTiles("decreasing item count"); 111 } 112 113 @Test 114 public void itemChanged() throws Throwable { 115 final int position = 30; 116 final int count = 20; 117 118 assertLoadedItemsOnUiThread("no new items should be loaded", 0, position, count); 119 120 mViewCallback.expectItemRangeChanged(position, count); 121 scrollAndExpectTiles(20, "scrolling to missing items", 30, 40); 122 mViewCallback.waitForItems(); 123 124 assertLoadedItemsOnUiThread("all new items should be loaded", count, position, count); 125 } 126 127 @UiThread 128 private int getLoadedItemCount(int startPosition, int itemCount) { 129 int loaded = 0; 130 for (int i = 0; i < itemCount; i++) { 131 if (mAsyncListUtil.getItem(startPosition + i) != null) { 132 loaded++; 133 } 134 } 135 return loaded; 136 } 137 138 private void scrollAndExpectTiles(int position, String context, int... positions) 139 throws Throwable { 140 mDataCallback.expectTiles(positions); 141 scrollOnUiThread(position); 142 mDataCallback.waitForTiles(context); 143 } 144 145 private static void waitForLatch(String context, CountDownLatch latch) 146 throws InterruptedException { 147 assertTrue("timed out waiting for " + context, latch.await(1, TimeUnit.SECONDS)); 148 } 149 150 private void refreshOnUiThread() throws Throwable { 151 runTestOnUiThread(new Runnable() { 152 @Override 153 public void run() { 154 mAsyncListUtil.refresh(); 155 } 156 }); 157 } 158 159 private void assertLoadedItemsOnUiThread(final String message, 160 final int expectedCount, 161 final int position, 162 final int count) throws Throwable { 163 runTestOnUiThread(new Runnable() { 164 @Override 165 public void run() { 166 assertEquals(message, expectedCount, getLoadedItemCount(position, count)); 167 } 168 }); 169 } 170 171 private void scrollOnUiThread(final int position) throws Throwable { 172 runTestOnUiThread(new Runnable() { 173 @Override 174 public void run() { 175 mViewCallback.scrollTo(position); 176 } 177 }); 178 } 179 180 private class TestDataCallback extends AsyncListUtil.DataCallback<String> { 181 private int mCacheSize = 10; 182 183 int mDataItemCount = 100; 184 185 final PositionSetLatch mTilesFilledLatch = new PositionSetLatch("filled"); 186 187 @Override 188 public void fillData(String[] data, int startPosition, int itemCount) { 189 synchronized (mTilesFilledLatch) { 190 assertEquals(Math.min(TILE_SIZE, mDataItemCount - startPosition), itemCount); 191 mTilesFilledLatch.countDown(startPosition); 192 } 193 for (int i = 0; i < itemCount; i++) { 194 data[i] = "item #" + startPosition; 195 } 196 } 197 198 @Override 199 public int refreshData() { 200 return mDataItemCount; 201 } 202 203 public int getMaxCachedTiles() { 204 return mCacheSize; 205 } 206 207 public void expectTiles(int... positions) { 208 synchronized (mTilesFilledLatch) { 209 mTilesFilledLatch.expect(positions); 210 } 211 } 212 213 private void waitForTiles(String context) throws InterruptedException { 214 waitForLatch("filled tiles (" + context + ")", mTilesFilledLatch.mLatch); 215 } 216 } 217 218 private class TestViewCallback extends AsyncListUtil.ViewCallback { 219 public static final int VIEWPORT_SIZE = 25; 220 private int mStartPreload; 221 private int mEndPreload; 222 223 int mFirstVisibleItem; 224 int mLastVisibleItem = VIEWPORT_SIZE - 1; 225 226 private int mExpectedItemCount; 227 CountDownLatch mDataRefreshLatch; 228 229 PositionSetLatch mItemsChangedLatch = new PositionSetLatch("item changed"); 230 231 @Override 232 public void getItemRangeInto(int[] outRange) { 233 outRange[0] = mFirstVisibleItem; 234 outRange[1] = mLastVisibleItem; 235 } 236 237 @Override 238 public void extendRangeInto(int[] range, int[] outRange, int scrollHint) { 239 outRange[0] = range[0] - mStartPreload; 240 outRange[1] = range[1] + mEndPreload; 241 } 242 243 @Override 244 @UiThread 245 public void onDataRefresh() { 246 if (mDataRefreshLatch == null) { 247 return; 248 } 249 assertTrue("unexpected onDataRefresh notification", mDataRefreshLatch.getCount() == 1); 250 assertEquals(mExpectedItemCount, mAsyncListUtil.getItemCount()); 251 mDataRefreshLatch.countDown(); 252 updateViewport(); 253 } 254 255 @Override 256 public void onItemLoaded(int position) { 257 mItemsChangedLatch.countDown(position); 258 } 259 260 public void expectDataSetChanged(int expectedItemCount) { 261 mDataCallback.mDataItemCount = expectedItemCount; 262 mExpectedItemCount = expectedItemCount; 263 mDataRefreshLatch = new CountDownLatch(1); 264 } 265 266 public void waitForDataSetChanged(String context) throws InterruptedException { 267 waitForLatch("timed out waiting for data set change (" + context + ")", 268 mDataRefreshLatch); 269 } 270 271 public void expectItemRangeChanged(int startPosition, int itemCount) { 272 mItemsChangedLatch.expectRange(startPosition, itemCount); 273 } 274 275 public void waitForItems() throws InterruptedException { 276 waitForLatch("onItemChanged", mItemsChangedLatch.mLatch); 277 } 278 279 @UiThread 280 public void scrollTo(int position) { 281 mLastVisibleItem += position - mFirstVisibleItem; 282 mFirstVisibleItem = position; 283 mAsyncListUtil.onRangeChanged(); 284 } 285 286 @UiThread 287 private void updateViewport() { 288 int itemCount = mAsyncListUtil.getItemCount(); 289 if (mLastVisibleItem < itemCount) { 290 return; 291 } 292 mLastVisibleItem = itemCount - 1; 293 mFirstVisibleItem = Math.max(0, mLastVisibleItem - VIEWPORT_SIZE + 1); 294 } 295 } 296 297 private static class PositionSetLatch { 298 public CountDownLatch mLatch = new CountDownLatch(0); 299 300 final private SparseBooleanArray mExpectedPositions = new SparseBooleanArray(); 301 final private String mKind; 302 303 PositionSetLatch(String kind) { 304 this.mKind = kind; 305 } 306 307 void expect(int ... positions) { 308 mExpectedPositions.clear(); 309 for (int position : positions) { 310 mExpectedPositions.put(position, true); 311 } 312 createLatch(); 313 } 314 315 void expectRange(int position, int count) { 316 mExpectedPositions.clear(); 317 for (int i = 0; i < count; i++) { 318 mExpectedPositions.put(position + i, true); 319 } 320 createLatch(); 321 } 322 323 void countDown(int position) { 324 if (mLatch == null) { 325 return; 326 } 327 assertTrue("unexpected " + mKind + " @" + position, mExpectedPositions.get(position)); 328 mExpectedPositions.delete(position); 329 if (mExpectedPositions.size() == 0) { 330 mLatch.countDown(); 331 } 332 } 333 334 private void createLatch() { 335 mLatch = new CountDownLatch(1); 336 if (mExpectedPositions.size() == 0) { 337 mLatch.countDown(); 338 } 339 } 340 } 341} 342