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