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