1 /*
2  * Copyright 2018 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 androidx.recyclerview.widget;
18 
19 import static org.hamcrest.CoreMatchers.is;
20 import static org.hamcrest.MatcherAssert.assertThat;
21 import static org.hamcrest.core.IsEqual.equalTo;
22 import static org.junit.Assert.assertEquals;
23 import static org.junit.Assert.assertFalse;
24 import static org.junit.Assert.assertNotEquals;
25 import static org.junit.Assert.assertNotNull;
26 import static org.junit.Assert.assertSame;
27 import static org.junit.Assert.assertTrue;
28 import static org.mockito.ArgumentMatchers.any;
29 import static org.mockito.ArgumentMatchers.anyInt;
30 import static org.mockito.ArgumentMatchers.argThat;
31 import static org.mockito.Mockito.mock;
32 import static org.mockito.Mockito.never;
33 import static org.mockito.Mockito.times;
34 import static org.mockito.Mockito.verify;
35 import static org.mockito.Mockito.when;
36 
37 import android.content.Context;
38 import android.os.Build;
39 import android.os.Parcelable;
40 import android.os.SystemClock;
41 import android.view.MotionEvent;
42 import android.view.View;
43 import android.view.ViewGroup;
44 import android.widget.FrameLayout;
45 
46 import androidx.test.core.app.ApplicationProvider;
47 import androidx.test.ext.junit.runners.AndroidJUnit4;
48 import androidx.test.filters.MediumTest;
49 import androidx.test.filters.SdkSuppress;
50 
51 import org.jspecify.annotations.NonNull;
52 import org.junit.After;
53 import org.junit.Before;
54 import org.junit.Test;
55 import org.junit.runner.RunWith;
56 import org.mockito.ArgumentMatcher;
57 import org.mockito.invocation.InvocationOnMock;
58 import org.mockito.stubbing.Answer;
59 
60 import java.util.ArrayList;
61 import java.util.List;
62 import java.util.concurrent.TimeUnit;
63 
64 @SuppressWarnings("unchecked")
65 @MediumTest
66 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
67 @RunWith(AndroidJUnit4.class)
68 public class RecyclerViewCacheTest {
69     TimeMockingRecyclerView mRecyclerView;
70     RecyclerView.Recycler mRecycler;
71 
72     private class TimeMockingRecyclerView extends RecyclerView {
73         private long mMockNanoTime = 0;
74 
TimeMockingRecyclerView(Context context)75         TimeMockingRecyclerView(Context context) {
76             super(context);
77         }
78 
registerTimePassingMs(long ms)79         public void registerTimePassingMs(long ms) {
80             mMockNanoTime += TimeUnit.MILLISECONDS.toNanos(ms);
81         }
82 
83         @Override
getNanoTime()84         long getNanoTime() {
85             return mMockNanoTime;
86         }
87 
88         @Override
getWindowVisibility()89         public int getWindowVisibility() {
90             // Pretend to be visible to avoid being filtered out
91             return View.VISIBLE;
92         }
93     }
94 
95     @Before
setup()96     public void setup() throws Exception {
97         mRecyclerView = new TimeMockingRecyclerView(getContext());
98         mRecyclerView.onAttachedToWindow();
99         mRecycler = mRecyclerView.mRecycler;
100     }
101 
102     @After
teardown()103     public void teardown() throws Exception {
104         if (mRecyclerView.isAttachedToWindow()) {
105             mRecyclerView.onDetachedFromWindow();
106         }
107         GapWorker gapWorker = GapWorker.sGapWorker.get();
108         if (gapWorker != null) {
109             assertTrue(gapWorker.mRecyclerViews.isEmpty());
110         }
111     }
112 
getContext()113     private Context getContext() {
114         return ApplicationProvider.getApplicationContext();
115     }
116 
layout(int width, int height)117     private void layout(int width, int height) {
118         mRecyclerView.measure(
119                 View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
120                 View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY));
121         mRecyclerView.layout(0, 0, width, height);
122     }
123 
124     @Test
prefetchReusesCacheItems()125     public void prefetchReusesCacheItems() {
126         RecyclerView.LayoutManager prefetchingLayoutManager = new RecyclerView.LayoutManager() {
127             @Override
128             public RecyclerView.LayoutParams generateDefaultLayoutParams() {
129                 return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
130                         ViewGroup.LayoutParams.WRAP_CONTENT);
131             }
132 
133             @Override
134             public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state,
135                     LayoutPrefetchRegistry prefetchManager) {
136                 prefetchManager.addPosition(0, 0);
137                 prefetchManager.addPosition(1, 0);
138                 prefetchManager.addPosition(2, 0);
139             }
140 
141             @Override
142             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
143             }
144         };
145         mRecyclerView.setLayoutManager(prefetchingLayoutManager);
146 
147         RecyclerView.Adapter mockAdapter = mock(RecyclerView.Adapter.class);
148         when(mockAdapter.onCreateViewHolder(any(ViewGroup.class), anyInt()))
149                 .thenAnswer(new Answer<RecyclerView.ViewHolder>() {
150                     @Override
151                     public RecyclerView.ViewHolder answer(InvocationOnMock invocation)
152                             throws Throwable {
153                         return new RecyclerView.ViewHolder(new View(getContext())) {};
154                     }
155                 });
156         when(mockAdapter.getItemCount()).thenReturn(10);
157         mRecyclerView.setAdapter(mockAdapter);
158 
159         layout(320, 320);
160 
161         verify(mockAdapter, never()).onCreateViewHolder(any(ViewGroup.class), anyInt());
162         verify(mockAdapter, never()).onBindViewHolder(
163                 any(RecyclerView.ViewHolder.class), anyInt(), any(List.class));
164         assertTrue(mRecycler.mCachedViews.isEmpty());
165 
166         // Prefetch multiple times...
167         for (int i = 0; i < 4; i++) {
168             mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
169 
170             // ...but should only see the same three items fetched/bound once each
171             verify(mockAdapter, times(3)).onCreateViewHolder(any(ViewGroup.class), anyInt());
172             verify(mockAdapter, times(3)).onBindViewHolder(
173                     any(RecyclerView.ViewHolder.class), anyInt(), any(List.class));
174 
175             assertTrue(mRecycler.mCachedViews.size() == 3);
176             CacheUtils.verifyCacheContainsPrefetchedPositions(mRecyclerView, 0, 1, 2);
177         }
178     }
179 
180     @Test
prefetchItemsNotEvictedWithInserts()181     public void prefetchItemsNotEvictedWithInserts() {
182         mRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 3));
183 
184         RecyclerView.Adapter mockAdapter = mock(RecyclerView.Adapter.class);
185         when(mockAdapter.onCreateViewHolder(any(ViewGroup.class), anyInt()))
186                 .thenAnswer(new Answer<RecyclerView.ViewHolder>() {
187                     @Override
188                     public RecyclerView.ViewHolder answer(InvocationOnMock invocation)
189                             throws Throwable {
190                         View view = new View(getContext());
191                         view.setMinimumWidth(100);
192                         view.setMinimumHeight(100);
193                         return new RecyclerView.ViewHolder(view) {};
194                     }
195                 });
196         when(mockAdapter.getItemCount()).thenReturn(100);
197         mRecyclerView.setAdapter(mockAdapter);
198 
199         layout(300, 100);
200 
201         assertEquals(2, mRecyclerView.mRecycler.mViewCacheMax);
202         mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
203         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
204         assertEquals(5, mRecyclerView.mRecycler.mViewCacheMax);
205 
206         CacheUtils.verifyCacheContainsPrefetchedPositions(mRecyclerView, 3, 4, 5);
207 
208         // further views recycled, as though from scrolling, shouldn't evict prefetched views:
209         mRecycler.recycleView(mRecycler.getViewForPosition(10));
210         CacheUtils.verifyCacheContainsPositions(mRecyclerView, 3, 4, 5, 10);
211 
212         mRecycler.recycleView(mRecycler.getViewForPosition(20));
213         CacheUtils.verifyCacheContainsPositions(mRecyclerView, 3, 4, 5, 10, 20);
214 
215         mRecycler.recycleView(mRecycler.getViewForPosition(30));
216         CacheUtils.verifyCacheContainsPositions(mRecyclerView, 3, 4, 5, 20, 30);
217 
218         mRecycler.recycleView(mRecycler.getViewForPosition(40));
219         CacheUtils.verifyCacheContainsPositions(mRecyclerView, 3, 4, 5, 30, 40);
220 
221         // After clearing the cache, the prefetch priorities should be cleared as well:
222         mRecyclerView.mRecycler.recycleAndClearCachedViews();
223         for (int i : new int[] {3, 4, 5, 50, 60, 70, 80, 90}) {
224             mRecycler.recycleView(mRecycler.getViewForPosition(i));
225         }
226 
227         // cache only contains most recent positions, no priority for previous prefetches:
228         CacheUtils.verifyCacheContainsPositions(mRecyclerView, 50, 60, 70, 80, 90);
229     }
230 
231     @Test
prefetchItemsNotEvictedOnScroll()232     public void prefetchItemsNotEvictedOnScroll() {
233         mRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 3));
234 
235         // 100x100 pixel views
236         RecyclerView.Adapter mockAdapter = mock(RecyclerView.Adapter.class);
237         when(mockAdapter.onCreateViewHolder(any(ViewGroup.class), anyInt()))
238                 .thenAnswer(new Answer<RecyclerView.ViewHolder>() {
239                     @Override
240                     public RecyclerView.ViewHolder answer(InvocationOnMock invocation)
241                             throws Throwable {
242                         View view = new View(getContext());
243                         view.setMinimumWidth(100);
244                         view.setMinimumHeight(100);
245                         return new RecyclerView.ViewHolder(view) {};
246                     }
247                 });
248         when(mockAdapter.getItemCount()).thenReturn(100);
249         mRecyclerView.setAdapter(mockAdapter);
250 
251         // NOTE: requested cache size must be smaller than span count so two rows cannot fit
252         mRecyclerView.setItemViewCacheSize(2);
253 
254         layout(300, 150);
255         mRecyclerView.scrollBy(0, 75);
256         assertTrue(mRecycler.mCachedViews.isEmpty());
257 
258         // rows 0, 1, and 2 are all attached and visible. Prefetch row 3:
259         mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
260         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
261 
262         // row 3 is cached:
263         CacheUtils.verifyCacheContainsPositions(mRecyclerView, 9, 10, 11);
264         assertTrue(mRecycler.mCachedViews.size() == 3);
265 
266         // Scroll so 1 falls off (though 3 is still not on screen)
267         mRecyclerView.scrollBy(0, 50);
268 
269         // row 3 is still cached, with a couple other recycled views:
270         CacheUtils.verifyCacheContainsPositions(mRecyclerView, 9, 10, 11);
271         assertTrue(mRecycler.mCachedViews.size() == 5);
272     }
273 
274     @Test
prefetchIsComputingLayout()275     public void prefetchIsComputingLayout() {
276         mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
277 
278         // 100x100 pixel views
279         RecyclerView.Adapter mockAdapter = mock(RecyclerView.Adapter.class);
280         when(mockAdapter.onCreateViewHolder(any(ViewGroup.class), anyInt()))
281                 .thenAnswer(new Answer<RecyclerView.ViewHolder>() {
282                     @Override
283                     public RecyclerView.ViewHolder answer(InvocationOnMock invocation)
284                             throws Throwable {
285                         View view = new View(getContext());
286                         view.setMinimumWidth(100);
287                         view.setMinimumHeight(100);
288                         assertTrue(mRecyclerView.isComputingLayout());
289                         return new RecyclerView.ViewHolder(view) {};
290                     }
291                 });
292         when(mockAdapter.getItemCount()).thenReturn(100);
293         mRecyclerView.setAdapter(mockAdapter);
294 
295         layout(100, 100);
296 
297         verify(mockAdapter, times(1)).onCreateViewHolder(mRecyclerView, 0);
298 
299         // prefetch an item, should still observe isComputingLayout in that create
300         mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
301         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
302 
303         verify(mockAdapter, times(2)).onCreateViewHolder(mRecyclerView, 0);
304     }
305 
306     @Test
prefetchAfterOrientationChange()307     public void prefetchAfterOrientationChange() {
308         LinearLayoutManager layout = new LinearLayoutManager(getContext(),
309                 LinearLayoutManager.VERTICAL, false);
310         mRecyclerView.setLayoutManager(layout);
311 
312         // 100x100 pixel views
313         mRecyclerView.setAdapter(new RecyclerView.Adapter() {
314             @Override
315             public RecyclerView.@NonNull ViewHolder onCreateViewHolder(
316                     @NonNull ViewGroup parent, int viewType) {
317                 View view = new View(getContext());
318                 view.setMinimumWidth(100);
319                 view.setMinimumHeight(100);
320                 assertTrue(mRecyclerView.isComputingLayout());
321                 return new RecyclerView.ViewHolder(view) {};
322             }
323 
324             @Override
325             public void onBindViewHolder(RecyclerView.@NonNull ViewHolder holder, int position) {}
326 
327             @Override
328             public int getItemCount() {
329                 return 100;
330             }
331         });
332 
333         layout(100, 100);
334 
335         layout.setOrientation(LinearLayoutManager.HORIZONTAL);
336 
337         // Prefetch an item after changing orientation, before layout - shouldn't crash
338         mRecyclerView.mPrefetchRegistry.setPrefetchVector(1, 1);
339         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
340     }
341 
342     @Test
prefetchDrag()343     public void prefetchDrag() {
344         // event dispatch requires a parent
345         ViewGroup parent = new FrameLayout(getContext());
346         parent.addView(mRecyclerView);
347 
348 
349         mRecyclerView.setLayoutManager(
350                 new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false));
351 
352         // 1000x1000 pixel views
353         RecyclerView.Adapter adapter = new RecyclerView.Adapter() {
354             @Override
355             public RecyclerView.ViewHolder onCreateViewHolder(
356                     @NonNull ViewGroup parent, int viewType) {
357                 mRecyclerView.registerTimePassingMs(5);
358                 View view = new View(getContext());
359                 view.setMinimumWidth(1000);
360                 view.setMinimumHeight(1000);
361                 return new RecyclerView.ViewHolder(view) {};
362             }
363 
364             @Override
365             public void onBindViewHolder(RecyclerView.@NonNull ViewHolder holder, int position) {
366                 mRecyclerView.registerTimePassingMs(5);
367             }
368 
369             @Override
370             public int getItemCount() {
371                 return 100;
372             }
373         };
374         mRecyclerView.setAdapter(adapter);
375 
376         layout(1000, 1000);
377 
378         long time = SystemClock.uptimeMillis();
379         mRecyclerView.onTouchEvent(
380                 MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 500, 1000, 0));
381 
382         assertEquals(0, mRecyclerView.mPrefetchRegistry.mPrefetchDx);
383         assertEquals(0, mRecyclerView.mPrefetchRegistry.mPrefetchDy);
384 
385         // Consume slop
386         mRecyclerView.onTouchEvent(
387                 MotionEvent.obtain(time, time, MotionEvent.ACTION_MOVE, 50, 500, 0));
388 
389         // move by 0,30
390         mRecyclerView.onTouchEvent(
391                 MotionEvent.obtain(time, time, MotionEvent.ACTION_MOVE, 50, 470, 0));
392         assertEquals(0, mRecyclerView.mPrefetchRegistry.mPrefetchDx);
393         assertEquals(30, mRecyclerView.mPrefetchRegistry.mPrefetchDy);
394 
395         // move by 10,15
396         mRecyclerView.onTouchEvent(
397                 MotionEvent.obtain(time, time, MotionEvent.ACTION_MOVE, 40, 455, 0));
398         assertEquals(10, mRecyclerView.mPrefetchRegistry.mPrefetchDx);
399         assertEquals(15, mRecyclerView.mPrefetchRegistry.mPrefetchDy);
400 
401         // move by 0,0 - IGNORED
402         mRecyclerView.onTouchEvent(
403                 MotionEvent.obtain(time, time, MotionEvent.ACTION_MOVE, 40, 455, 0));
404         assertEquals(10, mRecyclerView.mPrefetchRegistry.mPrefetchDx); // same as prev
405         assertEquals(15, mRecyclerView.mPrefetchRegistry.mPrefetchDy); // same as prev
406     }
407 
408     @Test
prefetchItemsRespectDeadline()409     public void prefetchItemsRespectDeadline() {
410         mRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 3));
411 
412         // 100x100 pixel views
413         RecyclerView.Adapter adapter = new RecyclerView.Adapter() {
414             @Override
415             public RecyclerView.ViewHolder onCreateViewHolder(
416                     @NonNull ViewGroup parent, int viewType) {
417                 mRecyclerView.registerTimePassingMs(5);
418                 View view = new View(getContext());
419                 view.setMinimumWidth(100);
420                 view.setMinimumHeight(100);
421                 return new RecyclerView.ViewHolder(view) {};
422             }
423 
424             @Override
425             public void onBindViewHolder(
426                     RecyclerView.@NonNull ViewHolder holder, int position) {
427                 mRecyclerView.registerTimePassingMs(5);
428             }
429 
430             @Override
431             public int getItemCount() {
432                 return 100;
433             }
434         };
435         mRecyclerView.setAdapter(adapter);
436 
437         layout(300, 300);
438 
439         // offset scroll so that no prefetch-able views are directly adjacent to viewport
440         mRecyclerView.scrollBy(0, 50);
441 
442         assertTrue(mRecycler.mCachedViews.size() == 0);
443         assertTrue(mRecyclerView.getRecycledViewPool().getRecycledViewCount(0) == 0);
444 
445         // Should take 15 ms to inflate, bind, inflate, so give 19 to be safe
446         final long deadlineNs = mRecyclerView.getNanoTime() + TimeUnit.MILLISECONDS.toNanos(19);
447 
448         // Timed prefetch
449         mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
450         mRecyclerView.mGapWorker.prefetch(deadlineNs);
451 
452         // will have enough time to inflate/bind one view, and inflate another
453         assertTrue(mRecycler.mCachedViews.size() == 1);
454         assertTrue(mRecyclerView.getRecycledViewPool().getRecycledViewCount(0) == 1);
455         // Note: order/view below is an implementation detail
456         CacheUtils.verifyCacheContainsPositions(mRecyclerView, 12);
457 
458 
459         // Unbounded prefetch this time
460         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
461 
462         // Should finish all work
463         assertTrue(mRecycler.mCachedViews.size() == 3);
464         assertTrue(mRecyclerView.getRecycledViewPool().getRecycledViewCount(0) == 0);
465         CacheUtils.verifyCacheContainsPositions(mRecyclerView, 12, 13, 14);
466     }
467 
468     @Test
partialPrefetchAvoidsViewRecycledCallback()469     public void partialPrefetchAvoidsViewRecycledCallback() {
470         mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
471 
472         // 100x100 pixel views
473         RecyclerView.Adapter adapter = new RecyclerView.Adapter() {
474             @Override
475             public RecyclerView.ViewHolder onCreateViewHolder(
476                     @NonNull ViewGroup parent, int viewType) {
477                 mRecyclerView.registerTimePassingMs(5);
478                 View view = new View(getContext());
479                 view.setMinimumWidth(100);
480                 view.setMinimumHeight(100);
481                 return new RecyclerView.ViewHolder(view) {};
482             }
483 
484             @Override
485             public void onBindViewHolder(RecyclerView.@NonNull ViewHolder holder, int position) {
486                 mRecyclerView.registerTimePassingMs(5);
487             }
488 
489             @Override
490             public int getItemCount() {
491                 return 100;
492             }
493 
494             @Override
495             public void onViewRecycled(RecyclerView.@NonNull ViewHolder holder) {
496                 // verify unbound view doesn't get
497                 assertNotEquals(RecyclerView.NO_POSITION, holder.getAbsoluteAdapterPosition());
498             }
499         };
500         mRecyclerView.setAdapter(adapter);
501 
502         layout(100, 300);
503 
504         // offset scroll so that no prefetch-able views are directly adjacent to viewport
505         mRecyclerView.scrollBy(0, 50);
506 
507         assertTrue(mRecycler.mCachedViews.size() == 0);
508         assertTrue(mRecyclerView.getRecycledViewPool().getRecycledViewCount(0) == 0);
509 
510         // Should take 10 ms to inflate + bind, so just give it 9 so it doesn't have time to bind
511         final long deadlineNs = mRecyclerView.getNanoTime() + TimeUnit.MILLISECONDS.toNanos(9);
512 
513         // Timed prefetch
514         mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
515         mRecyclerView.mGapWorker.prefetch(deadlineNs);
516 
517         // will have enough time to inflate but not bind one view
518         assertTrue(mRecycler.mCachedViews.size() == 0);
519         assertTrue(mRecyclerView.getRecycledViewPool().getRecycledViewCount(0) == 1);
520         RecyclerView.ViewHolder pooledHolder = mRecyclerView.getRecycledViewPool()
521                 .mScrap.get(0).mScrapHeap.get(0);
522         assertEquals(RecyclerView.NO_POSITION, pooledHolder.getAbsoluteAdapterPosition());
523     }
524 
525     @Test
prefetchStaggeredItemsPriority()526     public void prefetchStaggeredItemsPriority() {
527         StaggeredGridLayoutManager sglm =
528                 new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
529         mRecyclerView.setLayoutManager(sglm);
530 
531         // first view 50x100 pixels, rest are 100x100 so second column is offset
532         mRecyclerView.setAdapter(new RecyclerView.Adapter() {
533             @Override
534             public RecyclerView.@NonNull ViewHolder onCreateViewHolder(
535                     @NonNull ViewGroup parent, int viewType) {
536                 return new RecyclerView.ViewHolder(new View(getContext())) {};
537             }
538 
539             @Override
540             public void onBindViewHolder(RecyclerView.@NonNull ViewHolder holder, int position) {
541                 holder.itemView.setMinimumWidth(100);
542                 holder.itemView.setMinimumHeight(position == 0 ? 50 : 100);
543             }
544 
545             @Override
546             public int getItemCount() {
547                 return 100;
548             }
549         });
550 
551         layout(200, 200);
552 
553         /* Each row is 50 pixels:
554          * ------------- *
555          *   0   |   1   *
556          *   2   |   1   *
557          *   2   |   3   *
558          *___4___|___3___*
559          *   4   |   5   *
560          *   6   |   5   *
561          *      ...      *
562          */
563         assertEquals(5, mRecyclerView.getChildCount());
564         assertEquals(0, sglm.getFirstChildPosition());
565         assertEquals(4, sglm.getLastChildPosition());
566 
567         // prefetching down shows 5 at 0 pixels away, 6 at 50 pixels away
568         CacheUtils.verifyPositionsPrefetched(mRecyclerView, 0, 10,
569                 new Integer[] {5, 0}, new Integer[] {6, 50});
570 
571         // Prefetch upward shows nothing
572         CacheUtils.verifyPositionsPrefetched(mRecyclerView, 0, -10);
573 
574         mRecyclerView.scrollBy(0, 100);
575 
576         /* Each row is 50 pixels:
577          * ------------- *
578          *   0   |   1   *
579          *___2___|___1___*
580          *   2   |   3   *
581          *   4   |   3   *
582          *   4   |   5   *
583          *___6___|___5___*
584          *   6   |   7   *
585          *   8   |   7   *
586          *      ...      *
587          */
588 
589         assertEquals(5, mRecyclerView.getChildCount());
590         assertEquals(2, sglm.getFirstChildPosition());
591         assertEquals(6, sglm.getLastChildPosition());
592 
593         // prefetching down shows 7 at 0 pixels away, 8 at 50 pixels away
594         CacheUtils.verifyPositionsPrefetched(mRecyclerView, 0, 10,
595                 new Integer[] {7, 0}, new Integer[] {8, 50});
596 
597         // prefetching up shows 1 is 0 pixels away, 0 at 50 pixels away
598         CacheUtils.verifyPositionsPrefetched(mRecyclerView, 0, -10,
599                 new Integer[] {1, 0}, new Integer[] {0, 50});
600     }
601 
602     @Test
prefetchStaggeredPastBoundary()603     public void prefetchStaggeredPastBoundary() {
604         StaggeredGridLayoutManager sglm =
605                 new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
606         mRecyclerView.setLayoutManager(sglm);
607         mRecyclerView.setAdapter(new RecyclerView.Adapter() {
608             @Override
609             public RecyclerView.@NonNull ViewHolder onCreateViewHolder(
610                     @NonNull ViewGroup parent, int viewType) {
611                 return new RecyclerView.ViewHolder(new View(getContext())) {};
612             }
613 
614             @Override
615             public void onBindViewHolder(RecyclerView.@NonNull ViewHolder holder, int position) {
616                 holder.itemView.setMinimumWidth(100);
617                 holder.itemView.setMinimumHeight(position == 0 ? 100 : 200);
618             }
619 
620             @Override
621             public int getItemCount() {
622                 return 2;
623             }
624         });
625 
626         layout(200, 100);
627         mRecyclerView.scrollBy(0, 50);
628 
629         /* Each row is 50 pixels:
630          * ------------- *
631          *___0___|___1___*
632          *   0   |   1   *
633          *_______|___1___*
634          *       |   1   *
635          */
636         assertEquals(2, mRecyclerView.getChildCount());
637         assertEquals(0, sglm.getFirstChildPosition());
638         assertEquals(1, sglm.getLastChildPosition());
639 
640         // prefetch upward gets nothing
641         CacheUtils.verifyPositionsPrefetched(mRecyclerView, 0, -10);
642 
643         // prefetch downward gets nothing (and doesn't crash...)
644         CacheUtils.verifyPositionsPrefetched(mRecyclerView, 0, 10);
645     }
646 
647     @Test
prefetchItemsSkipAnimations()648     public void prefetchItemsSkipAnimations() {
649         LinearLayoutManager llm = new LinearLayoutManager(getContext());
650         mRecyclerView.setLayoutManager(llm);
651         final int[] expandedPosition = new int[] {-1};
652 
653         final RecyclerView.Adapter adapter = new RecyclerView.Adapter() {
654             @Override
655             public RecyclerView.ViewHolder onCreateViewHolder(
656                     @NonNull ViewGroup parent, int viewType) {
657                 return new RecyclerView.ViewHolder(new View(parent.getContext())) {};
658             }
659 
660             @Override
661             public void onBindViewHolder(RecyclerView.@NonNull ViewHolder holder, int position) {
662                 int height = expandedPosition[0] == position ? 400 : 100;
663                 holder.itemView.setLayoutParams(new RecyclerView.LayoutParams(200, height));
664             }
665 
666             @Override
667             public int getItemCount() {
668                 return 10;
669             }
670         };
671 
672         // make move duration long enough to be able to see the effects
673         RecyclerView.ItemAnimator itemAnimator = mRecyclerView.getItemAnimator();
674         itemAnimator.setMoveDuration(10000);
675         mRecyclerView.setAdapter(adapter);
676 
677         layout(200, 400);
678 
679         expandedPosition[0] = 1;
680         // insert payload to avoid creating a new view
681         adapter.notifyItemChanged(1, new Object());
682 
683         layout(200, 400);
684         layout(200, 400);
685 
686         assertTrue(itemAnimator.isRunning());
687         assertEquals(2, llm.getChildCount());
688         assertEquals(4, mRecyclerView.getChildCount());
689 
690         // animating view should be observable as hidden, uncached...
691         CacheUtils.verifyCacheDoesNotContainPositions(mRecyclerView, 2);
692         assertNotNull("Animating view should be found, hidden",
693                 mRecyclerView.mChildHelper.findHiddenNonRemovedView(2));
694         assertTrue(GapWorker.isPrefetchPositionAttached(mRecyclerView, 2));
695 
696         // ...but must not be removed for prefetch
697         mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
698         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
699 
700         assertEquals("Prefetch must target one view", 1, mRecyclerView.mPrefetchRegistry.mCount);
701         int prefetchTarget = mRecyclerView.mPrefetchRegistry.mPrefetchArray[0];
702         assertEquals("Prefetch must target view 2", 2, prefetchTarget);
703 
704         // animating view still observable as hidden, uncached
705         CacheUtils.verifyCacheDoesNotContainPositions(mRecyclerView, 2);
706         assertNotNull("Animating view should be found, hidden",
707                 mRecyclerView.mChildHelper.findHiddenNonRemovedView(2));
708         assertTrue(GapWorker.isPrefetchPositionAttached(mRecyclerView, 2));
709 
710         assertTrue(itemAnimator.isRunning());
711         assertEquals(2, llm.getChildCount());
712         assertEquals(4, mRecyclerView.getChildCount());
713     }
714 
715     @Test
viewHolderFindsNestedRecyclerViews()716     public void viewHolderFindsNestedRecyclerViews() {
717         LinearLayoutManager llm = new LinearLayoutManager(getContext());
718         mRecyclerView.setLayoutManager(llm);
719 
720         RecyclerView.Adapter mockAdapter = mock(RecyclerView.Adapter.class);
721         when(mockAdapter.onCreateViewHolder(any(ViewGroup.class), anyInt()))
722                 .thenAnswer(new Answer<RecyclerView.ViewHolder>() {
723                     @Override
724                     public RecyclerView.ViewHolder answer(InvocationOnMock invocation)
725                             throws Throwable {
726                         View view = new RecyclerView(getContext());
727                         view.setLayoutParams(new RecyclerView.LayoutParams(100, 100));
728                         return new RecyclerView.ViewHolder(view) {};
729                     }
730                 });
731         when(mockAdapter.getItemCount()).thenReturn(100);
732         mRecyclerView.setAdapter(mockAdapter);
733 
734         layout(100, 200);
735 
736         verify(mockAdapter, times(2)).onCreateViewHolder(any(ViewGroup.class), anyInt());
737         verify(mockAdapter, times(2)).onBindViewHolder(
738                 argThat(new ArgumentMatcher<RecyclerView.ViewHolder>() {
739                     @Override
740                     public boolean matches(RecyclerView.ViewHolder holder) {
741                         return holder.itemView == holder.mNestedRecyclerView.get();
742                     }
743                 }),
744                 anyInt(),
745                 any(List.class));
746     }
747 
748     class InnerAdapter extends RecyclerView.Adapter<InnerAdapter.ViewHolder> {
749         private static final int INNER_ITEM_COUNT = 20;
750         int mItemsBound = 0;
751 
752         class ViewHolder extends RecyclerView.ViewHolder {
ViewHolder(View itemView)753             ViewHolder(View itemView) {
754                 super(itemView);
755             }
756         }
757 
InnerAdapter()758         InnerAdapter() {}
759 
760         @Override
onCreateViewHolder(@onNull ViewGroup parent, int viewType)761         public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
762             mRecyclerView.registerTimePassingMs(5);
763             View view = new View(parent.getContext());
764             view.setLayoutParams(new RecyclerView.LayoutParams(100, 100));
765             return new ViewHolder(view);
766         }
767 
768         @Override
onBindViewHolder(@onNull ViewHolder holder, int position)769         public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
770             mRecyclerView.registerTimePassingMs(5);
771             mItemsBound++;
772         }
773 
774         @Override
getItemCount()775         public int getItemCount() {
776             return INNER_ITEM_COUNT;
777         }
778     }
779 
780     class OuterAdapter extends RecyclerView.Adapter<OuterAdapter.ViewHolder> {
781 
782         private boolean mReverseInner;
783 
784         class ViewHolder extends RecyclerView.ViewHolder {
785             private final RecyclerView mRecyclerView;
ViewHolder(RecyclerView itemView)786             ViewHolder(RecyclerView itemView) {
787                 super(itemView);
788                 mRecyclerView = itemView;
789             }
790         }
791 
792         ArrayList<InnerAdapter> mAdapters = new ArrayList<>();
793         ArrayList<Parcelable> mSavedStates = new ArrayList<>();
794         RecyclerView.RecycledViewPool mSharedPool = new RecyclerView.RecycledViewPool();
795 
OuterAdapter()796         OuterAdapter() {
797             this(false);
798         }
799 
OuterAdapter(boolean reverseInner)800         OuterAdapter(boolean reverseInner) {
801             this(reverseInner, 10);
802         }
803 
OuterAdapter(boolean reverseInner, int itemCount)804         OuterAdapter(boolean reverseInner, int itemCount) {
805             mReverseInner = reverseInner;
806             for (int i = 0; i < itemCount; i++) {
807                 mAdapters.add(new InnerAdapter());
808                 mSavedStates.add(null);
809             }
810         }
811 
addItem()812         void addItem() {
813             int index = getItemCount();
814             mAdapters.add(new InnerAdapter());
815             mSavedStates.add(null);
816             notifyItemInserted(index);
817         }
818 
819         @Override
onCreateViewHolder(@onNull ViewGroup parent, int viewType)820         public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
821             mRecyclerView.registerTimePassingMs(5);
822 
823             RecyclerView rv = new RecyclerView(parent.getContext()) {
824                 @Override
825                 public int getWindowVisibility() {
826                     // Pretend to be visible to avoid being filtered out
827                     return View.VISIBLE;
828                 }
829             };
830             rv.setLayoutManager(new LinearLayoutManager(parent.getContext(),
831                     LinearLayoutManager.HORIZONTAL, mReverseInner));
832             rv.setRecycledViewPool(mSharedPool);
833             rv.setLayoutParams(new RecyclerView.LayoutParams(200, 100));
834             return new ViewHolder(rv);
835         }
836 
837         @Override
onBindViewHolder(@onNull ViewHolder holder, int position)838         public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
839             mRecyclerView.registerTimePassingMs(5);
840 
841             // Tests may rely on bound holders not being shared between inner adapters,
842             // since we force recycle here
843             holder.mRecyclerView.swapAdapter(mAdapters.get(position), true);
844 
845             Parcelable savedState = mSavedStates.get(position);
846             if (savedState != null) {
847                 holder.mRecyclerView.getLayoutManager().onRestoreInstanceState(savedState);
848                 mSavedStates.set(position, null);
849             }
850         }
851 
852         @Override
onViewRecycled(@onNull ViewHolder holder)853         public void onViewRecycled(@NonNull ViewHolder holder) {
854             mSavedStates.set(holder.getAbsoluteAdapterPosition(),
855                     holder.mRecyclerView.getLayoutManager().onSaveInstanceState());
856         }
857 
858         @Override
getItemCount()859         public int getItemCount() {
860             return mAdapters.size();
861         }
862     }
863 
864     @Test
nestedPrefetchSimple()865     public void nestedPrefetchSimple() {
866         LinearLayoutManager llm = new LinearLayoutManager(getContext());
867         assertEquals(2, llm.getInitialPrefetchItemCount());
868 
869         mRecyclerView.setLayoutManager(llm);
870         mRecyclerView.setAdapter(new OuterAdapter());
871 
872         layout(200, 200);
873         mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
874 
875         // prefetch 2 (default)
876         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
877         RecyclerView.ViewHolder holder = CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 2);
878         assertNotNull(holder);
879         assertNotNull(holder.mNestedRecyclerView);
880         RecyclerView innerView = holder.mNestedRecyclerView.get();
881         CacheUtils.verifyCacheContainsPrefetchedPositions(innerView, 0, 1);
882 
883         // prefetch 4
884         ((LinearLayoutManager) innerView.getLayoutManager())
885                 .setInitialPrefetchItemCount(4);
886         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
887         CacheUtils.verifyCacheContainsPrefetchedPositions(innerView, 0, 1, 2, 3);
888     }
889 
890     @Test
nestedPrefetchNotClearInnerStructureChangeFlag()891     public void nestedPrefetchNotClearInnerStructureChangeFlag() {
892         LinearLayoutManager llm = new LinearLayoutManager(getContext());
893         assertEquals(2, llm.getInitialPrefetchItemCount());
894 
895         mRecyclerView.setLayoutManager(llm);
896         mRecyclerView.setAdapter(new OuterAdapter());
897 
898         layout(200, 200);
899         mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
900 
901         // prefetch 2 (default)
902         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
903         RecyclerView.ViewHolder holder = CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 2);
904         assertNotNull(holder);
905         assertNotNull(holder.mNestedRecyclerView);
906         RecyclerView innerView = holder.mNestedRecyclerView.get();
907         RecyclerView.Adapter innerAdapter = innerView.getAdapter();
908         CacheUtils.verifyCacheContainsPrefetchedPositions(innerView, 0, 1);
909         // mStructureChanged is initially true before first layout pass.
910         assertTrue(innerView.mState.mStructureChanged);
911         assertTrue(innerView.hasPendingAdapterUpdates());
912 
913         // layout position 2 and clear mStructureChanged
914         mRecyclerView.scrollToPosition(2);
915         layout(200, 200);
916         mRecyclerView.scrollToPosition(0);
917         layout(200, 200);
918         assertFalse(innerView.mState.mStructureChanged);
919         assertFalse(innerView.hasPendingAdapterUpdates());
920 
921         // notify change on the cached innerView.
922         innerAdapter.notifyDataSetChanged();
923         assertTrue(innerView.mState.mStructureChanged);
924         assertTrue(innerView.hasPendingAdapterUpdates());
925 
926         // prefetch again
927         mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
928         ((LinearLayoutManager) innerView.getLayoutManager())
929                 .setInitialPrefetchItemCount(2);
930         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
931         CacheUtils.verifyCacheContainsPrefetchedPositions(innerView, 0, 1);
932 
933         // The re-prefetch is not necessary get the same inner view but we will get same Adapter
934         holder = CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 2);
935         innerView = holder.mNestedRecyclerView.get();
936         assertSame(innerAdapter, innerView.getAdapter());
937         // prefetch shouldn't clear the mStructureChanged flag
938         assertTrue(innerView.mState.mStructureChanged);
939         assertTrue(innerView.hasPendingAdapterUpdates());
940     }
941 
942     @Test
nestedPrefetchReverseInner()943     public void nestedPrefetchReverseInner() {
944         mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
945         mRecyclerView.setAdapter(new OuterAdapter(/* reverseInner = */ true));
946 
947         layout(200, 200);
948         mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
949 
950         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
951         RecyclerView.ViewHolder holder = CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 2);
952 
953         // anchor from right side, should see last two positions
954         CacheUtils.verifyCacheContainsPrefetchedPositions(holder.mNestedRecyclerView.get(), 18, 19);
955     }
956 
957     @Test
nestedPrefetchOffset()958     public void nestedPrefetchOffset() {
959         mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
960         mRecyclerView.setAdapter(new OuterAdapter());
961 
962         layout(200, 200);
963 
964         // Scroll top row by 5.5 items, verify positions 5, 6, 7 showing
965         RecyclerView inner = (RecyclerView) mRecyclerView.getChildAt(0);
966         inner.scrollBy(550, 0);
967         assertEquals(5, RecyclerView.getChildViewHolderInt(inner.getChildAt(0)).mPosition);
968         assertEquals(6, RecyclerView.getChildViewHolderInt(inner.getChildAt(1)).mPosition);
969         assertEquals(7, RecyclerView.getChildViewHolderInt(inner.getChildAt(2)).mPosition);
970 
971         // scroll down 4 rows, up 3 so row 0 is adjacent but uncached
972         mRecyclerView.scrollBy(0, 400);
973         mRecyclerView.scrollBy(0, -300);
974 
975         // top row no longer present
976         CacheUtils.verifyCacheDoesNotContainPositions(mRecyclerView, 0);
977 
978         // prefetch upward, and validate that we've gotten the top row with correct offsets
979         mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, -1);
980         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
981         inner = (RecyclerView) CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 0).itemView;
982         CacheUtils.verifyCacheContainsPrefetchedPositions(inner, 5, 6);
983 
984         // prefetch 4
985         ((LinearLayoutManager) inner.getLayoutManager()).setInitialPrefetchItemCount(4);
986         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
987         CacheUtils.verifyCacheContainsPrefetchedPositions(inner, 5, 6, 7, 8);
988     }
989 
990     @Test
nestedPrefetchNotReset()991     public void nestedPrefetchNotReset() {
992         mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
993         OuterAdapter outerAdapter = new OuterAdapter();
994         mRecyclerView.setAdapter(outerAdapter);
995 
996         layout(200, 200);
997 
998         mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
999 
1000         // prefetch row 2, items 0 & 1
1001         assertEquals(0, outerAdapter.mAdapters.get(2).mItemsBound);
1002         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
1003         RecyclerView.ViewHolder holder = CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 2);
1004         RecyclerView innerRecyclerView = holder.mNestedRecyclerView.get();
1005 
1006         assertNotNull(innerRecyclerView);
1007         CacheUtils.verifyCacheContainsPrefetchedPositions(innerRecyclerView, 0, 1);
1008         assertEquals(2, outerAdapter.mAdapters.get(2).mItemsBound);
1009 
1010         // new row comes on, triggers layout...
1011         mRecyclerView.scrollBy(0, 50);
1012 
1013         // ... which shouldn't require new items to be bound,
1014         // as prefetch has already done that work
1015         assertEquals(2, outerAdapter.mAdapters.get(2).mItemsBound);
1016     }
1017 
validateRvChildrenValid(RecyclerView recyclerView, int childCount)1018     static void validateRvChildrenValid(RecyclerView recyclerView, int childCount) {
1019         ChildHelper childHelper = recyclerView.mChildHelper;
1020 
1021         assertEquals(childCount, childHelper.getUnfilteredChildCount());
1022         for (int i = 0; i < childHelper.getUnfilteredChildCount(); i++) {
1023             assertFalse(recyclerView.getChildViewHolder(
1024                     childHelper.getUnfilteredChildAt(i)).isInvalid());
1025         }
1026     }
1027 
1028     @Test
nestedPrefetchCacheNotTouched()1029     public void nestedPrefetchCacheNotTouched() {
1030         mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
1031         OuterAdapter outerAdapter = new OuterAdapter();
1032         mRecyclerView.setAdapter(outerAdapter);
1033 
1034         layout(200, 200);
1035         mRecyclerView.scrollBy(0, 100);
1036 
1037         // item 0 is cached
1038         assertEquals(2, outerAdapter.mAdapters.get(0).mItemsBound);
1039         RecyclerView.ViewHolder holder = CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 0);
1040         validateRvChildrenValid(holder.mNestedRecyclerView.get(), 2);
1041 
1042         // try and prefetch it
1043         mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, -1);
1044         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
1045 
1046         // make sure cache's inner items aren't rebound unnecessarily
1047         assertEquals(2, outerAdapter.mAdapters.get(0).mItemsBound);
1048         validateRvChildrenValid(holder.mNestedRecyclerView.get(), 2);
1049     }
1050 
1051     @Test
nestedRemoveAnimatingView()1052     public void nestedRemoveAnimatingView() {
1053         mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
1054         OuterAdapter outerAdapter = new OuterAdapter(false, 1);
1055         mRecyclerView.setAdapter(outerAdapter);
1056         mRecyclerView.getItemAnimator().setAddDuration(TimeUnit.MILLISECONDS.toNanos(30));
1057 
1058         layout(200, 200);
1059 
1060         // Insert 3 items - only first one in viewport, so only it animates
1061         for (int i = 0; i < 3; i++) {
1062             outerAdapter.addItem();
1063         }
1064         layout(200, 200); // layout again to kick off animation
1065 
1066 
1067         // item 1 is animating, so scroll it out of viewport
1068         mRecyclerView.scrollBy(0, 200);
1069 
1070         // 2 items attached, 2 cached
1071         assertEquals(2, mRecyclerView.mChildHelper.getUnfilteredChildCount());
1072         assertEquals(2, mRecycler.mCachedViews.size());
1073         CacheUtils.verifyCacheContainsPositions(mRecyclerView, 0, 1);
1074         assertEquals(0, mRecyclerView.getRecycledViewPool().getRecycledViewCount(0));
1075 
1076         // The animation should automatically stop when removing the addition view
1077         assertFalse(mRecyclerView.getItemAnimator().isRunning());
1078 
1079         for (RecyclerView.ViewHolder viewHolder : mRecycler.mCachedViews) {
1080             assertNotNull(viewHolder.mNestedRecyclerView);
1081         }
1082     }
1083 
1084     @Test
nestedExpandCacheCorrectly()1085     public void nestedExpandCacheCorrectly() {
1086         final int DEFAULT_CACHE_SIZE = RecyclerView.Recycler.DEFAULT_CACHE_SIZE;
1087 
1088         mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
1089         OuterAdapter outerAdapter = new OuterAdapter();
1090         mRecyclerView.setAdapter(outerAdapter);
1091 
1092         layout(200, 200);
1093 
1094         mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
1095         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
1096 
1097         // after initial prefetch, view cache max expanded by number of inner items prefetched (2)
1098         RecyclerView.ViewHolder holder = CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 2);
1099         RecyclerView innerView = holder.mNestedRecyclerView.get();
1100         assertTrue(innerView.getLayoutManager().mPrefetchMaxObservedInInitialPrefetch);
1101         assertEquals(2, innerView.getLayoutManager().mPrefetchMaxCountObserved);
1102         assertEquals(2 + DEFAULT_CACHE_SIZE, innerView.mRecycler.mViewCacheMax);
1103 
1104         try {
1105             // Note: As a hack, we not only must manually dispatch attachToWindow(), but we
1106             // also have to be careful to call innerView.mGapWorker below. mRecyclerView.mGapWorker
1107             // is registered to the wrong thread, since @setup is called on a different thread
1108             // from @Test. Assert this, so this test can be fixed when setup == test thread.
1109             assertEquals(1, mRecyclerView.mGapWorker.mRecyclerViews.size());
1110             assertFalse(innerView.isAttachedToWindow());
1111             innerView.onAttachedToWindow();
1112 
1113             // bring prefetch view into viewport, at which point it shouldn't have cache expanded...
1114             mRecyclerView.scrollBy(0, 100);
1115             assertFalse(innerView.getLayoutManager().mPrefetchMaxObservedInInitialPrefetch);
1116             assertEquals(0, innerView.getLayoutManager().mPrefetchMaxCountObserved);
1117             assertEquals(DEFAULT_CACHE_SIZE, innerView.mRecycler.mViewCacheMax);
1118 
1119             // until a valid horizontal prefetch caches an item, and expands view count by one
1120             innerView.mPrefetchRegistry.setPrefetchVector(1, 0);
1121             innerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS); // NB: must be innerView.mGapWorker
1122             assertFalse(innerView.getLayoutManager().mPrefetchMaxObservedInInitialPrefetch);
1123             assertEquals(1, innerView.getLayoutManager().mPrefetchMaxCountObserved);
1124             assertEquals(1 + DEFAULT_CACHE_SIZE, innerView.mRecycler.mViewCacheMax);
1125         } finally {
1126             if (innerView.isAttachedToWindow()) {
1127                 innerView.onDetachedFromWindow();
1128             }
1129         }
1130     }
1131 
1132     /**
1133      * Similar to OuterAdapter above, but uses notifyDataSetChanged() instead of set/swapAdapter
1134      * to update data for the inner RecyclerViews when containing ViewHolder is bound.
1135      */
1136     class OuterNotifyAdapter extends RecyclerView.Adapter<OuterNotifyAdapter.ViewHolder> {
1137         private static final int OUTER_ITEM_COUNT = 10;
1138 
1139         private boolean mReverseInner;
1140 
1141         class ViewHolder extends RecyclerView.ViewHolder {
1142             private final RecyclerView mRecyclerView;
1143             private final InnerAdapter mAdapter;
ViewHolder(RecyclerView itemView)1144             ViewHolder(RecyclerView itemView) {
1145                 super(itemView);
1146                 mRecyclerView = itemView;
1147                 mAdapter = new InnerAdapter();
1148                 mRecyclerView.setAdapter(mAdapter);
1149             }
1150         }
1151 
1152         ArrayList<Parcelable> mSavedStates = new ArrayList<>();
1153         RecyclerView.RecycledViewPool mSharedPool = new RecyclerView.RecycledViewPool();
1154 
OuterNotifyAdapter()1155         OuterNotifyAdapter() {
1156             this(false);
1157         }
1158 
OuterNotifyAdapter(boolean reverseInner)1159         OuterNotifyAdapter(boolean reverseInner) {
1160             mReverseInner = reverseInner;
1161             for (int i = 0; i <= OUTER_ITEM_COUNT; i++) {
1162                 mSavedStates.add(null);
1163             }
1164         }
1165 
1166         @Override
onCreateViewHolder(@onNull ViewGroup parent, int viewType)1167         public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
1168             mRecyclerView.registerTimePassingMs(5);
1169             RecyclerView rv = new RecyclerView(parent.getContext());
1170             rv.setLayoutManager(new LinearLayoutManager(parent.getContext(),
1171                     LinearLayoutManager.HORIZONTAL, mReverseInner));
1172             rv.setRecycledViewPool(mSharedPool);
1173             rv.setLayoutParams(new RecyclerView.LayoutParams(200, 100));
1174             return new ViewHolder(rv);
1175         }
1176 
1177         @Override
onBindViewHolder(@onNull ViewHolder holder, int position)1178         public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
1179             mRecyclerView.registerTimePassingMs(5);
1180             // if we had actual data to put into our adapter, this is where we'd do it...
1181 
1182             // ... then notify the adapter that it has new content:
1183             holder.mAdapter.notifyDataSetChanged();
1184 
1185             Parcelable savedState = mSavedStates.get(position);
1186             if (savedState != null) {
1187                 holder.mRecyclerView.getLayoutManager().onRestoreInstanceState(savedState);
1188                 mSavedStates.set(position, null);
1189             }
1190         }
1191 
1192         @Override
onViewRecycled(@onNull ViewHolder holder)1193         public void onViewRecycled(@NonNull ViewHolder holder) {
1194             if (holder.getAbsoluteAdapterPosition() >= 0) {
1195                 mSavedStates.set(holder.getAbsoluteAdapterPosition(),
1196                         holder.mRecyclerView.getLayoutManager().onSaveInstanceState());
1197             }
1198         }
1199 
1200         @Override
getItemCount()1201         public int getItemCount() {
1202             return OUTER_ITEM_COUNT;
1203         }
1204     }
1205 
1206     @Test
nestedPrefetchDiscardStaleChildren()1207     public void nestedPrefetchDiscardStaleChildren() {
1208         LinearLayoutManager llm = new LinearLayoutManager(getContext());
1209         assertEquals(2, llm.getInitialPrefetchItemCount());
1210 
1211         mRecyclerView.setLayoutManager(llm);
1212         OuterNotifyAdapter outerAdapter = new OuterNotifyAdapter();
1213         mRecyclerView.setAdapter(outerAdapter);
1214 
1215         // zero cache, so item we prefetch can't already be ready
1216         mRecyclerView.setItemViewCacheSize(0);
1217 
1218         // layout 3 items, then resize to 2...
1219         layout(200, 300);
1220         layout(200, 200);
1221 
1222         // so 1 item is evicted into the RecycledViewPool (bypassing cache)
1223         assertEquals(1, mRecycler.mRecyclerPool.getRecycledViewCount(0));
1224         assertEquals(0, mRecycler.mCachedViews.size());
1225 
1226         // This is a simple imitation of other behavior (namely, varied types in the outer adapter)
1227         // that results in the same initial state to test: items in the pool with attached children
1228         for (RecyclerView.ViewHolder holder : mRecycler.mRecyclerPool.mScrap.get(0).mScrapHeap) {
1229             // verify that children are attached and valid, since the RVs haven't been rebound
1230             assertNotNull(holder.mNestedRecyclerView);
1231             assertFalse(holder.mNestedRecyclerView.get().mDataSetHasChangedAfterLayout);
1232             validateRvChildrenValid(holder.mNestedRecyclerView.get(), 2);
1233         }
1234 
1235         // prefetch the outer item bind, but without enough time to do any inner binds
1236         final long deadlineNs = mRecyclerView.getNanoTime() + TimeUnit.MILLISECONDS.toNanos(9);
1237         mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
1238         mRecyclerView.mGapWorker.prefetch(deadlineNs);
1239 
1240         // 2 is prefetched without children
1241         CacheUtils.verifyCacheContainsPrefetchedPositions(mRecyclerView, 2);
1242         RecyclerView.ViewHolder holder = CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 2);
1243         assertNotNull(holder);
1244         assertNotNull(holder.mNestedRecyclerView);
1245         assertEquals(0, holder.mNestedRecyclerView.get().mChildHelper.getUnfilteredChildCount());
1246         assertEquals(0, holder.mNestedRecyclerView.get().mRecycler.mCachedViews.size());
1247 
1248         // but if we give it more time to bind items, it'll now acquire its inner items
1249         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
1250         CacheUtils.verifyCacheContainsPrefetchedPositions(holder.mNestedRecyclerView.get(), 0, 1);
1251     }
1252 
1253 
1254     @Test
nestedPrefetchDiscardStalePrefetch()1255     public void nestedPrefetchDiscardStalePrefetch() {
1256         mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
1257         OuterNotifyAdapter outerAdapter = new OuterNotifyAdapter();
1258         mRecyclerView.setAdapter(outerAdapter);
1259 
1260         // zero cache, so item we prefetch can't already be ready
1261         mRecyclerView.setItemViewCacheSize(0);
1262 
1263         // layout as 2x2, starting on row index 2, with empty cache
1264         layout(200, 200);
1265         mRecyclerView.scrollBy(0, 200);
1266 
1267         // no views cached, or previously used (so we can trust number in mItemsBound)
1268         mRecycler.mRecyclerPool.clear();
1269         assertEquals(0, mRecycler.mRecyclerPool.getRecycledViewCount(0));
1270         assertEquals(0, mRecycler.mCachedViews.size());
1271 
1272         // prefetch the outer item and its inner children
1273         mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
1274         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
1275 
1276         // 4 is prefetched with 2 inner children, first two binds
1277         CacheUtils.verifyCacheContainsPrefetchedPositions(mRecyclerView, 4);
1278         RecyclerView.ViewHolder holder = CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 4);
1279         assertNotNull(holder);
1280         assertNotNull(holder.mNestedRecyclerView);
1281         RecyclerView innerRecyclerView = holder.mNestedRecyclerView.get();
1282         assertEquals(0, innerRecyclerView.mChildHelper.getUnfilteredChildCount());
1283         assertEquals(2, innerRecyclerView.mRecycler.mCachedViews.size());
1284         assertEquals(2, ((InnerAdapter) innerRecyclerView.getAdapter()).mItemsBound);
1285 
1286         // notify data set changed, so any previously prefetched items invalid, and re-prefetch
1287         innerRecyclerView.getAdapter().notifyDataSetChanged();
1288         mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
1289         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
1290 
1291         // 4 is prefetched again...
1292         CacheUtils.verifyCacheContainsPrefetchedPositions(mRecyclerView, 4);
1293 
1294         // reusing the same instance with 2 inner children...
1295         assertSame(holder, CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 4));
1296         assertSame(innerRecyclerView, holder.mNestedRecyclerView.get());
1297         assertEquals(0, innerRecyclerView.mChildHelper.getUnfilteredChildCount());
1298         assertEquals(2, innerRecyclerView.mRecycler.mCachedViews.size());
1299 
1300         // ... but there should be two new binds
1301         assertEquals(4, ((InnerAdapter) innerRecyclerView.getAdapter()).mItemsBound);
1302     }
1303 
1304     @Test
setRecycledViewPool_followedByTwoSetAdapters_clearsRecycledViewPool()1305     public void setRecycledViewPool_followedByTwoSetAdapters_clearsRecycledViewPool() {
1306         RecyclerView.ViewHolder viewHolder = new RecyclerView.ViewHolder(new View(getContext())) {};
1307         viewHolder.mItemViewType = 123;
1308         RecyclerView.Adapter<?> adapter = mock(RecyclerView.Adapter.class);
1309         RecyclerView.RecycledViewPool recycledViewPool = new RecyclerView.RecycledViewPool();
1310         recycledViewPool.putRecycledView(viewHolder);
1311 
1312         mRecyclerView.setRecycledViewPool(recycledViewPool);
1313         mRecyclerView.setAdapter(adapter);
1314         mRecyclerView.setAdapter(adapter);
1315 
1316         assertThat(recycledViewPool.getRecycledViewCount(123), is(equalTo(0)));
1317     }
1318 
1319     @Test
setRecycledViewPool_followedByTwoSwapAdapters_doesntClearRecycledViewPool()1320     public void setRecycledViewPool_followedByTwoSwapAdapters_doesntClearRecycledViewPool() {
1321         RecyclerView.ViewHolder viewHolder = new RecyclerView.ViewHolder(new View(getContext())) {};
1322         viewHolder.mItemViewType = 123;
1323         RecyclerView.Adapter<?> adapter = mock(RecyclerView.Adapter.class);
1324         RecyclerView.RecycledViewPool recycledViewPool = new RecyclerView.RecycledViewPool();
1325         recycledViewPool.putRecycledView(viewHolder);
1326 
1327         mRecyclerView.setRecycledViewPool(recycledViewPool);
1328         mRecyclerView.swapAdapter(adapter, false);
1329         mRecyclerView.swapAdapter(adapter, false);
1330 
1331         assertThat(recycledViewPool.getRecycledViewCount(123), is(equalTo(1)));
1332     }
1333 }
1334