• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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 package android.support.v7.widget;
17 
18 import static org.hamcrest.CoreMatchers.instanceOf;
19 import static org.hamcrest.CoreMatchers.is;
20 import static org.hamcrest.CoreMatchers.not;
21 import static org.hamcrest.CoreMatchers.notNullValue;
22 import static org.hamcrest.CoreMatchers.sameInstance;
23 import static org.hamcrest.MatcherAssert.assertThat;
24 
25 import android.support.annotation.NonNull;
26 import android.support.annotation.Nullable;
27 import android.support.test.filters.MediumTest;
28 import android.support.v7.recyclerview.test.R;
29 import android.view.LayoutInflater;
30 import android.view.View;
31 import android.view.ViewGroup;
32 import android.widget.TextView;
33 
34 import org.junit.Test;
35 import org.junit.runner.RunWith;
36 import org.junit.runners.Parameterized;
37 
38 import java.util.Arrays;
39 import java.util.List;
40 import java.util.concurrent.atomic.AtomicLong;
41 
42 /**
43  * This class only tests the RV's focus recovery logic as focus moves between two views that
44  * represent the same item in the adapter. Keeping a focused view visible is up-to-the
45  * LayoutManager and all FW LayoutManagers already have tests for it.
46  */
47 @MediumTest
48 @RunWith(Parameterized.class)
49 public class RecyclerViewFocusRecoveryTest extends BaseRecyclerViewInstrumentationTest {
50     TestLayoutManager mLayoutManager;
51     TestAdapter mAdapter;
52     int mChildCount = 10;
53 
54     // Parameter indicating whether RV's children are simple views (false) or ViewGroups (true).
55     private final boolean mFocusOnChild;
56     // Parameter indicating whether RV recovers focus after layout is finished.
57     private final boolean mDisableRecovery;
58     // Parameter indicating whether animation is enabled for the ViewHolder items.
59     private final boolean mDisableAnimation;
60 
61     @Parameterized.Parameters(name = "focusSubChild:{0},disableRecovery:{1},"
62             + "disableAnimation:{2}")
getParams()63     public static List<Object[]> getParams() {
64         return Arrays.asList(
65                 new Object[]{false, false, true},
66                 new Object[]{true, false, true},
67                 new Object[]{false, true, true},
68                 new Object[]{true, true, true},
69                 new Object[]{false, false, false},
70                 new Object[]{true, false, false},
71                 new Object[]{false, true, false},
72                 new Object[]{true, true, false}
73         );
74     }
75 
RecyclerViewFocusRecoveryTest(boolean focusOnChild, boolean disableRecovery, boolean disableAnimation)76     public RecyclerViewFocusRecoveryTest(boolean focusOnChild, boolean disableRecovery,
77                                          boolean disableAnimation) {
78         super(false);
79         mFocusOnChild = focusOnChild;
80         mDisableRecovery = disableRecovery;
81         mDisableAnimation = disableAnimation;
82     }
83 
setupBasic()84     void setupBasic() throws Throwable {
85         setupBasic(false);
86     }
87 
setupBasic(boolean hasStableIds)88     void setupBasic(boolean hasStableIds) throws Throwable {
89         TestAdapter adapter = new FocusTestAdapter(mChildCount);
90         adapter.setHasStableIds(hasStableIds);
91         setupBasic(adapter, null);
92     }
93 
setupBasic(TestLayoutManager layoutManager)94     void setupBasic(TestLayoutManager layoutManager) throws Throwable {
95         setupBasic(null, layoutManager);
96     }
97 
setupBasic(TestAdapter adapter)98     void setupBasic(TestAdapter adapter) throws Throwable {
99         setupBasic(adapter, null);
100     }
101 
setupBasic(@ullable TestAdapter adapter, @Nullable TestLayoutManager layoutManager)102     void setupBasic(@Nullable TestAdapter adapter, @Nullable TestLayoutManager layoutManager)
103             throws Throwable {
104         RecyclerView recyclerView = new RecyclerView(getActivity());
105         if (layoutManager == null) {
106             layoutManager = new FocusLayoutManager();
107         }
108 
109         if (adapter == null) {
110             adapter = new FocusTestAdapter(mChildCount);
111         }
112         mLayoutManager = layoutManager;
113         mAdapter = adapter;
114         recyclerView.setAdapter(adapter);
115         recyclerView.setLayoutManager(mLayoutManager);
116         recyclerView.setPreserveFocusAfterLayout(!mDisableRecovery);
117         if (mDisableAnimation) {
118             recyclerView.setItemAnimator(null);
119         }
120         mLayoutManager.expectLayouts(1);
121         setRecyclerView(recyclerView);
122         mLayoutManager.waitForLayout(1);
123     }
124 
125     @Test
testFocusRecoveryInChange()126     public void testFocusRecoveryInChange() throws Throwable {
127         setupBasic();
128         mLayoutManager.setSupportsPredictive(true);
129         final RecyclerView.ViewHolder oldVh = focusVh(3);
130 
131         mLayoutManager.expectLayouts(mDisableAnimation ? 1 : 2);
132         mAdapter.changeAndNotify(3, 1);
133         mLayoutManager.waitForLayout(2);
134         if (!mDisableAnimation) {
135             // waiting for RV's ItemAnimator to finish the animation of the removed item
136             waitForAnimations(2);
137         }
138 
139         mActivityRule.runOnUiThread(new Runnable() {
140             @Override
141             public void run() {
142                 RecyclerView.ViewHolder newVh = mRecyclerView.findViewHolderForAdapterPosition(3);
143                 assertFocusTransition(oldVh, newVh, false);
144 
145             }
146         });
147     }
148 
149     @Test
testFocusRecoveryAfterRemovingFocusedChild()150     public void testFocusRecoveryAfterRemovingFocusedChild() throws Throwable {
151         setupBasic(true);
152         FocusViewHolder fvh = cast(focusVh(4));
153 
154         assertThat("test sanity", fvh, notNullValue());
155         assertThat("RV should have focus", mRecyclerView.hasFocus(), is(true));
156 
157         assertThat("RV should pass the focus down to its children",
158                 mRecyclerView.isFocused(), is(false));
159         assertThat("Viewholder did not receive focus", fvh.itemView.hasFocus(),
160                 is(true));
161         assertThat("Viewholder is not focused", fvh.getViewToFocus().isFocused(),
162                 is(true));
163 
164         mLayoutManager.expectLayouts(1);
165         mActivityRule.runOnUiThread(new Runnable() {
166             @Override
167             public void run() {
168                 // removing focused child
169                 mAdapter.mItems.remove(4);
170                 mAdapter.notifyItemRemoved(4);
171             }
172         });
173         mLayoutManager.waitForLayout(1);
174         if (!mDisableAnimation) {
175             // waiting for RV's ItemAnimator to finish the animation of the removed item
176             waitForAnimations(2);
177         }
178         assertThat("RV should have " + (mChildCount - 1) + " instead of "
179                         + mRecyclerView.getChildCount() + " children",
180                 mChildCount - 1, is(mRecyclerView.getChildCount()));
181         assertFocusAfterLayout(4, 0);
182     }
183 
184     @Test
testFocusRecoveryAfterMovingFocusedChild()185     public void testFocusRecoveryAfterMovingFocusedChild() throws Throwable {
186         setupBasic(true);
187         FocusViewHolder fvh = cast(focusVh(3));
188 
189         assertThat("test sanity", fvh, notNullValue());
190         assertThat("RV should have focus", mRecyclerView.hasFocus(), is(true));
191 
192         assertThat("RV should pass the focus down to its children",
193                 mRecyclerView.isFocused(), is(false));
194         assertThat("Viewholder did not receive focus", fvh.itemView.hasFocus(),
195                 is(true));
196         assertThat("Viewholder is not focused", fvh.getViewToFocus().isFocused(),
197                 is(true));
198 
199         mLayoutManager.expectLayouts(1);
200         mAdapter.moveAndNotify(3, 1);
201         mLayoutManager.waitForLayout(1);
202         if (!mDisableAnimation) {
203             // waiting for RV's ItemAnimator to finish the animation of the removed item
204             waitForAnimations(2);
205         }
206         assertFocusAfterLayout(1, 1);
207     }
208 
209     @Test
testFocusRecoveryAfterRemovingLastChild()210     public void testFocusRecoveryAfterRemovingLastChild() throws Throwable {
211         mChildCount = 1;
212         setupBasic(true);
213         FocusViewHolder fvh = cast(focusVh(0));
214 
215         assertThat("test sanity", fvh, notNullValue());
216         assertThat("RV should have focus", mRecyclerView.hasFocus(), is(true));
217 
218         assertThat("RV should pass the focus down to its children",
219                 mRecyclerView.isFocused(), is(false));
220         assertThat("Viewholder did not receive focus", fvh.itemView.hasFocus(),
221                 is(true));
222         assertThat("Viewholder is not focused", fvh.getViewToFocus().isFocused(),
223                 is(true));
224 
225         mLayoutManager.expectLayouts(1);
226         mActivityRule.runOnUiThread(new Runnable() {
227             @Override
228             public void run() {
229                 // removing focused child
230                 mAdapter.mItems.remove(0);
231                 mAdapter.notifyDataSetChanged();
232             }
233         });
234         mLayoutManager.waitForLayout(1);
235         if (!mDisableAnimation) {
236             // waiting for RV's ItemAnimator to finish the animation of the removed item
237             waitForAnimations(2);
238         }
239         assertThat("RV should have " + (mChildCount - 1) + " instead of "
240                         + mRecyclerView.getChildCount() + " children",
241                 mChildCount - 1, is(mRecyclerView.getChildCount()));
242         assertFocusAfterLayout(-1, -1);
243     }
244 
245     @Test
testFocusRecoveryAfterAddingFirstChild()246     public void testFocusRecoveryAfterAddingFirstChild() throws Throwable {
247         mChildCount = 0;
248         setupBasic(true);
249         mActivityRule.runOnUiThread(new Runnable() {
250             @Override
251             public void run() {
252                 requestFocusOnRV();
253             }
254         });
255 
256         mLayoutManager.expectLayouts(1);
257         mActivityRule.runOnUiThread(new Runnable() {
258             @Override
259             public void run() {
260                 // adding first child
261                 mAdapter.mItems.add(0, new Item(0, TestAdapter.DEFAULT_ITEM_PREFIX));
262                 mAdapter.notifyDataSetChanged();
263             }
264         });
265         mLayoutManager.waitForLayout(1);
266         if (!mDisableAnimation) {
267             // waiting for RV's ItemAnimator to finish the animation of the removed item
268             waitForAnimations(2);
269         }
270         assertFocusAfterLayout(0, -1);
271     }
272 
273     @Test
testFocusRecoveryAfterChangingFocusableFlag()274     public void testFocusRecoveryAfterChangingFocusableFlag() throws Throwable {
275         setupBasic(true);
276         FocusViewHolder fvh = cast(focusVh(6));
277 
278         assertThat("test sanity", fvh, notNullValue());
279         assertThat("RV should have focus", mRecyclerView.hasFocus(), is(true));
280 
281         assertThat("RV should pass the focus down to its children",
282                 mRecyclerView.isFocused(), is(false));
283         assertThat("Viewholder did not receive focus", fvh.itemView.hasFocus(),
284                 is(true));
285         assertThat("Viewholder is not focused", fvh.getViewToFocus().isFocused(),
286                 is(true));
287 
288         mLayoutManager.expectLayouts(1);
289         mActivityRule.runOnUiThread(new Runnable() {
290             @Override
291             public void run() {
292                 Item item = mAdapter.mItems.get(6);
293                 item.setFocusable(false);
294                 mAdapter.notifyItemChanged(6);
295             }
296         });
297         mLayoutManager.waitForLayout(1);
298         if (!mDisableAnimation) {
299             waitForAnimations(2);
300         }
301         FocusViewHolder newVh = cast(mRecyclerView.findViewHolderForAdapterPosition(6));
302         assertThat("VH should no longer be focusable", newVh.getViewToFocus().isFocusable(),
303                 is(false));
304         assertFocusAfterLayout(7, 0);
305     }
306 
307     @Test
testFocusRecoveryBeforeLayoutWithFocusBefore()308     public void testFocusRecoveryBeforeLayoutWithFocusBefore() throws Throwable {
309         testFocusRecoveryBeforeLayout(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
310     }
311 
312     @Test
testFocusRecoveryBeforeLayoutWithFocusAfter()313     public void testFocusRecoveryBeforeLayoutWithFocusAfter() throws Throwable {
314         testFocusRecoveryBeforeLayout(ViewGroup.FOCUS_AFTER_DESCENDANTS);
315     }
316 
317     @Test
testFocusRecoveryBeforeLayoutWithFocusBlocked()318     public void testFocusRecoveryBeforeLayoutWithFocusBlocked() throws Throwable {
319         testFocusRecoveryBeforeLayout(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
320     }
321 
322     @Test
testFocusRecoveryDuringLayoutWithFocusBefore()323     public void testFocusRecoveryDuringLayoutWithFocusBefore() throws Throwable {
324         testFocusRecoveryDuringLayout(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
325     }
326 
327     @Test
testFocusRecoveryDuringLayoutWithFocusAfter()328     public void testFocusRecoveryDuringLayoutWithFocusAfter() throws Throwable {
329         testFocusRecoveryDuringLayout(ViewGroup.FOCUS_AFTER_DESCENDANTS);
330     }
331 
332     @Test
testFocusRecoveryDuringLayoutWithFocusBlocked()333     public void testFocusRecoveryDuringLayoutWithFocusBlocked() throws Throwable {
334         testFocusRecoveryDuringLayout(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
335     }
336 
337     /**
338      * Tests whether the focus is correctly recovered when requestFocus on RV is called before
339      * laying out the children.
340      * @throws Throwable
341      */
testFocusRecoveryBeforeLayout(int descendantFocusability)342     private void testFocusRecoveryBeforeLayout(int descendantFocusability) throws Throwable {
343         RecyclerView recyclerView = new RecyclerView(getActivity());
344         recyclerView.setDescendantFocusability(descendantFocusability);
345         mLayoutManager = new FocusLayoutManager();
346         mAdapter = new FocusTestAdapter(10);
347         recyclerView.setLayoutManager(mLayoutManager);
348         recyclerView.setPreserveFocusAfterLayout(!mDisableRecovery);
349         if (mDisableAnimation) {
350             recyclerView.setItemAnimator(null);
351         }
352         setRecyclerView(recyclerView);
353         assertThat("RV should always be focusable", mRecyclerView.isFocusable(), is(true));
354 
355         mLayoutManager.expectLayouts(1);
356         mActivityRule.runOnUiThread(new Runnable() {
357             @Override
358             public void run() {
359                 requestFocusOnRV();
360                 mRecyclerView.setAdapter(mAdapter);
361             }
362         });
363         mLayoutManager.waitForLayout(1);
364         assertFocusAfterLayout(0, -1);
365     }
366 
367     /**
368      * Tests whether the focus is correctly recovered when requestFocus on RV is called during
369      * laying out the children.
370      * @throws Throwable
371      */
testFocusRecoveryDuringLayout(int descendantFocusability)372     private void testFocusRecoveryDuringLayout(int descendantFocusability) throws Throwable {
373         RecyclerView recyclerView = new RecyclerView(getActivity());
374         recyclerView.setDescendantFocusability(descendantFocusability);
375         mLayoutManager = new FocusLayoutManager() {
376             @Override
377             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
378                 super.onLayoutChildren(recycler, state);
379                 requestFocusOnRV();
380             }
381         };
382         mAdapter = new FocusTestAdapter(10);
383         recyclerView.setAdapter(mAdapter);
384         recyclerView.setLayoutManager(mLayoutManager);
385         if (mDisableAnimation) {
386             recyclerView.setItemAnimator(null);
387         }
388         recyclerView.setPreserveFocusAfterLayout(!mDisableRecovery);
389         mLayoutManager.expectLayouts(1);
390         setRecyclerView(recyclerView);
391         mLayoutManager.waitForLayout(1);
392         assertFocusAfterLayout(0, -1);
393     }
394 
requestFocusOnRV()395     private void requestFocusOnRV() {
396         assertThat("RV initially has no focus", mRecyclerView.hasFocus(), is(false));
397         assertThat("RV initially is not focused", mRecyclerView.isFocused(), is(false));
398         mRecyclerView.requestFocus();
399         String msg = !mRecyclerView.isComputingLayout() ? " before laying out the children"
400                 : " during laying out the children";
401         assertThat("RV should have focus after calling requestFocus()" + msg,
402                 mRecyclerView.hasFocus(), is(true));
403         assertThat("RV after calling requestFocus() should become focused" + msg,
404                 mRecyclerView.isFocused(), is(true));
405     }
406 
407     /**
408      * Asserts whether RV and one of its children have the correct focus flags after the layout is
409      * complete. This is normally called once the RV layout is complete after initiating
410      * notifyItemChanged.
411      * @param focusedChildIndexWhenRecoveryEnabled
412      * This index is relevant when mDisableRecovery is false. In that case, it refers to the index
413      * of the child that should have focus if the ancestors allow passing down the focus. -1
414      * indicates none of the children can receive focus even if the ancestors don't block focus, in
415      * which case RV holds and becomes focused.
416      * @param focusedChildIndexWhenRecoveryDisabled
417      * This index is relevant when mDisableRecovery is true. In that case, it refers to the index
418      * of the child that should have focus if the ancestors allow passing down the focus. -1
419      * indicates none of the children can receive focus even if the ancestors don't block focus, in
420      * which case RV holds and becomes focused.
421      */
assertFocusAfterLayout(int focusedChildIndexWhenRecoveryEnabled, int focusedChildIndexWhenRecoveryDisabled)422     private void assertFocusAfterLayout(int focusedChildIndexWhenRecoveryEnabled,
423                                         int focusedChildIndexWhenRecoveryDisabled) {
424         if (mDisableAnimation && mDisableRecovery) {
425             // This case is not quite handled properly at the moment. For now, RV may become focused
426             // without re-delivering the focus down to the children. Skip the checks for now.
427             return;
428         }
429         if (mRecyclerView.getChildCount() == 0) {
430             assertThat("RV should have focus when it has no children",
431                     mRecyclerView.hasFocus(), is(true));
432             assertThat("RV should be focused when it has no children",
433                     mRecyclerView.isFocused(), is(true));
434             return;
435         }
436 
437         assertThat("RV should still have focus after layout", mRecyclerView.hasFocus(), is(true));
438         if ((mDisableRecovery && focusedChildIndexWhenRecoveryDisabled == -1)
439                 || (!mDisableRecovery && focusedChildIndexWhenRecoveryEnabled == -1)
440                 || mRecyclerView.getDescendantFocusability() == ViewGroup.FOCUS_BLOCK_DESCENDANTS
441                 || mRecyclerView.getDescendantFocusability()
442                 == ViewGroup.FOCUS_BEFORE_DESCENDANTS) {
443             FocusViewHolder fvh = cast(mRecyclerView.findViewHolderForAdapterPosition(0));
444             String msg1 = " when focus recovery is disabled";
445             String msg2 = " when descendant focusability is FOCUS_BLOCK_DESCENDANTS";
446             String msg3 = " when descendant focusability is FOCUS_BEFORE_DESCENDANTS";
447 
448             assertThat("RV should not pass the focus down to its children"
449                             + (mDisableRecovery ? msg1 : (mRecyclerView.getDescendantFocusability()
450                                     == ViewGroup.FOCUS_BLOCK_DESCENDANTS ? msg2 : msg3)),
451                     mRecyclerView.isFocused(), is(true));
452             assertThat("RV's first child should not have focus"
453                             + (mDisableRecovery ? msg1 : (mRecyclerView.getDescendantFocusability()
454                                     == ViewGroup.FOCUS_BLOCK_DESCENDANTS ? msg2 : msg3)),
455                     fvh.itemView.hasFocus(), is(false));
456             assertThat("RV's first child should not be focused"
457                             + (mDisableRecovery ? msg1 : (mRecyclerView.getDescendantFocusability()
458                                     == ViewGroup.FOCUS_BLOCK_DESCENDANTS ? msg2 : msg3)),
459                     fvh.getViewToFocus().isFocused(), is(false));
460         } else {
461             FocusViewHolder fvh = mDisableRecovery
462                     ? cast(mRecyclerView.findViewHolderForAdapterPosition(
463                             focusedChildIndexWhenRecoveryDisabled)) :
464                     (focusedChildIndexWhenRecoveryEnabled != -1
465                             ? cast(mRecyclerView.findViewHolderForAdapterPosition(
466                                     focusedChildIndexWhenRecoveryEnabled)) :
467                     cast(mRecyclerView.findViewHolderForAdapterPosition(0)));
468 
469             assertThat("test sanity", fvh, notNullValue());
470             assertThat("RV's first child should be focusable", fvh.getViewToFocus().isFocusable(),
471                     is(true));
472             String msg = " when descendant focusability is FOCUS_AFTER_DESCENDANTS";
473             assertThat("RV should pass the focus down to its children after layout" + msg,
474                     mRecyclerView.isFocused(), is(false));
475             assertThat("RV's child #" + focusedChildIndexWhenRecoveryEnabled + " should have focus"
476                             + " after layout" + msg,
477                     fvh.itemView.hasFocus(), is(true));
478             if (mFocusOnChild) {
479                 assertThat("Either the ViewGroup or the TextView within the first child of RV"
480                                 + "should be focused after layout" + msg,
481                         fvh.itemView.isFocused() || fvh.getViewToFocus().isFocused(), is(true));
482             } else {
483                 assertThat("RV's first child should be focused after layout" + msg,
484                         fvh.getViewToFocus().isFocused(), is(true));
485             }
486 
487         }
488     }
489 
assertFocusTransition(RecyclerView.ViewHolder oldVh, RecyclerView.ViewHolder newVh, boolean typeChanged)490     private void assertFocusTransition(RecyclerView.ViewHolder oldVh,
491             RecyclerView.ViewHolder newVh, boolean typeChanged) {
492         if (mDisableRecovery) {
493             if (mDisableAnimation) {
494                 return;
495             }
496             assertFocus(newVh, false);
497             return;
498         }
499         assertThat("test sanity", newVh, notNullValue());
500         if (!typeChanged && mDisableAnimation) {
501             assertThat(oldVh, sameInstance(newVh));
502         } else {
503             assertThat(oldVh, not(sameInstance(newVh)));
504             assertFocus(oldVh, false);
505         }
506         assertFocus(newVh, true);
507     }
508 
509     @Test
testFocusRecoveryInTypeChangeWithPredictive()510     public void testFocusRecoveryInTypeChangeWithPredictive() throws Throwable {
511         testFocusRecoveryInTypeChange(true);
512     }
513 
514     @Test
testFocusRecoveryInTypeChangeWithoutPredictive()515     public void testFocusRecoveryInTypeChangeWithoutPredictive() throws Throwable {
516         testFocusRecoveryInTypeChange(false);
517     }
518 
testFocusRecoveryInTypeChange(boolean withAnimation)519     private void testFocusRecoveryInTypeChange(boolean withAnimation) throws Throwable {
520         setupBasic();
521         if (!mDisableAnimation) {
522             ((SimpleItemAnimator) (mRecyclerView.getItemAnimator()))
523                     .setSupportsChangeAnimations(true);
524         }
525         mLayoutManager.setSupportsPredictive(withAnimation);
526         final RecyclerView.ViewHolder oldVh = focusVh(3);
527         mLayoutManager.expectLayouts(!mDisableAnimation && withAnimation ? 2 : 1);
528         mActivityRule.runOnUiThread(new Runnable() {
529             @Override
530             public void run() {
531                 Item item = mAdapter.mItems.get(3);
532                 item.mType += 2;
533                 mAdapter.notifyItemChanged(3);
534             }
535         });
536         mLayoutManager.waitForLayout(2);
537 
538         RecyclerView.ViewHolder newVh = mRecyclerView.findViewHolderForAdapterPosition(3);
539         assertFocusTransition(oldVh, newVh, true);
540         assertThat("test sanity", oldVh.getItemViewType(), not(newVh.getItemViewType()));
541     }
542 
543     @Test
testRecoverAdapterChangeViaStableIdOnDataSetChanged()544     public void testRecoverAdapterChangeViaStableIdOnDataSetChanged() throws Throwable {
545         recoverAdapterChangeViaStableId(false, false);
546     }
547 
548     @Test
testRecoverAdapterChangeViaStableIdOnSwap()549     public void testRecoverAdapterChangeViaStableIdOnSwap() throws Throwable {
550         recoverAdapterChangeViaStableId(true, false);
551     }
552 
553     @Test
testRecoverAdapterChangeViaStableIdOnDataSetChangedWithTypeChange()554     public void testRecoverAdapterChangeViaStableIdOnDataSetChangedWithTypeChange()
555             throws Throwable {
556         recoverAdapterChangeViaStableId(false, true);
557     }
558 
559     @Test
testRecoverAdapterChangeViaStableIdOnSwapWithTypeChange()560     public void testRecoverAdapterChangeViaStableIdOnSwapWithTypeChange() throws Throwable {
561         recoverAdapterChangeViaStableId(true, true);
562     }
563 
recoverAdapterChangeViaStableId(final boolean swap, final boolean changeType)564     private void recoverAdapterChangeViaStableId(final boolean swap, final boolean changeType)
565             throws Throwable {
566         setupBasic(true);
567         RecyclerView.ViewHolder oldVh = focusVh(4);
568         long itemId = oldVh.getItemId();
569 
570         mLayoutManager.expectLayouts(1);
571         mActivityRule.runOnUiThread(new Runnable() {
572             @Override
573             public void run() {
574                 Item item = mAdapter.mItems.get(4);
575                 if (changeType) {
576                     item.mType += 2;
577                 }
578                 if (swap) {
579                     mAdapter = new FocusTestAdapter(8);
580                     mAdapter.setHasStableIds(true);
581                     mAdapter.mItems.add(2, item);
582                     mRecyclerView.swapAdapter(mAdapter, false);
583                 } else {
584                     mAdapter.mItems.remove(0);
585                     mAdapter.mItems.remove(0);
586                     mAdapter.notifyDataSetChanged();
587                 }
588             }
589         });
590         mLayoutManager.waitForLayout(1);
591         if (!mDisableAnimation) {
592             waitForAnimations(2);
593         }
594 
595         RecyclerView.ViewHolder newVh = mRecyclerView.findViewHolderForItemId(itemId);
596         if (changeType) {
597             assertFocusTransition(oldVh, newVh, true);
598         } else {
599             // in this case we should use the same VH because we have stable ids
600             assertThat(oldVh, sameInstance(newVh));
601             assertFocus(newVh, true);
602         }
603     }
604 
605     @Test
testDoNotRecoverViaPositionOnSetAdapter()606     public void testDoNotRecoverViaPositionOnSetAdapter() throws Throwable {
607         testDoNotRecoverViaPositionOnNewDataSet(new RecyclerViewLayoutTest.AdapterRunnable() {
608             @Override
609             public void run(TestAdapter adapter) throws Throwable {
610                 mRecyclerView.setAdapter(new FocusTestAdapter(10));
611             }
612         });
613     }
614 
615     @Test
testDoNotRecoverViaPositionOnSwapAdapterWithRecycle()616     public void testDoNotRecoverViaPositionOnSwapAdapterWithRecycle() throws Throwable {
617         testDoNotRecoverViaPositionOnNewDataSet(new RecyclerViewLayoutTest.AdapterRunnable() {
618             @Override
619             public void run(TestAdapter adapter) throws Throwable {
620                 mRecyclerView.swapAdapter(new FocusTestAdapter(10), true);
621             }
622         });
623     }
624 
625     @Test
testDoNotRecoverViaPositionOnSwapAdapterWithoutRecycle()626     public void testDoNotRecoverViaPositionOnSwapAdapterWithoutRecycle() throws Throwable {
627         testDoNotRecoverViaPositionOnNewDataSet(new RecyclerViewLayoutTest.AdapterRunnable() {
628             @Override
629             public void run(TestAdapter adapter) throws Throwable {
630                 mRecyclerView.swapAdapter(new FocusTestAdapter(10), false);
631             }
632         });
633     }
634 
testDoNotRecoverViaPositionOnNewDataSet( final RecyclerViewLayoutTest.AdapterRunnable runnable)635     public void testDoNotRecoverViaPositionOnNewDataSet(
636             final RecyclerViewLayoutTest.AdapterRunnable runnable) throws Throwable {
637         setupBasic(false);
638         assertThat("test sanity", mAdapter.hasStableIds(), is(false));
639         focusVh(4);
640         mLayoutManager.expectLayouts(1);
641         mActivityRule.runOnUiThread(new Runnable() {
642             @Override
643             public void run() {
644                 try {
645                     runnable.run(mAdapter);
646                 } catch (Throwable throwable) {
647                     postExceptionToInstrumentation(throwable);
648                 }
649             }
650         });
651 
652         mLayoutManager.waitForLayout(1);
653         RecyclerView.ViewHolder otherVh = mRecyclerView.findViewHolderForAdapterPosition(4);
654         checkForMainThreadException();
655         // even if the VH is re-used, it will be removed-reAdded so focus will go away from it.
656         assertFocus("should not recover focus if data set is badly invalid", otherVh, false);
657 
658     }
659 
660     @Test
testDoNotRecoverIfReplacementIsNotFocusable()661     public void testDoNotRecoverIfReplacementIsNotFocusable() throws Throwable {
662         final int TYPE_NO_FOCUS = 1001;
663         TestAdapter adapter = new FocusTestAdapter(10) {
664             @Override
665             public void onBindViewHolder(TestViewHolder holder,
666                     int position) {
667                 super.onBindViewHolder(holder, position);
668                 if (holder.getItemViewType() == TYPE_NO_FOCUS) {
669                     cast(holder).setFocusable(false);
670                 }
671             }
672         };
673         adapter.setHasStableIds(true);
674         setupBasic(adapter);
675         RecyclerView.ViewHolder oldVh = focusVh(3);
676         final long itemId = oldVh.getItemId();
677         mLayoutManager.expectLayouts(1);
678         mActivityRule.runOnUiThread(new Runnable() {
679             @Override
680             public void run() {
681                 mAdapter.mItems.get(3).mType = TYPE_NO_FOCUS;
682                 mAdapter.notifyDataSetChanged();
683             }
684         });
685         mLayoutManager.waitForLayout(2);
686         if (!mDisableAnimation) {
687             waitForAnimations(2);
688         }
689         RecyclerView.ViewHolder newVh = mRecyclerView.findViewHolderForItemId(itemId);
690         assertFocus(newVh, false);
691     }
692 
693     @NonNull
focusVh(int pos)694     private RecyclerView.ViewHolder focusVh(int pos) throws Throwable {
695         final RecyclerView.ViewHolder oldVh = mRecyclerView.findViewHolderForAdapterPosition(pos);
696         assertThat("test sanity", oldVh, notNullValue());
697         requestFocus(oldVh);
698         assertFocus("test sanity", oldVh, true);
699         getInstrumentation().waitForIdleSync();
700         return oldVh;
701     }
702 
703     @Test
testDoNotOverrideAdapterRequestedFocus()704     public void testDoNotOverrideAdapterRequestedFocus() throws Throwable {
705         final AtomicLong toFocusId = new AtomicLong(-1);
706 
707         FocusTestAdapter adapter = new FocusTestAdapter(10) {
708             @Override
709             public void onBindViewHolder(TestViewHolder holder,
710                     int position) {
711                 super.onBindViewHolder(holder, position);
712                 if (holder.getItemId() == toFocusId.get()) {
713                     try {
714                         requestFocus(holder);
715                     } catch (Throwable throwable) {
716                         postExceptionToInstrumentation(throwable);
717                     }
718                 }
719             }
720         };
721         adapter.setHasStableIds(true);
722         toFocusId.set(adapter.mItems.get(3).mId);
723         long firstFocusId = toFocusId.get();
724         setupBasic(adapter);
725         RecyclerView.ViewHolder oldVh = mRecyclerView.findViewHolderForItemId(toFocusId.get());
726         assertFocus(oldVh, true);
727         toFocusId.set(mAdapter.mItems.get(5).mId);
728         mLayoutManager.expectLayouts(1);
729         mActivityRule.runOnUiThread(new Runnable() {
730             @Override
731             public void run() {
732                 mAdapter.mItems.get(3).mType += 2;
733                 mAdapter.mItems.get(5).mType += 2;
734                 mAdapter.notifyDataSetChanged();
735             }
736         });
737         mLayoutManager.waitForLayout(2);
738         if (!mDisableAnimation) {
739             waitForAnimations(2);
740         }
741         RecyclerView.ViewHolder requested = mRecyclerView.findViewHolderForItemId(toFocusId.get());
742         assertFocus(oldVh, false);
743         assertFocus(requested, true);
744         RecyclerView.ViewHolder oldReplacement = mRecyclerView
745                 .findViewHolderForItemId(firstFocusId);
746         assertFocus(oldReplacement, false);
747         checkForMainThreadException();
748     }
749 
750     @Test
testDoNotOverrideLayoutManagerRequestedFocus()751     public void testDoNotOverrideLayoutManagerRequestedFocus() throws Throwable {
752         final AtomicLong toFocusId = new AtomicLong(-1);
753         FocusTestAdapter adapter = new FocusTestAdapter(10);
754         adapter.setHasStableIds(true);
755 
756         FocusLayoutManager lm = new FocusLayoutManager() {
757             @Override
758             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
759                 detachAndScrapAttachedViews(recycler);
760                 layoutRange(recycler, 0, state.getItemCount());
761                 RecyclerView.ViewHolder toFocus = mRecyclerView
762                         .findViewHolderForItemId(toFocusId.get());
763                 if (toFocus != null) {
764                     try {
765                         requestFocus(toFocus);
766                     } catch (Throwable throwable) {
767                         postExceptionToInstrumentation(throwable);
768                     }
769                 }
770                 layoutLatch.countDown();
771             }
772         };
773 
774         toFocusId.set(adapter.mItems.get(3).mId);
775         long firstFocusId = toFocusId.get();
776         setupBasic(adapter, lm);
777 
778         RecyclerView.ViewHolder oldVh = mRecyclerView.findViewHolderForItemId(toFocusId.get());
779         assertFocus(oldVh, true);
780         toFocusId.set(mAdapter.mItems.get(5).mId);
781         mLayoutManager.expectLayouts(1);
782         requestLayoutOnUIThread(mRecyclerView);
783         mLayoutManager.waitForLayout(2);
784         RecyclerView.ViewHolder requested = mRecyclerView.findViewHolderForItemId(toFocusId.get());
785         assertFocus(oldVh, false);
786         assertFocus(requested, true);
787         RecyclerView.ViewHolder oldReplacement = mRecyclerView
788                 .findViewHolderForItemId(firstFocusId);
789         assertFocus(oldReplacement, false);
790         checkForMainThreadException();
791     }
792 
requestFocus(RecyclerView.ViewHolder viewHolder)793     private void requestFocus(RecyclerView.ViewHolder viewHolder) throws Throwable {
794         FocusViewHolder fvh = cast(viewHolder);
795         requestFocus(fvh.getViewToFocus(), false);
796     }
797 
assertFocus(RecyclerView.ViewHolder viewHolder, boolean hasFocus)798     private void assertFocus(RecyclerView.ViewHolder viewHolder, boolean hasFocus) {
799         assertFocus("", viewHolder, hasFocus);
800     }
801 
assertFocus(String msg, RecyclerView.ViewHolder vh, boolean hasFocus)802     private void assertFocus(String msg, RecyclerView.ViewHolder vh, boolean hasFocus) {
803         FocusViewHolder fvh = cast(vh);
804         assertThat(msg, fvh.getViewToFocus().hasFocus(), is(hasFocus));
805     }
806 
cast(RecyclerView.ViewHolder vh)807     private <T extends FocusViewHolder> T cast(RecyclerView.ViewHolder vh) {
808         assertThat(vh, instanceOf(FocusViewHolder.class));
809         //noinspection unchecked
810         return (T) vh;
811     }
812 
813     private class FocusTestAdapter extends TestAdapter {
814 
FocusTestAdapter(int count)815         public FocusTestAdapter(int count) {
816             super(count);
817         }
818 
819         @Override
onCreateViewHolder(ViewGroup parent, int viewType)820         public FocusViewHolder onCreateViewHolder(ViewGroup parent,
821                 int viewType) {
822             final FocusViewHolder fvh;
823             if (mFocusOnChild) {
824                 fvh = new FocusViewHolderWithChildren(
825                         LayoutInflater.from(parent.getContext())
826                                 .inflate(R.layout.focus_test_item_view, parent, false));
827             } else {
828                 fvh = new SimpleFocusViewHolder(new TextView(parent.getContext()));
829             }
830             fvh.setFocusable(true);
831             return fvh;
832         }
833 
834         @Override
onBindViewHolder(TestViewHolder holder, int position)835         public void onBindViewHolder(TestViewHolder holder, int position) {
836             cast(holder).bindTo(mItems.get(position));
837         }
838     }
839 
840     private class FocusLayoutManager extends TestLayoutManager {
841         @Override
onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)842         public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
843             detachAndScrapAttachedViews(recycler);
844             layoutRange(recycler, 0, state.getItemCount());
845             layoutLatch.countDown();
846         }
847     }
848 
849     private class FocusViewHolderWithChildren extends FocusViewHolder {
850         public final ViewGroup root;
851         public final ViewGroup parent1;
852         public final ViewGroup parent2;
853         public final TextView textView;
854 
FocusViewHolderWithChildren(View view)855         public FocusViewHolderWithChildren(View view) {
856             super(view);
857             root = (ViewGroup) view;
858             parent1 = (ViewGroup) root.findViewById(R.id.parent1);
859             parent2 = (ViewGroup) root.findViewById(R.id.parent2);
860             textView = (TextView) root.findViewById(R.id.text_view);
861 
862         }
863 
864         @Override
setFocusable(boolean focusable)865         void setFocusable(boolean focusable) {
866             parent1.setFocusableInTouchMode(focusable);
867             parent2.setFocusableInTouchMode(focusable);
868             textView.setFocusableInTouchMode(focusable);
869             root.setFocusableInTouchMode(focusable);
870 
871             parent1.setFocusable(focusable);
872             parent2.setFocusable(focusable);
873             textView.setFocusable(focusable);
874             root.setFocusable(focusable);
875         }
876 
877         @Override
onBind(Item item)878         void onBind(Item item) {
879             textView.setText(getText(item));
880         }
881 
882         @Override
getViewToFocus()883         View getViewToFocus() {
884             return textView;
885         }
886     }
887 
888     private class SimpleFocusViewHolder extends FocusViewHolder {
889 
SimpleFocusViewHolder(View itemView)890         public SimpleFocusViewHolder(View itemView) {
891             super(itemView);
892         }
893 
894         @Override
setFocusable(boolean focusable)895         void setFocusable(boolean focusable) {
896             itemView.setFocusableInTouchMode(focusable);
897             itemView.setFocusable(focusable);
898         }
899 
900         @Override
getViewToFocus()901         View getViewToFocus() {
902             return itemView;
903         }
904 
905         @Override
onBind(Item item)906         void onBind(Item item) {
907             ((TextView) (itemView)).setText(getText(item));
908         }
909     }
910 
911     private abstract class FocusViewHolder extends TestViewHolder {
912 
FocusViewHolder(View itemView)913         public FocusViewHolder(View itemView) {
914             super(itemView);
915         }
916 
getText(Item item)917         protected String getText(Item item) {
918             return item.mText + "(" + item.mId + ")";
919         }
920 
setFocusable(boolean focusable)921         abstract void setFocusable(boolean focusable);
922 
getViewToFocus()923         abstract View getViewToFocus();
924 
onBind(Item item)925         abstract void onBind(Item item);
926 
bindTo(Item item)927         final void bindTo(Item item) {
928             mBoundItem = item;
929             setFocusable(item.isFocusable());
930             onBind(item);
931         }
932     }
933 }
934