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 17 package android.support.v7.util; 18 19 import org.junit.After; 20 import org.junit.Before; 21 import org.junit.Test; 22 import org.junit.runner.RunWith; 23 import org.junit.runners.JUnit4; 24 25 import android.support.annotation.UiThread; 26 import android.test.suitebuilder.annotation.MediumTest; 27 import android.util.SparseBooleanArray; 28 29 import java.util.concurrent.CountDownLatch; 30 import java.util.concurrent.TimeUnit; 31 import static org.junit.Assert.*; 32 33 @MediumTest 34 @RunWith(JUnit4.class) 35 public 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 setupCallbacks()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 setUpUi()55 protected void setUpUi() { 56 mAsyncListUtil = new AsyncListUtil<String>( 57 String.class, TILE_SIZE, mDataCallback, mViewCallback); 58 } 59 60 @After tearDown()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 withNoPreload()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 withPreload()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 tileCaching()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 dataRefresh()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 itemChanged()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 getLoadedItemCount(int startPosition, int itemCount)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 scrollAndExpectTiles(int position, String context, int... positions)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 waitForLatch(String context, CountDownLatch latch)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 refreshOnUiThread()150 private void refreshOnUiThread() throws Throwable { 151 runTestOnUiThread(new Runnable() { 152 @Override 153 public void run() { 154 mAsyncListUtil.refresh(); 155 } 156 }); 157 } 158 assertLoadedItemsOnUiThread(final String message, final int expectedCount, final int position, final int count)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 scrollOnUiThread(final int position)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 fillData(String[] data, int startPosition, int itemCount)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 refreshData()199 public int refreshData() { 200 return mDataItemCount; 201 } 202 getMaxCachedTiles()203 public int getMaxCachedTiles() { 204 return mCacheSize; 205 } 206 expectTiles(int... positions)207 public void expectTiles(int... positions) { 208 synchronized (mTilesFilledLatch) { 209 mTilesFilledLatch.expect(positions); 210 } 211 } 212 waitForTiles(String context)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 getItemRangeInto(int[] outRange)232 public void getItemRangeInto(int[] outRange) { 233 outRange[0] = mFirstVisibleItem; 234 outRange[1] = mLastVisibleItem; 235 } 236 237 @Override extendRangeInto(int[] range, int[] outRange, int scrollHint)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 onDataRefresh()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 onItemLoaded(int position)256 public void onItemLoaded(int position) { 257 mItemsChangedLatch.countDown(position); 258 } 259 expectDataSetChanged(int expectedItemCount)260 public void expectDataSetChanged(int expectedItemCount) { 261 mDataCallback.mDataItemCount = expectedItemCount; 262 mExpectedItemCount = expectedItemCount; 263 mDataRefreshLatch = new CountDownLatch(1); 264 } 265 waitForDataSetChanged(String context)266 public void waitForDataSetChanged(String context) throws InterruptedException { 267 waitForLatch("timed out waiting for data set change (" + context + ")", 268 mDataRefreshLatch); 269 } 270 expectItemRangeChanged(int startPosition, int itemCount)271 public void expectItemRangeChanged(int startPosition, int itemCount) { 272 mItemsChangedLatch.expectRange(startPosition, itemCount); 273 } 274 waitForItems()275 public void waitForItems() throws InterruptedException { 276 waitForLatch("onItemChanged", mItemsChangedLatch.mLatch); 277 } 278 279 @UiThread scrollTo(int position)280 public void scrollTo(int position) { 281 mLastVisibleItem += position - mFirstVisibleItem; 282 mFirstVisibleItem = position; 283 mAsyncListUtil.onRangeChanged(); 284 } 285 286 @UiThread updateViewport()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 PositionSetLatch(String kind)303 PositionSetLatch(String kind) { 304 this.mKind = kind; 305 } 306 expect(int ... positions)307 void expect(int ... positions) { 308 mExpectedPositions.clear(); 309 for (int position : positions) { 310 mExpectedPositions.put(position, true); 311 } 312 createLatch(); 313 } 314 expectRange(int position, int count)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 countDown(int position)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 createLatch()334 private void createLatch() { 335 mLatch = new CountDownLatch(1); 336 if (mExpectedPositions.size() == 0) { 337 mLatch.countDown(); 338 } 339 } 340 } 341 } 342