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