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.junit.Assert.assertEquals;
20 import static org.junit.Assert.assertFalse;
21 import static org.junit.Assert.assertNotEquals;
22 import static org.junit.Assert.assertNotNull;
23 import static org.junit.Assert.assertNull;
24 import static org.junit.Assert.assertSame;
25 import static org.junit.Assert.assertTrue;
26 import static org.junit.Assert.fail;
27 import static org.mockito.ArgumentMatchers.anyBoolean;
28 import static org.mockito.ArgumentMatchers.anyInt;
29 import static org.mockito.ArgumentMatchers.eq;
30 import static org.mockito.Mockito.mock;
31 import static org.mockito.Mockito.never;
32 import static org.mockito.Mockito.verify;
33 
34 import android.annotation.SuppressLint;
35 import android.content.Context;
36 import android.os.Build;
37 import android.os.Parcel;
38 import android.os.Parcelable;
39 import android.os.SystemClock;
40 import android.util.AttributeSet;
41 import android.util.SparseArray;
42 import android.view.LayoutInflater;
43 import android.view.MotionEvent;
44 import android.view.View;
45 import android.view.ViewGroup;
46 import android.view.animation.Interpolator;
47 import android.view.animation.LinearInterpolator;
48 import android.widget.FrameLayout;
49 import android.widget.LinearLayout;
50 import android.widget.TextView;
51 
52 import androidx.core.util.Pair;
53 import androidx.core.view.InputDeviceCompat;
54 import androidx.core.view.ScrollFeedbackProviderCompat;
55 import androidx.core.view.ViewCompat;
56 import androidx.recyclerview.test.R;
57 import androidx.test.core.app.ApplicationProvider;
58 import androidx.test.ext.junit.runners.AndroidJUnit4;
59 import androidx.test.filters.SdkSuppress;
60 import androidx.test.filters.SmallTest;
61 
62 import org.jspecify.annotations.NonNull;
63 import org.junit.Before;
64 import org.junit.Ignore;
65 import org.junit.Test;
66 import org.junit.runner.RunWith;
67 
68 import java.lang.ref.WeakReference;
69 import java.util.ArrayList;
70 import java.util.List;
71 import java.util.UUID;
72 
73 @SuppressWarnings("unchecked")
74 @SmallTest
75 @RunWith(AndroidJUnit4.class)
76 public class RecyclerViewBasicTest {
77 
78     RecyclerView mRecyclerView;
79 
80     ScrollFeedbackProviderCompat mScrollFeedbackProvider;
81 
82     @Before
setUp()83     public void setUp() throws Exception {
84         mRecyclerView = new RecyclerView(getContext());
85     }
86 
getContext()87     private Context getContext() {
88         return ApplicationProvider.getApplicationContext();
89     }
90 
91     @Test
measureWithoutLayoutManager()92     public void measureWithoutLayoutManager() {
93         measure();
94     }
95 
measure()96     private void measure() {
97         mRecyclerView.measure(View.MeasureSpec.AT_MOST | 320, View.MeasureSpec.AT_MOST | 240);
98     }
99 
layout()100     private void layout() {
101         mRecyclerView.layout(0, 0, 320, 320);
102     }
103 
focusSearch()104     private void focusSearch() {
105         mRecyclerView.focusSearch(1);
106     }
107 
108     @Test
layoutWithoutAdapter()109     public void layoutWithoutAdapter() throws InterruptedException {
110         MockLayoutManager layoutManager = new MockLayoutManager();
111         mRecyclerView.setLayoutManager(layoutManager);
112         layout();
113         assertEquals("layout manager should not be called if there is no adapter attached",
114                 0, layoutManager.mLayoutCount);
115     }
116 
117     @Test
118     @Ignore("b/236978861")
setScrollContainer()119     public void setScrollContainer() {
120         assertTrue("RecyclerView should announce itself as scroll container for the IME to "
121                 + "handle it properly", mRecyclerView.isScrollContainer());
122     }
123 
124     @Test
layoutWithoutLayoutManager()125     public void layoutWithoutLayoutManager() throws InterruptedException {
126         mRecyclerView.setAdapter(new MockAdapter(20));
127         measure();
128         layout();
129     }
130 
131     @Test
focusWithoutLayoutManager()132     public void focusWithoutLayoutManager() throws InterruptedException {
133         mRecyclerView.setAdapter(new MockAdapter(20));
134         measure();
135         layout();
136         focusSearch();
137     }
138 
139     @Test
scrollWithoutLayoutManager()140     public void scrollWithoutLayoutManager() throws InterruptedException {
141         mRecyclerView.setAdapter(new MockAdapter(20));
142         measure();
143         layout();
144         mRecyclerView.scrollBy(10, 10);
145     }
146 
147     @Test
smoothScrollWithoutLayoutManager()148     public void smoothScrollWithoutLayoutManager() throws InterruptedException {
149         mRecyclerView.setAdapter(new MockAdapter(20));
150         measure();
151         layout();
152         mRecyclerView.smoothScrollBy(10, 10);
153     }
154 
155     @Test
scrollToPositionWithoutLayoutManager()156     public void scrollToPositionWithoutLayoutManager() throws InterruptedException {
157         mRecyclerView.setAdapter(new MockAdapter(20));
158         measure();
159         layout();
160         mRecyclerView.scrollToPosition(5);
161     }
162 
163     @Test
164     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
scrollFeedbackCallbacks()165     public void scrollFeedbackCallbacks() {
166         mScrollFeedbackProvider = mock(ScrollFeedbackProviderCompat.class);
167         mRecyclerView.mScrollFeedbackProvider = mScrollFeedbackProvider;
168         mRecyclerView.setAdapter(new MockAdapter(20));
169         MockLayoutManager layoutManager = new MockLayoutManager();
170         mRecyclerView.setLayoutManager(layoutManager);
171         measure();
172         layout();
173 
174         MotionEvent ev = TouchUtils.createMotionEvent(
175                 /* inputDeviceId= */ 1,
176                 InputDeviceCompat.SOURCE_TOUCHSCREEN,
177                 MotionEvent.ACTION_MOVE,
178                 List.of(
179                     Pair.create(MotionEvent.AXIS_X, 10),
180                     Pair.create(MotionEvent.AXIS_Y, -20)));
181         layoutManager.mConsumedHorizontalScroll = 3;
182         layoutManager.mConsumedVerticalScroll = -20;
183         mRecyclerView.scrollByInternal(
184                 /* x= */ 10,
185                 /* y= */ -20,
186                 /* horizontalAxis= */ MotionEvent.AXIS_X,
187                 /* verticalAxis= */ MotionEvent.AXIS_Y,
188                 ev,
189                 ViewCompat.TYPE_TOUCH);
190 
191         // Verify onScrollProgress calls equating to the amount of consumed pixels on each axis.
192         verify(mScrollFeedbackProvider).onScrollProgress(
193                 /* inputDeviceId= */ 1, InputDeviceCompat.SOURCE_TOUCHSCREEN, MotionEvent.AXIS_X,
194                 /* deltaInPixels= */ 3);
195         verify(mScrollFeedbackProvider).onScrollProgress(
196                 /* inputDeviceId= */ 1, InputDeviceCompat.SOURCE_TOUCHSCREEN, MotionEvent.AXIS_Y,
197                 /* deltaInPixels= */ -20);
198         // Part of the X scroll was not consumed, so expect an onScrollLimit call.
199         verify(mScrollFeedbackProvider).onScrollLimit(
200                 /* inputDeviceId= */ 1, InputDeviceCompat.SOURCE_TOUCHSCREEN, MotionEvent.AXIS_X,
201                 /* isStart= */ false);
202         // All of the Y scroll was consumed. So expect no onScrollLimit call.
203         verify(mScrollFeedbackProvider, never()).onScrollLimit(
204                 /* inputDeviceId= */ anyInt(), anyInt(), eq(MotionEvent.AXIS_Y),
205                 /* isStart= */ eq(false));
206     }
207 
208     @Test
209     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
scrollFeedbackCallbacks_motionEventUnavailable()210     public void scrollFeedbackCallbacks_motionEventUnavailable() {
211         mScrollFeedbackProvider = mock(ScrollFeedbackProviderCompat.class);
212         mRecyclerView.mScrollFeedbackProvider = mScrollFeedbackProvider;
213         mRecyclerView.setAdapter(new MockAdapter(20));
214         MockLayoutManager layoutManager = new MockLayoutManager();
215         mRecyclerView.setLayoutManager(layoutManager);
216         measure();
217         layout();
218 
219         mRecyclerView.scrollByInternal(
220                 /* x= */ 10,
221                 /* y= */ -20,
222                 /* horizontalAxis= */ -1,
223                 /* verticalAxis= */ -1,
224                 null,
225                 ViewCompat.TYPE_TOUCH);
226 
227         verify(mScrollFeedbackProvider, never()).onScrollProgress(
228                 anyInt(), anyInt(), anyInt(), anyInt());
229         verify(mScrollFeedbackProvider, never()).onScrollLimit(
230                 anyInt(), anyInt(), anyInt(), anyBoolean());
231     }
232 
233     @Test
smoothScrollToPositionWithoutLayoutManager()234     public void smoothScrollToPositionWithoutLayoutManager() throws InterruptedException {
235         mRecyclerView.setAdapter(new MockAdapter(20));
236         measure();
237         layout();
238         mRecyclerView.smoothScrollToPosition(5);
239     }
240 
241     @Test
interceptTouchWithoutLayoutManager()242     public void interceptTouchWithoutLayoutManager() {
243         mRecyclerView.setAdapter(new MockAdapter(20));
244         measure();
245         layout();
246         assertFalse(mRecyclerView.onInterceptTouchEvent(
247                 MotionEvent.obtain(SystemClock.uptimeMillis(),
248                         SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, 10, 10, 0)));
249     }
250 
251     @Test
onTouchWithoutLayoutManager()252     public void onTouchWithoutLayoutManager() {
253         mRecyclerView.setAdapter(new MockAdapter(20));
254         measure();
255         layout();
256         assertFalse(mRecyclerView.onTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(),
257                 SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, 10, 10, 0)));
258     }
259 
260     @Test
layoutSimple()261     public void layoutSimple() throws InterruptedException {
262         MockLayoutManager layoutManager = new MockLayoutManager();
263         mRecyclerView.setLayoutManager(layoutManager);
264         mRecyclerView.setAdapter(new MockAdapter(3));
265         layout();
266         assertEquals("when both layout manager and activity is set, recycler view should call"
267                 + " layout manager's layout method", 1, layoutManager.mLayoutCount);
268     }
269 
270     @Test
observingAdapters()271     public void observingAdapters() {
272         MockAdapter adapterOld = new MockAdapter(1);
273         mRecyclerView.setAdapter(adapterOld);
274         assertTrue("attached adapter should have observables", adapterOld.hasObservers());
275 
276         MockAdapter adapterNew = new MockAdapter(2);
277         mRecyclerView.setAdapter(adapterNew);
278         assertFalse("detached adapter should lose observable", adapterOld.hasObservers());
279         assertTrue("new adapter should have observers", adapterNew.hasObservers());
280 
281         mRecyclerView.setAdapter(null);
282         assertNull("adapter should be removed successfully", mRecyclerView.getAdapter());
283         assertFalse("when adapter is removed, observables should be removed too",
284                 adapterNew.hasObservers());
285     }
286 
287     @Test
adapterChangeCallbacks()288     public void adapterChangeCallbacks() {
289         MockLayoutManager layoutManager = new MockLayoutManager();
290         mRecyclerView.setLayoutManager(layoutManager);
291         MockAdapter adapterOld = new MockAdapter(1);
292         mRecyclerView.setAdapter(adapterOld);
293         layoutManager.assertPrevNextAdapters(null, adapterOld);
294 
295         MockAdapter adapterNew = new MockAdapter(2);
296         mRecyclerView.setAdapter(adapterNew);
297         layoutManager.assertPrevNextAdapters("switching adapters should trigger correct callbacks"
298                 , adapterOld, adapterNew);
299 
300         mRecyclerView.setAdapter(null);
301         layoutManager.assertPrevNextAdapters(
302                 "Setting adapter null should trigger correct callbacks",
303                 adapterNew, null);
304     }
305 
306     @Test
recyclerOffsetsOnMove()307     public void recyclerOffsetsOnMove() {
308         MockLayoutManager  layoutManager = new MockLayoutManager();
309         final List<RecyclerView.ViewHolder> recycledVhs = new ArrayList<>();
310         mRecyclerView.setLayoutManager(layoutManager);
311         MockAdapter adapter = new MockAdapter(100) {
312             @Override
313             public void onViewRecycled(RecyclerView.@NonNull ViewHolder holder) {
314                 super.onViewRecycled(holder);
315                 recycledVhs.add(holder);
316             }
317         };
318         MockViewHolder mvh = new MockViewHolder(new TextView(getContext()));
319         mRecyclerView.setAdapter(adapter);
320         adapter.bindViewHolder(mvh, 20);
321         mRecyclerView.mRecycler.mCachedViews.add(mvh);
322         mRecyclerView.offsetPositionRecordsForRemove(10, 9, false);
323 
324         mRecyclerView.offsetPositionRecordsForRemove(11, 1, false);
325         assertEquals(1, recycledVhs.size());
326         assertSame(mvh, recycledVhs.get(0));
327     }
328 
329     @Test
recyclerOffsetsOnAdd()330     public void recyclerOffsetsOnAdd() {
331         MockLayoutManager  layoutManager = new MockLayoutManager();
332         final List<RecyclerView.ViewHolder> recycledVhs = new ArrayList<>();
333         mRecyclerView.setLayoutManager(layoutManager);
334         MockAdapter adapter = new MockAdapter(100) {
335             @Override
336             public void onViewRecycled(RecyclerView.@NonNull ViewHolder holder) {
337                 super.onViewRecycled(holder);
338                 recycledVhs.add(holder);
339             }
340         };
341         MockViewHolder mvh = new MockViewHolder(new TextView(getContext()));
342         mRecyclerView.setAdapter(adapter);
343         adapter.bindViewHolder(mvh, 20);
344         mRecyclerView.mRecycler.mCachedViews.add(mvh);
345         mRecyclerView.offsetPositionRecordsForRemove(10, 9, false);
346 
347         mRecyclerView.offsetPositionRecordsForInsert(15, 10);
348         assertEquals(11, mvh.mPosition);
349     }
350 
351     @Test
savedStateWithStatelessLayoutManager()352     public void savedStateWithStatelessLayoutManager() throws InterruptedException {
353         mRecyclerView.setLayoutManager(new MockLayoutManager() {
354             @Override
355             public Parcelable onSaveInstanceState() {
356                 return null;
357             }
358         });
359         mRecyclerView.setAdapter(new MockAdapter(3));
360         Parcel parcel = Parcel.obtain();
361         String parcelSuffix = UUID.randomUUID().toString();
362         Parcelable savedState = mRecyclerView.onSaveInstanceState();
363         savedState.writeToParcel(parcel, 0);
364         parcel.writeString(parcelSuffix);
365 
366         // reset position for reading
367         parcel.setDataPosition(0);
368         RecyclerView restored = new RecyclerView(getContext());
369         restored.setLayoutManager(new MockLayoutManager());
370         mRecyclerView.setAdapter(new MockAdapter(3));
371         // restore
372         savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel);
373         restored.onRestoreInstanceState(savedState);
374 
375         assertEquals("Parcel reading should not go out of bounds", parcelSuffix,
376                 parcel.readString());
377         assertEquals("When unmarshalling, all of the parcel should be read", 0, parcel.dataAvail());
378 
379     }
380 
381     @Test
savedState()382     public void savedState() throws InterruptedException {
383         MockLayoutManager mlm = new MockLayoutManager();
384         mRecyclerView.setLayoutManager(mlm);
385         mRecyclerView.setAdapter(new MockAdapter(3));
386         layout();
387         Parcelable savedState = mRecyclerView.onSaveInstanceState();
388         // we append a suffix to the parcelable to test out of bounds
389         String parcelSuffix = UUID.randomUUID().toString();
390         Parcel parcel = Parcel.obtain();
391         savedState.writeToParcel(parcel, 0);
392         parcel.writeString(parcelSuffix);
393 
394         // reset for reading
395         parcel.setDataPosition(0);
396         // re-create
397         savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel);
398 
399         RecyclerView restored = new RecyclerView(getContext());
400         mRecyclerView = restored;
401         MockLayoutManager mlmRestored = new MockLayoutManager();
402         restored.setLayoutManager(mlmRestored);
403         restored.setAdapter(new MockAdapter(3));
404         restored.onRestoreInstanceState(savedState);
405         layout();
406         assertEquals("Parcel reading should not go out of bounds", parcelSuffix,
407                 parcel.readString());
408         assertEquals("When unmarshalling, all of the parcel should be read", 0, parcel.dataAvail());
409         assertEquals("uuid in layout manager should be preserved properly", mlm.mUuid,
410                 mlmRestored.mUuid);
411     }
412 
413     @Test
dontSaveChildrenState()414     public void dontSaveChildrenState() throws InterruptedException {
415         MockLayoutManager mlm = new MockLayoutManager() {
416             @Override
417             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
418                 super.onLayoutChildren(recycler, state);
419                 View view = recycler.getViewForPosition(0);
420                 addView(view);
421                 measureChildWithMargins(view, 0, 0);
422                 view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
423             }
424         };
425         mRecyclerView.setLayoutManager(mlm);
426         mRecyclerView.setAdapter(new MockAdapter(3) {
427             @Override
428             public RecyclerView.@NonNull ViewHolder onCreateViewHolder(
429                     @NonNull ViewGroup parent, int viewType) {
430                 final LoggingView itemView = new LoggingView(parent.getContext());
431                 //noinspection ResourceType
432                 itemView.setId(3);
433                 return new MockViewHolder(itemView);
434             }
435         });
436         measure();
437         layout();
438         View view = mRecyclerView.getChildAt(0);
439         assertNotNull("Assumption check", view);
440         LoggingView loggingView = (LoggingView) view;
441         SparseArray<Parcelable> container = new SparseArray<Parcelable>();
442         mRecyclerView.saveHierarchyState(container);
443         assertEquals("children's save state method should not be called", 0,
444                 loggingView.getOnSavedInstanceCnt());
445     }
446 
447     @Test
smoothScrollBy_withCustomInterpolator_usesCustomInterpolator()448     public void smoothScrollBy_withCustomInterpolator_usesCustomInterpolator() {
449         mRecyclerView.setLayoutManager(new MockLayoutManager());
450         mRecyclerView.setAdapter(new MockAdapter(20));
451         Interpolator interpolator = new LinearInterpolator();
452         mRecyclerView.smoothScrollBy(0, 100, interpolator);
453         assertSame(interpolator, mRecyclerView.mViewFlinger.mInterpolator);
454     }
455 
456     @Test
smoothScrollBy_withoutCustomInterpolator_resetsToDefaultInterpolator()457     public void smoothScrollBy_withoutCustomInterpolator_resetsToDefaultInterpolator() {
458         mRecyclerView.setLayoutManager(new MockLayoutManager());
459         mRecyclerView.setAdapter(new MockAdapter(20));
460         Interpolator interpolator = new LinearInterpolator();
461         mRecyclerView.smoothScrollBy(0, 100, interpolator);
462 
463         mRecyclerView.smoothScrollBy(0, -100);
464 
465         assertSame(RecyclerView.sQuinticInterpolator, mRecyclerView.mViewFlinger.mInterpolator);
466     }
467 
468     @Test
fling_resetsInterpolatorToDefault()469     public void fling_resetsInterpolatorToDefault() {
470         mRecyclerView.setLayoutManager(new MockLayoutManager());
471         mRecyclerView.setAdapter(new MockAdapter(20));
472         Interpolator interpolator = new LinearInterpolator();
473         mRecyclerView.smoothScrollBy(0, 100, interpolator);
474 
475         mRecyclerView.fling(0, -1000);
476 
477         assertSame(RecyclerView.sQuinticInterpolator, mRecyclerView.mViewFlinger.mInterpolator);
478     }
479 
480     @Test
createAttachedException()481     public void createAttachedException() {
482         mRecyclerView.setAdapter(new RecyclerView.Adapter() {
483             @Override
484             public RecyclerView.@NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
485                     int viewType) {
486                 View view = LayoutInflater.from(parent.getContext())
487                         .inflate(R.layout.item_view, parent, true)
488                         .findViewById(R.id.item_view); // find child, since parent is returned
489                 return new RecyclerView.ViewHolder(view) {};
490             }
491 
492             @Override
493             public void onBindViewHolder(RecyclerView.@NonNull ViewHolder holder, int position) {
494                 fail("shouldn't get here, should throw during create");
495             }
496 
497             @Override
498             public int getItemCount() {
499                 return 1;
500             }
501         });
502         mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
503 
504         try {
505             measure();
506             //layout();
507             fail("IllegalStateException expected");
508         } catch (IllegalStateException e) {
509             // expected
510         }
511     }
512 
513     @Test
prefetchChangesCacheSize()514     public void prefetchChangesCacheSize() {
515         mRecyclerView.setAdapter(new MockAdapter(20));
516         MockLayoutManager mlm = new MockLayoutManager() {
517             @Override
518             public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state,
519                     RecyclerView.LayoutManager.LayoutPrefetchRegistry prefetchManager) {
520                 prefetchManager.addPosition(0, 0);
521                 prefetchManager.addPosition(1, 0);
522                 prefetchManager.addPosition(2, 0);
523             }
524         };
525 
526         RecyclerView.Recycler recycler = mRecyclerView.mRecycler;
527         assertEquals(RecyclerView.Recycler.DEFAULT_CACHE_SIZE, recycler.mViewCacheMax);
528         mRecyclerView.setLayoutManager(mlm);
529         assertEquals(RecyclerView.Recycler.DEFAULT_CACHE_SIZE, recycler.mViewCacheMax);
530 
531         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
532             // layout, so prefetches can occur
533             mRecyclerView.measure(View.MeasureSpec.EXACTLY | 100, View.MeasureSpec.EXACTLY | 100);
534             mRecyclerView.layout(0, 0, 100, 100);
535 
536             // prefetch gets 3 items, so expands cache by 3
537             mRecyclerView.mPrefetchRegistry.collectPrefetchPositionsFromView(mRecyclerView, false);
538             assertEquals(3, mRecyclerView.mPrefetchRegistry.mCount);
539             assertEquals(RecyclerView.Recycler.DEFAULT_CACHE_SIZE + 3, recycler.mViewCacheMax);
540 
541             // Reset to default by removing layout
542             mRecyclerView.setLayoutManager(null);
543             assertEquals(RecyclerView.Recycler.DEFAULT_CACHE_SIZE, recycler.mViewCacheMax);
544 
545             // And restore by restoring layout
546             mRecyclerView.setLayoutManager(mlm);
547             assertEquals(RecyclerView.Recycler.DEFAULT_CACHE_SIZE + 3, recycler.mViewCacheMax);
548         }
549     }
550 
551     @Test
getNanoTime()552     public void getNanoTime() throws InterruptedException {
553         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
554             // check that it looks vaguely time-ish
555             long time = mRecyclerView.getNanoTime();
556             assertNotEquals(0, time);
557 
558             // Sleep for 1 nano to ensure next call won't have the same measurement.
559             Thread.sleep(0, 1);
560             assertNotEquals(time, mRecyclerView.getNanoTime());
561         } else {
562             // expect to avoid cost of system.nanoTime on older platforms that don't do prefetch
563             assertEquals(0, mRecyclerView.getNanoTime());
564         }
565     }
566 
567     @Test
findNestedRecyclerView()568     public void findNestedRecyclerView() {
569         RecyclerView recyclerView = new RecyclerView(getContext());
570         assertEquals(recyclerView, RecyclerView.findNestedRecyclerView(recyclerView));
571 
572         ViewGroup parent = new FrameLayout(getContext());
573         assertEquals(null, RecyclerView.findNestedRecyclerView(parent));
574         parent.addView(recyclerView);
575         assertEquals(recyclerView, RecyclerView.findNestedRecyclerView(parent));
576 
577         ViewGroup grandParent = new FrameLayout(getContext());
578         assertEquals(null, RecyclerView.findNestedRecyclerView(grandParent));
579         grandParent.addView(parent);
580         assertEquals(recyclerView, RecyclerView.findNestedRecyclerView(grandParent));
581     }
582 
583     @Test
clearNestedRecyclerViewIfNotNested()584     public void clearNestedRecyclerViewIfNotNested() {
585         RecyclerView recyclerView = new RecyclerView(getContext());
586         ViewGroup parent = new FrameLayout(getContext());
587         parent.addView(recyclerView);
588         ViewGroup grandParent = new FrameLayout(getContext());
589         grandParent.addView(parent);
590 
591         // verify trivial noop case
592         RecyclerView.ViewHolder holder = new RecyclerView.ViewHolder(recyclerView) {};
593         holder.mNestedRecyclerView = new WeakReference<>(recyclerView);
594         RecyclerView.clearNestedRecyclerViewIfNotNested(holder);
595         assertEquals(recyclerView, holder.mNestedRecyclerView.get());
596 
597         // verify clear case
598         holder = new RecyclerView.ViewHolder(new View(getContext())) {};
599         holder.mNestedRecyclerView = new WeakReference<>(recyclerView);
600         RecyclerView.clearNestedRecyclerViewIfNotNested(holder);
601         assertNull(holder.mNestedRecyclerView);
602 
603         // verify more deeply nested case
604         holder = new RecyclerView.ViewHolder(grandParent) {};
605         holder.mNestedRecyclerView = new WeakReference<>(recyclerView);
606         RecyclerView.clearNestedRecyclerViewIfNotNested(holder);
607         assertEquals(recyclerView, holder.mNestedRecyclerView.get());
608     }
609 
610     @Test
exceptionContainsClasses()611     public void exceptionContainsClasses() {
612         RecyclerView first = new RecyclerView(getContext());
613         first.setLayoutManager(new LinearLayoutManager(getContext()));
614         first.setAdapter(new MockAdapter(10));
615 
616         RecyclerView second = new RecyclerView(getContext());
617         try {
618             second.setLayoutManager(first.getLayoutManager());
619             fail("exception expected");
620         } catch (IllegalArgumentException e) {
621             // Note: exception contains first RV
622             String m = e.getMessage();
623             assertTrue("must contain RV class", m.contains(RecyclerView.class.getName()));
624             assertTrue("must contain Adapter class", m.contains(MockAdapter.class.getName()));
625             assertTrue("must contain LM class", m.contains(LinearLayoutManager.class.getName()));
626             assertTrue("must contain ctx class", m.contains(getContext().getClass().getName()));
627         }
628     }
629 
630     @Test
focusOrderTest()631     public void focusOrderTest() {
632         FocusOrderAdapter focusAdapter = new FocusOrderAdapter(getContext());
633         mRecyclerView.setAdapter(focusAdapter);
634         mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
635         measure();
636         layout();
637 
638         View expected = focusAdapter.mBottomLeft;
639         assertEquals(expected, focusAdapter.mTopRight.focusSearch(View.FOCUS_FORWARD));
640 
641         expected = focusAdapter.mBottomRight;
642         assertSame(expected, focusAdapter.mBottomLeft.focusSearch(View.FOCUS_FORWARD));
643 
644         // we don't want looping within RecyclerView
645         assertNull(focusAdapter.mBottomRight.focusSearch(View.FOCUS_FORWARD));
646         assertNull(focusAdapter.mTopLeft.focusSearch(View.FOCUS_BACKWARD));
647     }
648 
649     @Test
setAdapter_callsCorrectLmMethods()650     public void setAdapter_callsCorrectLmMethods() throws Throwable {
651         MockLayoutManager mockLayoutManager = new MockLayoutManager();
652         MockAdapter mockAdapter = new MockAdapter(1);
653         mRecyclerView.setLayoutManager(mockLayoutManager);
654 
655         mRecyclerView.setAdapter(mockAdapter);
656         layout();
657 
658         assertEquals(1, mockLayoutManager.mAdapterChangedCount);
659         assertEquals(0, mockLayoutManager.mItemsChangedCount);
660     }
661 
662     @Test
swapAdapter_callsCorrectLmMethods()663     public void swapAdapter_callsCorrectLmMethods() throws Throwable {
664         MockLayoutManager mockLayoutManager = new MockLayoutManager();
665         MockAdapter mockAdapter = new MockAdapter(1);
666         mRecyclerView.setLayoutManager(mockLayoutManager);
667 
668         mRecyclerView.swapAdapter(mockAdapter, true);
669         layout();
670 
671         assertEquals(1, mockLayoutManager.mAdapterChangedCount);
672         assertEquals(1, mockLayoutManager.mItemsChangedCount);
673     }
674 
675     @Test
notifyDataSetChanged_callsCorrectLmMethods()676     public void notifyDataSetChanged_callsCorrectLmMethods() throws Throwable {
677         MockLayoutManager mockLayoutManager = new MockLayoutManager();
678         MockAdapter mockAdapter = new MockAdapter(1);
679         mRecyclerView.setLayoutManager(mockLayoutManager);
680         mRecyclerView.setAdapter(mockAdapter);
681         mockLayoutManager.mAdapterChangedCount = 0;
682         mockLayoutManager.mItemsChangedCount = 0;
683 
684         mockAdapter.notifyDataSetChanged();
685         layout();
686 
687         assertEquals(0, mockLayoutManager.mAdapterChangedCount);
688         assertEquals(1, mockLayoutManager.mItemsChangedCount);
689     }
690 
691     static class MockLayoutManager extends RecyclerView.LayoutManager {
692 
693         int mLayoutCount = 0;
694 
695         int mAdapterChangedCount = 0;
696         int mItemsChangedCount = 0;
697 
698         int mConsumedHorizontalScroll = Integer.MIN_VALUE;
699         int mConsumedVerticalScroll = Integer.MIN_VALUE;
700 
701         RecyclerView.Adapter mPrevAdapter;
702 
703         RecyclerView.Adapter mNextAdapter;
704 
705         String mUuid = UUID.randomUUID().toString();
706 
707         @Override
onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter)708         public void onAdapterChanged(RecyclerView.Adapter oldAdapter,
709                 RecyclerView.Adapter newAdapter) {
710             super.onAdapterChanged(oldAdapter, newAdapter);
711             mPrevAdapter = oldAdapter;
712             mNextAdapter = newAdapter;
713             mAdapterChangedCount++;
714         }
715 
716         @Override
onItemsChanged(RecyclerView recyclerView)717         public void onItemsChanged(RecyclerView recyclerView) {
718             mItemsChangedCount++;
719         }
720 
721         @Override
onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)722         public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
723             mLayoutCount += 1;
724         }
725 
726         @Override
onSaveInstanceState()727         public Parcelable onSaveInstanceState() {
728             LayoutManagerSavedState lss = new LayoutManagerSavedState();
729             lss.mUuid = mUuid;
730             return lss;
731         }
732 
733         @Override
onRestoreInstanceState(Parcelable state)734         public void onRestoreInstanceState(Parcelable state) {
735             super.onRestoreInstanceState(state);
736             if (state instanceof LayoutManagerSavedState) {
737                 mUuid = ((LayoutManagerSavedState) state).mUuid;
738             }
739         }
740 
741         @Override
generateDefaultLayoutParams()742         public RecyclerView.LayoutParams generateDefaultLayoutParams() {
743             return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
744                     ViewGroup.LayoutParams.WRAP_CONTENT);
745         }
746 
assertPrevNextAdapters(String message, RecyclerView.Adapter prevAdapter, RecyclerView.Adapter nextAdapter)747         public void assertPrevNextAdapters(String message, RecyclerView.Adapter prevAdapter,
748                 RecyclerView.Adapter nextAdapter) {
749             assertSame(message, prevAdapter, mPrevAdapter);
750             assertSame(message, nextAdapter, mNextAdapter);
751         }
752 
assertPrevNextAdapters(RecyclerView.Adapter prevAdapter, RecyclerView.Adapter nextAdapter)753         public void assertPrevNextAdapters(RecyclerView.Adapter prevAdapter,
754                 RecyclerView.Adapter nextAdapter) {
755             assertPrevNextAdapters("Adapters from onAdapterChanged callback should match",
756                     prevAdapter, nextAdapter);
757         }
758 
759         @Override
scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state)760         public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
761                 RecyclerView.State state) {
762             return mConsumedHorizontalScroll != Integer.MIN_VALUE ? mConsumedHorizontalScroll : dx;
763         }
764 
765         @Override
scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state)766         public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
767                 RecyclerView.State state) {
768             return mConsumedVerticalScroll != Integer.MIN_VALUE ? mConsumedVerticalScroll : dy;
769         }
770 
771         @Override
canScrollHorizontally()772         public boolean canScrollHorizontally() {
773             return true;
774         }
775 
776         @Override
canScrollVertically()777         public boolean canScrollVertically() {
778             return true;
779         }
780     }
781 
782     @SuppressLint("BanParcelableUsage")
783     static class LayoutManagerSavedState implements Parcelable {
784 
785         String mUuid;
786 
LayoutManagerSavedState(Parcel in)787         public LayoutManagerSavedState(Parcel in) {
788             mUuid = in.readString();
789         }
790 
LayoutManagerSavedState()791         public LayoutManagerSavedState() {
792 
793         }
794 
795         @Override
describeContents()796         public int describeContents() {
797             return 0;
798         }
799 
800         @Override
writeToParcel(Parcel dest, int flags)801         public void writeToParcel(Parcel dest, int flags) {
802             dest.writeString(mUuid);
803         }
804 
805         public static final Parcelable.Creator<LayoutManagerSavedState> CREATOR
806                 = new Parcelable.Creator<LayoutManagerSavedState>() {
807             @Override
808             public LayoutManagerSavedState createFromParcel(Parcel in) {
809                 return new LayoutManagerSavedState(in);
810             }
811 
812             @Override
813             public LayoutManagerSavedState[] newArray(int size) {
814                 return new LayoutManagerSavedState[size];
815             }
816         };
817     }
818 
819     static class MockAdapter extends RecyclerView.Adapter {
820 
821         private int mCount = 0;
822 
823 
MockAdapter(int count)824         MockAdapter(int count) {
825             this.mCount = count;
826         }
827 
828         @Override
onCreateViewHolder(@onNull ViewGroup parent, int viewType)829         public RecyclerView.@NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
830                 int viewType) {
831             return new MockViewHolder(new TextView(parent.getContext()));
832         }
833 
834         @Override
onBindViewHolder(RecyclerView.@onNull ViewHolder holder, int position)835         public void onBindViewHolder(RecyclerView.@NonNull ViewHolder holder, int position) {
836 
837         }
838 
839         @Override
getItemCount()840         public int getItemCount() {
841             return mCount;
842         }
843 
removeItems(int start, int count)844         void removeItems(int start, int count) {
845             mCount -= count;
846             notifyItemRangeRemoved(start, count);
847         }
848 
addItems(int start, int count)849         void addItems(int start, int count) {
850             mCount += count;
851             notifyItemRangeInserted(start, count);
852         }
853     }
854 
855     static class MockViewHolder extends RecyclerView.ViewHolder {
856         public Object mItem;
MockViewHolder(View itemView)857         public MockViewHolder(View itemView) {
858             super(itemView);
859         }
860     }
861 
862     static class LoggingView extends TextView {
863         private int mOnSavedInstanceCnt = 0;
864 
LoggingView(Context context)865         public LoggingView(Context context) {
866             super(context);
867         }
868 
LoggingView(Context context, AttributeSet attrs)869         public LoggingView(Context context, AttributeSet attrs) {
870             super(context, attrs);
871         }
872 
LoggingView(Context context, AttributeSet attrs, int defStyleAttr)873         public LoggingView(Context context, AttributeSet attrs, int defStyleAttr) {
874             super(context, attrs, defStyleAttr);
875         }
876 
877         @Override
onSaveInstanceState()878         public Parcelable onSaveInstanceState() {
879             mOnSavedInstanceCnt ++;
880             return super.onSaveInstanceState();
881         }
882 
getOnSavedInstanceCnt()883         public int getOnSavedInstanceCnt() {
884             return mOnSavedInstanceCnt;
885         }
886     }
887 
888     static class FocusOrderAdapter extends RecyclerView.Adapter {
889         TextView mTopLeft;
890         TextView mTopRight;
891         TextView mBottomLeft;
892         TextView mBottomRight;
893 
FocusOrderAdapter(Context context)894         FocusOrderAdapter(Context context) {
895             mTopLeft = new TextView(context);
896             mTopRight = new TextView(context);
897             mBottomLeft = new TextView(context);
898             mBottomRight = new TextView(context);
899             for (TextView tv : new TextView[]{mTopLeft, mTopRight, mBottomLeft, mBottomRight}) {
900                 tv.setFocusableInTouchMode(true);
901                 tv.setLayoutParams(new LinearLayout.LayoutParams(100, 100));
902             }
903             // create a scenario where the "first" focusable is to the right of the last one
904             mTopLeft.setFocusable(false);
905             mTopRight.getLayoutParams().width = 101;
906             mTopLeft.getLayoutParams().width = 101;
907         }
908 
909         @Override
onCreateViewHolder(@onNull ViewGroup parent, int viewType)910         public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
911             LinearLayout holder = new LinearLayout(parent.getContext());
912             holder.setOrientation(LinearLayout.HORIZONTAL);
913             return new MockViewHolder(holder);
914         }
915 
916         @Override
onBindViewHolder(RecyclerView.@onNull ViewHolder holder, int position)917         public void onBindViewHolder(RecyclerView.@NonNull ViewHolder holder, int position) {
918             LinearLayout l = (LinearLayout) holder.itemView;
919             l.removeAllViews();
920             if (position == 0) {
921                 l.addView(mTopLeft);
922                 l.addView(mTopRight);
923             } else {
924                 l.addView(mBottomLeft);
925                 l.addView(mBottomRight);
926             }
927         }
928 
929         @Override
getItemCount()930         public int getItemCount() {
931             return 2;
932         }
933 
removeItems(int start, int count)934         void removeItems(int start, int count) {
935         }
936 
addItems(int start, int count)937         void addItems(int start, int count) {
938         }
939     }
940 }
941