1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.support.v7.widget;
18 
19 import static android.support.v7.widget.RecyclerView.SCROLL_STATE_IDLE;
20 
21 import static org.junit.Assert.assertEquals;
22 import static org.junit.Assert.assertFalse;
23 import static org.junit.Assert.assertNotNull;
24 import static org.junit.Assert.assertNotSame;
25 import static org.junit.Assert.assertNull;
26 import static org.junit.Assert.assertSame;
27 import static org.junit.Assert.assertThat;
28 import static org.junit.Assert.assertTrue;
29 
30 import static java.util.concurrent.TimeUnit.SECONDS;
31 
32 import android.app.Instrumentation;
33 import android.graphics.Rect;
34 import android.os.Looper;
35 import android.support.annotation.Nullable;
36 import android.support.test.InstrumentationRegistry;
37 import android.support.test.rule.ActivityTestRule;
38 import android.support.v4.view.ViewCompat;
39 import android.support.v7.recyclerview.test.R;
40 import android.support.v7.recyclerview.test.SameActivityTestRule;
41 import android.util.Log;
42 import android.view.LayoutInflater;
43 import android.view.View;
44 import android.view.ViewGroup;
45 import android.widget.FrameLayout;
46 import android.widget.TextView;
47 
48 import org.hamcrest.CoreMatchers;
49 import org.hamcrest.MatcherAssert;
50 import org.junit.After;
51 import org.junit.Before;
52 import org.junit.Rule;
53 
54 import java.lang.reflect.InvocationTargetException;
55 import java.lang.reflect.Method;
56 import java.util.ArrayList;
57 import java.util.HashSet;
58 import java.util.List;
59 import java.util.Set;
60 import java.util.concurrent.CountDownLatch;
61 import java.util.concurrent.TimeUnit;
62 import java.util.concurrent.atomic.AtomicBoolean;
63 import java.util.concurrent.atomic.AtomicInteger;
64 
65 abstract public class BaseRecyclerViewInstrumentationTest {
66 
67     private static final String TAG = "RecyclerViewTest";
68 
69     private boolean mDebug;
70 
71     protected RecyclerView mRecyclerView;
72 
73     protected AdapterHelper mAdapterHelper;
74 
75     private Throwable mMainThreadException;
76 
77     private boolean mIgnoreMainThreadException = false;
78 
79     Thread mInstrumentationThread;
80 
81     @Rule
82     public ActivityTestRule<TestActivity> mActivityRule = new SameActivityTestRule() {
83         @Override
84         public boolean canReUseActivity() {
85             return BaseRecyclerViewInstrumentationTest.this.canReUseActivity();
86         }
87     };
88 
BaseRecyclerViewInstrumentationTest()89     public BaseRecyclerViewInstrumentationTest() {
90         this(false);
91     }
92 
BaseRecyclerViewInstrumentationTest(boolean debug)93     public BaseRecyclerViewInstrumentationTest(boolean debug) {
94         mDebug = debug;
95     }
96 
checkForMainThreadException()97     void checkForMainThreadException() throws Throwable {
98         if (!mIgnoreMainThreadException && mMainThreadException != null) {
99             throw mMainThreadException;
100         }
101     }
102 
setIgnoreMainThreadException(boolean ignoreMainThreadException)103     public void setIgnoreMainThreadException(boolean ignoreMainThreadException) {
104         mIgnoreMainThreadException = ignoreMainThreadException;
105     }
106 
getMainThreadException()107     public Throwable getMainThreadException() {
108         return mMainThreadException;
109     }
110 
getActivity()111     protected TestActivity getActivity() {
112         return mActivityRule.getActivity();
113     }
114 
115     @Before
setUpInsThread()116     public final void setUpInsThread() throws Exception {
117         mInstrumentationThread = Thread.currentThread();
118         Item.idCounter.set(0);
119     }
120 
setHasTransientState(final View view, final boolean value)121     void setHasTransientState(final View view, final boolean value) {
122         try {
123             mActivityRule.runOnUiThread(new Runnable() {
124                 @Override
125                 public void run() {
126                     ViewCompat.setHasTransientState(view, value);
127                 }
128             });
129         } catch (Throwable throwable) {
130             Log.e(TAG, "", throwable);
131         }
132     }
133 
canReUseActivity()134     public boolean canReUseActivity() {
135         return true;
136     }
137 
enableAccessibility()138     protected void enableAccessibility()
139             throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
140         Method getUIAutomation = Instrumentation.class.getMethod("getUiAutomation");
141         getUIAutomation.invoke(InstrumentationRegistry.getInstrumentation());
142     }
143 
setAdapter(final RecyclerView.Adapter adapter)144     void setAdapter(final RecyclerView.Adapter adapter) throws Throwable {
145         mActivityRule.runOnUiThread(new Runnable() {
146             @Override
147             public void run() {
148                 mRecyclerView.setAdapter(adapter);
149             }
150         });
151     }
152 
focusSearch(final View focused, final int direction)153     public View focusSearch(final View focused, final int direction) throws Throwable {
154         return focusSearch(focused, direction, false);
155     }
156 
focusSearch(final View focused, final int direction, boolean waitForScroll)157     public View focusSearch(final View focused, final int direction, boolean waitForScroll)
158             throws Throwable {
159         final View[] result = new View[1];
160         mActivityRule.runOnUiThread(new Runnable() {
161             @Override
162             public void run() {
163                 View view = focused.focusSearch(direction);
164                 if (view != null && view != focused) {
165                     view.requestFocus();
166                 }
167                 result[0] = view;
168             }
169         });
170         if (waitForScroll && (result[0] != null)) {
171             waitForIdleScroll(mRecyclerView);
172         }
173         return result[0];
174     }
175 
inflateWrappedRV()176     protected WrappedRecyclerView inflateWrappedRV() {
177         return (WrappedRecyclerView)
178                 LayoutInflater.from(getActivity()).inflate(R.layout.wrapped_test_rv,
179                         getRecyclerViewContainer(), false);
180     }
181 
swapAdapter(final RecyclerView.Adapter adapter, final boolean removeAndRecycleExistingViews)182     void swapAdapter(final RecyclerView.Adapter adapter,
183             final boolean removeAndRecycleExistingViews) throws Throwable {
184         mActivityRule.runOnUiThread(new Runnable() {
185             @Override
186             public void run() {
187                 try {
188                     mRecyclerView.swapAdapter(adapter, removeAndRecycleExistingViews);
189                 } catch (Throwable t) {
190                     postExceptionToInstrumentation(t);
191                 }
192             }
193         });
194         checkForMainThreadException();
195     }
196 
postExceptionToInstrumentation(Throwable t)197     void postExceptionToInstrumentation(Throwable t) {
198         if (mInstrumentationThread == Thread.currentThread()) {
199             throw new RuntimeException(t);
200         }
201         if (mMainThreadException != null) {
202             Log.e(TAG, "receiving another main thread exception. dropping.", t);
203         } else {
204             Log.e(TAG, "captured exception on main thread", t);
205             mMainThreadException = t;
206         }
207 
208         if (mRecyclerView != null && mRecyclerView
209                 .getLayoutManager() instanceof TestLayoutManager) {
210             TestLayoutManager lm = (TestLayoutManager) mRecyclerView.getLayoutManager();
211             // finish all layouts so that we get the correct exception
212             if (lm.layoutLatch != null) {
213                 while (lm.layoutLatch.getCount() > 0) {
214                     lm.layoutLatch.countDown();
215                 }
216             }
217         }
218     }
219 
getInstrumentation()220     public Instrumentation getInstrumentation() {
221         return InstrumentationRegistry.getInstrumentation();
222     }
223 
224     @After
tearDown()225     public final void tearDown() throws Exception {
226         if (mRecyclerView != null) {
227             try {
228                 removeRecyclerView();
229             } catch (Throwable throwable) {
230                 throwable.printStackTrace();
231             }
232         }
233         getInstrumentation().waitForIdleSync();
234 
235         try {
236             checkForMainThreadException();
237         } catch (Exception e) {
238             throw e;
239         } catch (Throwable throwable) {
240             throw new Exception(Log.getStackTraceString(throwable));
241         }
242     }
243 
getDecoratedRecyclerViewBounds()244     public Rect getDecoratedRecyclerViewBounds() {
245         return new Rect(
246                 mRecyclerView.getPaddingLeft(),
247                 mRecyclerView.getPaddingTop(),
248                 mRecyclerView.getWidth() - mRecyclerView.getPaddingRight(),
249                 mRecyclerView.getHeight() - mRecyclerView.getPaddingBottom()
250         );
251     }
252 
removeRecyclerView()253     public void removeRecyclerView() throws Throwable {
254         if (mRecyclerView == null) {
255             return;
256         }
257         if (!isMainThread()) {
258             getInstrumentation().waitForIdleSync();
259         }
260         mActivityRule.runOnUiThread(new Runnable() {
261             @Override
262             public void run() {
263                 try {
264                     // do not run validation if we already have an error
265                     if (mMainThreadException == null) {
266                         final RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
267                         if (adapter instanceof AttachDetachCountingAdapter) {
268                             ((AttachDetachCountingAdapter) adapter).getCounter()
269                                     .validateRemaining(mRecyclerView);
270                         }
271                     }
272                     getActivity().getContainer().removeAllViews();
273                 } catch (Throwable t) {
274                     postExceptionToInstrumentation(t);
275                 }
276             }
277         });
278         mRecyclerView = null;
279     }
280 
waitForAnimations(int seconds)281     void waitForAnimations(int seconds) throws Throwable {
282         final CountDownLatch latch = new CountDownLatch(1);
283         mActivityRule.runOnUiThread(new Runnable() {
284             @Override
285             public void run() {
286                 mRecyclerView.mItemAnimator
287                         .isRunning(new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
288                             @Override
289                             public void onAnimationsFinished() {
290                                 latch.countDown();
291                             }
292                         });
293             }
294         });
295 
296         assertTrue("animations didn't finish on expected time of " + seconds + " seconds",
297                 latch.await(seconds, TimeUnit.SECONDS));
298     }
299 
waitForIdleScroll(final RecyclerView recyclerView)300     public void waitForIdleScroll(final RecyclerView recyclerView) throws Throwable {
301         final CountDownLatch latch = new CountDownLatch(1);
302         mActivityRule.runOnUiThread(new Runnable() {
303             @Override
304             public void run() {
305                 RecyclerView.OnScrollListener listener = new RecyclerView.OnScrollListener() {
306                     @Override
307                     public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
308                         if (newState == SCROLL_STATE_IDLE) {
309                             latch.countDown();
310                             recyclerView.removeOnScrollListener(this);
311                         }
312                     }
313                 };
314                 if (recyclerView.getScrollState() == SCROLL_STATE_IDLE) {
315                     latch.countDown();
316                 } else {
317                     recyclerView.addOnScrollListener(listener);
318                 }
319             }
320         });
321         assertTrue("should go idle in 10 seconds", latch.await(10, TimeUnit.SECONDS));
322     }
323 
requestFocus(final View view, boolean waitForScroll)324     public boolean requestFocus(final View view, boolean waitForScroll) throws Throwable {
325         final boolean[] result = new boolean[1];
326         mActivityRule.runOnUiThread(new Runnable() {
327             @Override
328             public void run() {
329                 result[0] = view.requestFocus();
330             }
331         });
332         if (waitForScroll && result[0]) {
333             waitForIdleScroll(mRecyclerView);
334         }
335         return result[0];
336     }
337 
setRecyclerView(final RecyclerView recyclerView)338     public void setRecyclerView(final RecyclerView recyclerView) throws Throwable {
339         setRecyclerView(recyclerView, true);
340     }
setRecyclerView(final RecyclerView recyclerView, boolean assignDummyPool)341     public void setRecyclerView(final RecyclerView recyclerView, boolean assignDummyPool)
342             throws Throwable {
343         setRecyclerView(recyclerView, assignDummyPool, true);
344     }
setRecyclerView(final RecyclerView recyclerView, boolean assignDummyPool, boolean addPositionCheckItemAnimator)345     public void setRecyclerView(final RecyclerView recyclerView, boolean assignDummyPool,
346             boolean addPositionCheckItemAnimator)
347             throws Throwable {
348         mRecyclerView = recyclerView;
349         if (assignDummyPool) {
350             RecyclerView.RecycledViewPool pool = new RecyclerView.RecycledViewPool() {
351                 @Override
352                 public RecyclerView.ViewHolder getRecycledView(int viewType) {
353                     RecyclerView.ViewHolder viewHolder = super.getRecycledView(viewType);
354                     if (viewHolder == null) {
355                         return null;
356                     }
357                     viewHolder.addFlags(RecyclerView.ViewHolder.FLAG_BOUND);
358                     viewHolder.mPosition = 200;
359                     viewHolder.mOldPosition = 300;
360                     viewHolder.mPreLayoutPosition = 500;
361                     return viewHolder;
362                 }
363 
364                 @Override
365                 public void putRecycledView(RecyclerView.ViewHolder scrap) {
366                     assertNull(scrap.mOwnerRecyclerView);
367                     super.putRecycledView(scrap);
368                 }
369             };
370             mRecyclerView.setRecycledViewPool(pool);
371         }
372         if (addPositionCheckItemAnimator) {
373             mRecyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
374                 @Override
375                 public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
376                         RecyclerView.State state) {
377                     RecyclerView.ViewHolder vh = parent.getChildViewHolder(view);
378                     if (!vh.isRemoved()) {
379                         assertNotSame("If getItemOffsets is called, child should have a valid"
380                                         + " adapter position unless it is removed : " + vh,
381                                 vh.getAdapterPosition(), RecyclerView.NO_POSITION);
382                     }
383                 }
384             });
385         }
386         mAdapterHelper = recyclerView.mAdapterHelper;
387         mActivityRule.runOnUiThread(new Runnable() {
388             @Override
389             public void run() {
390                 getActivity().getContainer().addView(recyclerView);
391             }
392         });
393     }
394 
getRecyclerViewContainer()395     protected FrameLayout getRecyclerViewContainer() {
396         return getActivity().getContainer();
397     }
398 
requestLayoutOnUIThread(final View view)399     protected void requestLayoutOnUIThread(final View view) throws Throwable {
400         mActivityRule.runOnUiThread(new Runnable() {
401             @Override
402             public void run() {
403                 view.requestLayout();
404             }
405         });
406     }
407 
scrollBy(final int dt)408     protected void scrollBy(final int dt) throws Throwable {
409         mActivityRule.runOnUiThread(new Runnable() {
410             @Override
411             public void run() {
412                 if (mRecyclerView.getLayoutManager().canScrollHorizontally()) {
413                     mRecyclerView.scrollBy(dt, 0);
414                 } else {
415                     mRecyclerView.scrollBy(0, dt);
416                 }
417 
418             }
419         });
420     }
421 
smoothScrollBy(final int dt)422     protected void smoothScrollBy(final int dt) throws Throwable {
423         mActivityRule.runOnUiThread(new Runnable() {
424             @Override
425             public void run() {
426                 if (mRecyclerView.getLayoutManager().canScrollHorizontally()) {
427                     mRecyclerView.smoothScrollBy(dt, 0);
428                 } else {
429                     mRecyclerView.smoothScrollBy(0, dt);
430                 }
431 
432             }
433         });
434         getInstrumentation().waitForIdleSync();
435     }
436 
scrollToPosition(final int position)437     void scrollToPosition(final int position) throws Throwable {
438         mActivityRule.runOnUiThread(new Runnable() {
439             @Override
440             public void run() {
441                 mRecyclerView.getLayoutManager().scrollToPosition(position);
442             }
443         });
444     }
445 
smoothScrollToPosition(final int position)446     void smoothScrollToPosition(final int position) throws Throwable {
447         smoothScrollToPosition(position, true);
448     }
449 
smoothScrollToPosition(final int position, boolean assertArrival)450     void smoothScrollToPosition(final int position, boolean assertArrival) throws Throwable {
451         if (mDebug) {
452             Log.d(TAG, "SMOOTH scrolling to " + position);
453         }
454         final CountDownLatch viewAdded = new CountDownLatch(1);
455         final RecyclerView.OnChildAttachStateChangeListener listener =
456                 new RecyclerView.OnChildAttachStateChangeListener() {
457                     @Override
458                     public void onChildViewAttachedToWindow(View view) {
459                         if (position == mRecyclerView.getChildAdapterPosition(view)) {
460                             viewAdded.countDown();
461                         }
462                     }
463                     @Override
464                     public void onChildViewDetachedFromWindow(View view) {
465                     }
466                 };
467         final AtomicBoolean addedListener = new AtomicBoolean(false);
468         mActivityRule.runOnUiThread(new Runnable() {
469             @Override
470             public void run() {
471                 RecyclerView.ViewHolder viewHolderForAdapterPosition =
472                         mRecyclerView.findViewHolderForAdapterPosition(position);
473                 if (viewHolderForAdapterPosition != null) {
474                     viewAdded.countDown();
475                 } else {
476                     mRecyclerView.addOnChildAttachStateChangeListener(listener);
477                     addedListener.set(true);
478                 }
479 
480             }
481         });
482         mActivityRule.runOnUiThread(new Runnable() {
483             @Override
484             public void run() {
485                 mRecyclerView.smoothScrollToPosition(position);
486             }
487         });
488         getInstrumentation().waitForIdleSync();
489         assertThat("should be able to scroll in 10 seconds", !assertArrival ||
490                         viewAdded.await(10, TimeUnit.SECONDS),
491                 CoreMatchers.is(true));
492         waitForIdleScroll(mRecyclerView);
493         if (mDebug) {
494             Log.d(TAG, "SMOOTH scrolling done");
495         }
496         if (addedListener.get()) {
497             mActivityRule.runOnUiThread(new Runnable() {
498                 @Override
499                 public void run() {
500                     mRecyclerView.removeOnChildAttachStateChangeListener(listener);
501                 }
502             });
503         }
504         getInstrumentation().waitForIdleSync();
505     }
506 
freezeLayout(final boolean freeze)507     void freezeLayout(final boolean freeze) throws Throwable {
508         mActivityRule.runOnUiThread(new Runnable() {
509             @Override
510             public void run() {
511                 mRecyclerView.setLayoutFrozen(freeze);
512             }
513         });
514     }
515 
setVisibility(final View view, final int visibility)516     public void setVisibility(final View view, final int visibility) throws Throwable {
517         mActivityRule.runOnUiThread(new Runnable() {
518             @Override
519             public void run() {
520                 view.setVisibility(visibility);
521             }
522         });
523     }
524 
525     public class TestViewHolder extends RecyclerView.ViewHolder {
526 
527         Item mBoundItem;
528         Object mData;
529 
TestViewHolder(View itemView)530         public TestViewHolder(View itemView) {
531             super(itemView);
532             itemView.setFocusable(true);
533         }
534 
535         @Override
toString()536         public String toString() {
537             return super.toString() + " item:" + mBoundItem + ", data:" + mData;
538         }
539 
getData()540         public Object getData() {
541             return mData;
542         }
543 
setData(Object data)544         public void setData(Object data) {
545             mData = data;
546         }
547     }
548     class DumbLayoutManager extends TestLayoutManager {
549         @Override
onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)550         public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
551             detachAndScrapAttachedViews(recycler);
552             layoutRange(recycler, 0, state.getItemCount());
553             if (layoutLatch != null) {
554                 layoutLatch.countDown();
555             }
556         }
557     }
558 
559     public class TestLayoutManager extends RecyclerView.LayoutManager {
560         int mScrollVerticallyAmount;
561         int mScrollHorizontallyAmount;
562         protected CountDownLatch layoutLatch;
563         private boolean mSupportsPredictive = false;
564 
expectLayouts(int count)565         public void expectLayouts(int count) {
566             layoutLatch = new CountDownLatch(count);
567         }
568 
waitForLayout(int seconds)569         public void waitForLayout(int seconds) throws Throwable {
570             layoutLatch.await(seconds * (mDebug ? 1000 : 1), SECONDS);
571             checkForMainThreadException();
572             MatcherAssert.assertThat("all layouts should complete on time",
573                     layoutLatch.getCount(), CoreMatchers.is(0L));
574             // use a runnable to ensure RV layout is finished
575             getInstrumentation().runOnMainSync(new Runnable() {
576                 @Override
577                 public void run() {
578                 }
579             });
580         }
581 
isSupportsPredictive()582         public boolean isSupportsPredictive() {
583             return mSupportsPredictive;
584         }
585 
setSupportsPredictive(boolean supportsPredictive)586         public void setSupportsPredictive(boolean supportsPredictive) {
587             mSupportsPredictive = supportsPredictive;
588         }
589 
590         @Override
supportsPredictiveItemAnimations()591         public boolean supportsPredictiveItemAnimations() {
592             return mSupportsPredictive;
593         }
594 
assertLayoutCount(int count, String msg, long timeout)595         public void assertLayoutCount(int count, String msg, long timeout) throws Throwable {
596             layoutLatch.await(timeout, TimeUnit.SECONDS);
597             assertEquals(msg, count, layoutLatch.getCount());
598         }
599 
assertNoLayout(String msg, long timeout)600         public void assertNoLayout(String msg, long timeout) throws Throwable {
601             layoutLatch.await(timeout, TimeUnit.SECONDS);
602             assertFalse(msg, layoutLatch.getCount() == 0);
603         }
604 
605         @Override
generateDefaultLayoutParams()606         public RecyclerView.LayoutParams generateDefaultLayoutParams() {
607             return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
608                     ViewGroup.LayoutParams.WRAP_CONTENT);
609         }
610 
assertVisibleItemPositions()611         void assertVisibleItemPositions() {
612             int i = getChildCount();
613             TestAdapter testAdapter = (TestAdapter) mRecyclerView.getAdapter();
614             while (i-- > 0) {
615                 View view = getChildAt(i);
616                 RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) view.getLayoutParams();
617                 Item item = ((TestViewHolder) lp.mViewHolder).mBoundItem;
618                 if (mDebug) {
619                     Log.d(TAG, "testing item " + i);
620                 }
621                 if (!lp.isItemRemoved()) {
622                     RecyclerView.ViewHolder vh = mRecyclerView.getChildViewHolder(view);
623                     assertSame("item position in LP should match adapter value :" + vh,
624                             testAdapter.mItems.get(vh.mPosition), item);
625                 }
626             }
627         }
628 
getLp(View v)629         RecyclerView.LayoutParams getLp(View v) {
630             return (RecyclerView.LayoutParams) v.getLayoutParams();
631         }
632 
layoutRange(RecyclerView.Recycler recycler, int start, int end)633         protected void layoutRange(RecyclerView.Recycler recycler, int start, int end) {
634             assertScrap(recycler);
635             if (mDebug) {
636                 Log.d(TAG, "will layout items from " + start + " to " + end);
637             }
638             int diff = end > start ? 1 : -1;
639             int top = 0;
640             for (int i = start; i != end; i+=diff) {
641                 if (mDebug) {
642                     Log.d(TAG, "laying out item " + i);
643                 }
644                 View view = recycler.getViewForPosition(i);
645                 assertNotNull("view should not be null for valid position. "
646                         + "got null view at position " + i, view);
647                 if (!mRecyclerView.mState.isPreLayout()) {
648                     RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) view
649                             .getLayoutParams();
650                     assertFalse("In post layout, getViewForPosition should never return a view "
651                             + "that is removed", layoutParams != null
652                             && layoutParams.isItemRemoved());
653 
654                 }
655                 assertEquals("getViewForPosition should return correct position",
656                         i, getPosition(view));
657                 addView(view);
658                 measureChildWithMargins(view, 0, 0);
659                 if (getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL) {
660                     layoutDecorated(view, getWidth() - getDecoratedMeasuredWidth(view), top,
661                             getWidth(), top + getDecoratedMeasuredHeight(view));
662                 } else {
663                     layoutDecorated(view, 0, top, getDecoratedMeasuredWidth(view)
664                             , top + getDecoratedMeasuredHeight(view));
665                 }
666 
667                 top += view.getMeasuredHeight();
668             }
669         }
670 
assertScrap(RecyclerView.Recycler recycler)671         private void assertScrap(RecyclerView.Recycler recycler) {
672             if (mRecyclerView.getAdapter() != null &&
673                     !mRecyclerView.getAdapter().hasStableIds()) {
674                 for (RecyclerView.ViewHolder viewHolder : recycler.getScrapList()) {
675                     assertFalse("Invalid scrap should be no kept", viewHolder.isInvalid());
676                 }
677             }
678         }
679 
680         @Override
canScrollHorizontally()681         public boolean canScrollHorizontally() {
682             return true;
683         }
684 
685         @Override
canScrollVertically()686         public boolean canScrollVertically() {
687             return true;
688         }
689 
690         @Override
scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state)691         public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
692                 RecyclerView.State state) {
693             mScrollHorizontallyAmount += dx;
694             return dx;
695         }
696 
697         @Override
scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state)698         public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
699                 RecyclerView.State state) {
700             mScrollVerticallyAmount += dy;
701             return dy;
702         }
703 
704         // START MOCKITO OVERRIDES
705         // We override package protected methods to make them public. This is necessary to run
706         // mockito on Kitkat
707         @Override
setRecyclerView(RecyclerView recyclerView)708         public void setRecyclerView(RecyclerView recyclerView) {
709             super.setRecyclerView(recyclerView);
710         }
711 
712         @Override
dispatchAttachedToWindow(RecyclerView view)713         public void dispatchAttachedToWindow(RecyclerView view) {
714             super.dispatchAttachedToWindow(view);
715         }
716 
717         @Override
dispatchDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler)718         public void dispatchDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) {
719             super.dispatchDetachedFromWindow(view, recycler);
720         }
721 
722         @Override
setExactMeasureSpecsFrom(RecyclerView recyclerView)723         public void setExactMeasureSpecsFrom(RecyclerView recyclerView) {
724             super.setExactMeasureSpecsFrom(recyclerView);
725         }
726 
727         @Override
setMeasureSpecs(int wSpec, int hSpec)728         public void setMeasureSpecs(int wSpec, int hSpec) {
729             super.setMeasureSpecs(wSpec, hSpec);
730         }
731 
732         @Override
setMeasuredDimensionFromChildren(int widthSpec, int heightSpec)733         public void setMeasuredDimensionFromChildren(int widthSpec, int heightSpec) {
734             super.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
735         }
736 
737         @Override
shouldReMeasureChild(View child, int widthSpec, int heightSpec, RecyclerView.LayoutParams lp)738         public boolean shouldReMeasureChild(View child, int widthSpec, int heightSpec,
739                 RecyclerView.LayoutParams lp) {
740             return super.shouldReMeasureChild(child, widthSpec, heightSpec, lp);
741         }
742 
743         @Override
shouldMeasureChild(View child, int widthSpec, int heightSpec, RecyclerView.LayoutParams lp)744         public boolean shouldMeasureChild(View child, int widthSpec, int heightSpec,
745                 RecyclerView.LayoutParams lp) {
746             return super.shouldMeasureChild(child, widthSpec, heightSpec, lp);
747         }
748 
749         @Override
removeAndRecycleScrapInt(RecyclerView.Recycler recycler)750         public void removeAndRecycleScrapInt(RecyclerView.Recycler recycler) {
751             super.removeAndRecycleScrapInt(recycler);
752         }
753 
754         @Override
stopSmoothScroller()755         public void stopSmoothScroller() {
756             super.stopSmoothScroller();
757         }
758 
759         // END MOCKITO OVERRIDES
760     }
761 
762     static class Item {
763         final static AtomicInteger idCounter = new AtomicInteger(0);
764         final public int mId = idCounter.incrementAndGet();
765 
766         int mAdapterIndex;
767 
768         String mText;
769         int mType = 0;
770         boolean mFocusable;
771 
Item(int adapterIndex, String text)772         Item(int adapterIndex, String text) {
773             mAdapterIndex = adapterIndex;
774             mText = text;
775             mFocusable = true;
776         }
777 
isFocusable()778         public boolean isFocusable() {
779             return mFocusable;
780         }
781 
setFocusable(boolean mFocusable)782         public void setFocusable(boolean mFocusable) {
783             this.mFocusable = mFocusable;
784         }
785 
786         @Override
toString()787         public String toString() {
788             return "Item{" +
789                     "mId=" + mId +
790                     ", originalIndex=" + mAdapterIndex +
791                     ", text='" + mText + '\'' +
792                     '}';
793         }
794     }
795 
796     public class TestAdapter extends RecyclerView.Adapter<TestViewHolder>
797             implements AttachDetachCountingAdapter {
798 
799         public static final String DEFAULT_ITEM_PREFIX = "Item ";
800 
801         ViewAttachDetachCounter mAttachmentCounter = new ViewAttachDetachCounter();
802         List<Item> mItems;
803         final @Nullable RecyclerView.LayoutParams mLayoutParams;
804 
TestAdapter(int count)805         public TestAdapter(int count) {
806             this(count, null);
807         }
808 
TestAdapter(int count, @Nullable RecyclerView.LayoutParams layoutParams)809         public TestAdapter(int count, @Nullable RecyclerView.LayoutParams layoutParams) {
810             mItems = new ArrayList<Item>(count);
811             addItems(0, count, DEFAULT_ITEM_PREFIX);
812             mLayoutParams = layoutParams;
813         }
814 
addItems(int pos, int count, String prefix)815         private void addItems(int pos, int count, String prefix) {
816             for (int i = 0; i < count; i++, pos++) {
817                 mItems.add(pos, new Item(pos, prefix));
818             }
819         }
820 
821         @Override
getItemViewType(int position)822         public int getItemViewType(int position) {
823             return getItemAt(position).mType;
824         }
825 
826         @Override
onViewAttachedToWindow(TestViewHolder holder)827         public void onViewAttachedToWindow(TestViewHolder holder) {
828             super.onViewAttachedToWindow(holder);
829             mAttachmentCounter.onViewAttached(holder);
830         }
831 
832         @Override
onViewDetachedFromWindow(TestViewHolder holder)833         public void onViewDetachedFromWindow(TestViewHolder holder) {
834             super.onViewDetachedFromWindow(holder);
835             mAttachmentCounter.onViewDetached(holder);
836         }
837 
838         @Override
onAttachedToRecyclerView(RecyclerView recyclerView)839         public void onAttachedToRecyclerView(RecyclerView recyclerView) {
840             super.onAttachedToRecyclerView(recyclerView);
841             mAttachmentCounter.onAttached(recyclerView);
842         }
843 
844         @Override
onDetachedFromRecyclerView(RecyclerView recyclerView)845         public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
846             super.onDetachedFromRecyclerView(recyclerView);
847             mAttachmentCounter.onDetached(recyclerView);
848         }
849 
850         @Override
onCreateViewHolder(ViewGroup parent, int viewType)851         public TestViewHolder onCreateViewHolder(ViewGroup parent,
852                 int viewType) {
853             TextView itemView = new TextView(parent.getContext());
854             itemView.setFocusableInTouchMode(true);
855             itemView.setFocusable(true);
856             return new TestViewHolder(itemView);
857         }
858 
859         @Override
onBindViewHolder(TestViewHolder holder, int position)860         public void onBindViewHolder(TestViewHolder holder, int position) {
861             assertNotNull(holder.mOwnerRecyclerView);
862             assertEquals(position, holder.getAdapterPosition());
863             final Item item = mItems.get(position);
864             ((TextView) (holder.itemView)).setText(item.mText + "(" + item.mId + ")");
865             holder.mBoundItem = item;
866             if (mLayoutParams != null) {
867                 holder.itemView.setLayoutParams(new RecyclerView.LayoutParams(mLayoutParams));
868             }
869         }
870 
getItemAt(int position)871         public Item getItemAt(int position) {
872             return mItems.get(position);
873         }
874 
875         @Override
onViewRecycled(TestViewHolder holder)876         public void onViewRecycled(TestViewHolder holder) {
877             super.onViewRecycled(holder);
878             final int adapterPosition = holder.getAdapterPosition();
879             final boolean shouldHavePosition = !holder.isRemoved() && holder.isBound() &&
880                     !holder.isAdapterPositionUnknown() && !holder.isInvalid();
881             String log = "Position check for " + holder.toString();
882             assertEquals(log, shouldHavePosition, adapterPosition != RecyclerView.NO_POSITION);
883             if (shouldHavePosition) {
884                 assertTrue(log, mItems.size() > adapterPosition);
885                 // TODO: fix b/36042615 getAdapterPosition() is wrong in
886                 // consumePendingUpdatesInOnePass where it applies pending change to already
887                 // modified position.
888                 if (holder.mPreLayoutPosition == RecyclerView.NO_POSITION) {
889                     assertSame(log, holder.mBoundItem, mItems.get(adapterPosition));
890                 }
891             }
892         }
893 
deleteAndNotify(final int start, final int count)894         public void deleteAndNotify(final int start, final int count) throws Throwable {
895             deleteAndNotify(new int[]{start, count});
896         }
897 
898         /**
899          * Deletes items in the given ranges.
900          * <p>
901          * Note that each operation affects the one after so you should offset them properly.
902          * <p>
903          * For example, if adapter has 5 items (A,B,C,D,E), and then you call this method with
904          * <code>[1, 2],[2, 1]</code>, it will first delete items B,C and the new adapter will be
905          * A D E. Then it will delete 2,1 which means it will delete E.
906          */
deleteAndNotify(final int[]... startCountTuples)907         public void deleteAndNotify(final int[]... startCountTuples) throws Throwable {
908             for (int[] tuple : startCountTuples) {
909                 tuple[1] = -tuple[1];
910             }
911             mActivityRule.runOnUiThread(new AddRemoveRunnable(startCountTuples));
912         }
913 
914         @Override
getItemId(int position)915         public long getItemId(int position) {
916             return hasStableIds() ? mItems.get(position).mId : super.getItemId(position);
917         }
918 
offsetOriginalIndices(int start, int offset)919         public void offsetOriginalIndices(int start, int offset) {
920             for (int i = start; i < mItems.size(); i++) {
921                 mItems.get(i).mAdapterIndex += offset;
922             }
923         }
924 
925         /**
926          * @param start inclusive
927          * @param end exclusive
928          * @param offset
929          */
offsetOriginalIndicesBetween(int start, int end, int offset)930         public void offsetOriginalIndicesBetween(int start, int end, int offset) {
931             for (int i = start; i < end && i < mItems.size(); i++) {
932                 mItems.get(i).mAdapterIndex += offset;
933             }
934         }
935 
addAndNotify(final int count)936         public void addAndNotify(final int count) throws Throwable {
937             assertEquals(0, mItems.size());
938             mActivityRule.runOnUiThread(
939                     new AddRemoveRunnable(DEFAULT_ITEM_PREFIX, new int[]{0, count}));
940         }
941 
resetItemsTo(final List<Item> testItems)942         public void resetItemsTo(final List<Item> testItems) throws Throwable {
943             if (!mItems.isEmpty()) {
944                 deleteAndNotify(0, mItems.size());
945             }
946             mItems = testItems;
947             mActivityRule.runOnUiThread(new Runnable() {
948                 @Override
949                 public void run() {
950                     notifyItemRangeInserted(0, testItems.size());
951                 }
952             });
953         }
954 
addAndNotify(final int start, final int count)955         public void addAndNotify(final int start, final int count) throws Throwable {
956             addAndNotify(new int[]{start, count});
957         }
958 
addAndNotify(final int[]... startCountTuples)959         public void addAndNotify(final int[]... startCountTuples) throws Throwable {
960             mActivityRule.runOnUiThread(new AddRemoveRunnable(startCountTuples));
961         }
962 
dispatchDataSetChanged()963         public void dispatchDataSetChanged() throws Throwable {
964             mActivityRule.runOnUiThread(new Runnable() {
965                 @Override
966                 public void run() {
967                     notifyDataSetChanged();
968                 }
969             });
970         }
971 
changeAndNotify(final int start, final int count)972         public void changeAndNotify(final int start, final int count) throws Throwable {
973             mActivityRule.runOnUiThread(new Runnable() {
974                 @Override
975                 public void run() {
976                     notifyItemRangeChanged(start, count);
977                 }
978             });
979         }
980 
changeAndNotifyWithPayload(final int start, final int count, final Object payload)981         public void changeAndNotifyWithPayload(final int start, final int count,
982                 final Object payload) throws Throwable {
983             mActivityRule.runOnUiThread(new Runnable() {
984                 @Override
985                 public void run() {
986                     notifyItemRangeChanged(start, count, payload);
987                 }
988             });
989         }
990 
changePositionsAndNotify(final int... positions)991         public void changePositionsAndNotify(final int... positions) throws Throwable {
992             mActivityRule.runOnUiThread(new Runnable() {
993                 @Override
994                 public void run() {
995                     for (int i = 0; i < positions.length; i += 1) {
996                         TestAdapter.super.notifyItemRangeChanged(positions[i], 1);
997                     }
998                 }
999             });
1000         }
1001 
1002         /**
1003          * Similar to other methods but negative count means delete and position count means add.
1004          * <p>
1005          * For instance, calling this method with <code>[1,1], [2,-1]</code> it will first add an
1006          * item to index 1, then remove an item from index 2 (updated index 2)
1007          */
addDeleteAndNotify(final int[]... startCountTuples)1008         public void addDeleteAndNotify(final int[]... startCountTuples) throws Throwable {
1009             mActivityRule.runOnUiThread(new AddRemoveRunnable(startCountTuples));
1010         }
1011 
1012         @Override
getItemCount()1013         public int getItemCount() {
1014             return mItems.size();
1015         }
1016 
1017         /**
1018          * Uses notifyDataSetChanged
1019          */
moveItems(boolean notifyChange, int[]... fromToTuples)1020         public void moveItems(boolean notifyChange, int[]... fromToTuples) throws Throwable {
1021             for (int i = 0; i < fromToTuples.length; i += 1) {
1022                 int[] tuple = fromToTuples[i];
1023                 moveItem(tuple[0], tuple[1], false);
1024             }
1025             if (notifyChange) {
1026                 dispatchDataSetChanged();
1027             }
1028         }
1029 
1030         /**
1031          * Uses notifyDataSetChanged
1032          */
moveItem(final int from, final int to, final boolean notifyChange)1033         public void moveItem(final int from, final int to, final boolean notifyChange)
1034                 throws Throwable {
1035             mActivityRule.runOnUiThread(new Runnable() {
1036                 @Override
1037                 public void run() {
1038                     moveInUIThread(from, to);
1039                     if (notifyChange) {
1040                         notifyDataSetChanged();
1041                     }
1042                 }
1043             });
1044         }
1045 
1046         /**
1047          * Uses notifyItemMoved
1048          */
moveAndNotify(final int from, final int to)1049         public void moveAndNotify(final int from, final int to) throws Throwable {
1050             mActivityRule.runOnUiThread(new Runnable() {
1051                 @Override
1052                 public void run() {
1053                     moveInUIThread(from, to);
1054                     notifyItemMoved(from, to);
1055                 }
1056             });
1057         }
1058 
clearOnUIThread()1059         public void clearOnUIThread() {
1060             assertEquals("clearOnUIThread called from a wrong thread",
1061                     Looper.getMainLooper(), Looper.myLooper());
1062             mItems = new ArrayList<Item>();
1063             notifyDataSetChanged();
1064         }
1065 
moveInUIThread(int from, int to)1066         protected void moveInUIThread(int from, int to) {
1067             Item item = mItems.remove(from);
1068             offsetOriginalIndices(from, -1);
1069             mItems.add(to, item);
1070             offsetOriginalIndices(to + 1, 1);
1071             item.mAdapterIndex = to;
1072         }
1073 
1074 
1075         @Override
getCounter()1076         public ViewAttachDetachCounter getCounter() {
1077             return mAttachmentCounter;
1078         }
1079 
1080         private class AddRemoveRunnable implements Runnable {
1081             final String mNewItemPrefix;
1082             final int[][] mStartCountTuples;
1083 
AddRemoveRunnable(String newItemPrefix, int[]... startCountTuples)1084             public AddRemoveRunnable(String newItemPrefix, int[]... startCountTuples) {
1085                 mNewItemPrefix = newItemPrefix;
1086                 mStartCountTuples = startCountTuples;
1087             }
1088 
AddRemoveRunnable(int[][] startCountTuples)1089             public AddRemoveRunnable(int[][] startCountTuples) {
1090                 this("new item ", startCountTuples);
1091             }
1092 
1093             @Override
run()1094             public void run() {
1095                 for (int[] tuple : mStartCountTuples) {
1096                     if (tuple[1] < 0) {
1097                         delete(tuple);
1098                     } else {
1099                         add(tuple);
1100                     }
1101                 }
1102             }
1103 
add(int[] tuple)1104             private void add(int[] tuple) {
1105                 // offset others
1106                 offsetOriginalIndices(tuple[0], tuple[1]);
1107                 addItems(tuple[0], tuple[1], mNewItemPrefix);
1108                 notifyItemRangeInserted(tuple[0], tuple[1]);
1109             }
1110 
delete(int[] tuple)1111             private void delete(int[] tuple) {
1112                 final int count = -tuple[1];
1113                 offsetOriginalIndices(tuple[0] + count, tuple[1]);
1114                 for (int i = 0; i < count; i++) {
1115                     mItems.remove(tuple[0]);
1116                 }
1117                 notifyItemRangeRemoved(tuple[0], count);
1118             }
1119         }
1120     }
1121 
isMainThread()1122     public boolean isMainThread() {
1123         return Looper.myLooper() == Looper.getMainLooper();
1124     }
1125 
1126     static class TargetTuple {
1127 
1128         final int mPosition;
1129 
1130         final int mLayoutDirection;
1131 
TargetTuple(int position, int layoutDirection)1132         TargetTuple(int position, int layoutDirection) {
1133             this.mPosition = position;
1134             this.mLayoutDirection = layoutDirection;
1135         }
1136 
1137         @Override
toString()1138         public String toString() {
1139             return "TargetTuple{" +
1140                     "mPosition=" + mPosition +
1141                     ", mLayoutDirection=" + mLayoutDirection +
1142                     '}';
1143         }
1144     }
1145 
1146     public interface AttachDetachCountingAdapter {
1147 
getCounter()1148         ViewAttachDetachCounter getCounter();
1149     }
1150 
1151     public class ViewAttachDetachCounter {
1152 
1153         Set<RecyclerView.ViewHolder> mAttachedSet = new HashSet<RecyclerView.ViewHolder>();
1154 
validateRemaining(RecyclerView recyclerView)1155         public void validateRemaining(RecyclerView recyclerView) {
1156             final int childCount = recyclerView.getChildCount();
1157             for (int i = 0; i < childCount; i++) {
1158                 View view = recyclerView.getChildAt(i);
1159                 RecyclerView.ViewHolder vh = recyclerView.getChildViewHolder(view);
1160                 assertTrue("remaining view should be in attached set " + vh,
1161                         mAttachedSet.contains(vh));
1162             }
1163             assertEquals("there should not be any views left in attached set",
1164                     childCount, mAttachedSet.size());
1165         }
1166 
onViewDetached(RecyclerView.ViewHolder viewHolder)1167         public void onViewDetached(RecyclerView.ViewHolder viewHolder) {
1168             try {
1169                 assertTrue("view holder should be in attached set",
1170                         mAttachedSet.remove(viewHolder));
1171             } catch (Throwable t) {
1172                 postExceptionToInstrumentation(t);
1173             }
1174         }
1175 
onViewAttached(RecyclerView.ViewHolder viewHolder)1176         public void onViewAttached(RecyclerView.ViewHolder viewHolder) {
1177             try {
1178                 assertTrue("view holder should not be in attached set",
1179                         mAttachedSet.add(viewHolder));
1180             } catch (Throwable t) {
1181                 postExceptionToInstrumentation(t);
1182             }
1183         }
1184 
onAttached(RecyclerView recyclerView)1185         public void onAttached(RecyclerView recyclerView) {
1186             // when a new RV is attached, clear the set and add all view holders
1187             mAttachedSet.clear();
1188             final int childCount = recyclerView.getChildCount();
1189             for (int i = 0; i < childCount; i ++) {
1190                 View view = recyclerView.getChildAt(i);
1191                 mAttachedSet.add(recyclerView.getChildViewHolder(view));
1192             }
1193         }
1194 
onDetached(RecyclerView recyclerView)1195         public void onDetached(RecyclerView recyclerView) {
1196             validateRemaining(recyclerView);
1197         }
1198     }
1199 
1200     /**
1201      * Returns whether a child of RecyclerView is partially in bound. A child is
1202      * partially in-bounds if it's either fully or partially visible on the screen.
1203      * @param parent The RecyclerView holding the child.
1204      * @param child The child view to be checked whether is partially (or fully) within RV's bounds.
1205      * @return True if the child view is partially (or fully) visible; false otherwise.
1206      */
isViewPartiallyInBound(RecyclerView parent, View child)1207     public static boolean isViewPartiallyInBound(RecyclerView parent, View child) {
1208         if (child == null) {
1209             return false;
1210         }
1211         final int parentLeft = parent.getPaddingLeft();
1212         final int parentTop = parent.getPaddingTop();
1213         final int parentRight = parent.getWidth() - parent.getPaddingRight();
1214         final int parentBottom = parent.getHeight() - parent.getPaddingBottom();
1215 
1216         final int childLeft = child.getLeft() - child.getScrollX();
1217         final int childTop = child.getTop() - child.getScrollY();
1218         final int childRight = child.getRight() - child.getScrollX();
1219         final int childBottom = child.getBottom() - child.getScrollY();
1220 
1221         if (childLeft >= parentRight || childRight <= parentLeft
1222                 || childTop >= parentBottom || childBottom <= parentTop) {
1223             return false;
1224         }
1225         return true;
1226     }
1227 
1228     /**
1229      * Returns whether a child of RecyclerView is fully in-bounds, that is it's fully visible
1230      * on the screen.
1231      * @param parent The RecyclerView holding the child.
1232      * @param child The child view to be checked whether is fully within RV's bounds.
1233      * @return True if the child view is fully visible; false otherwise.
1234      */
isViewFullyInBound(RecyclerView parent, View child)1235     public boolean isViewFullyInBound(RecyclerView parent, View child) {
1236         if (child == null) {
1237             return false;
1238         }
1239         final int parentLeft = parent.getPaddingLeft();
1240         final int parentTop = parent.getPaddingTop();
1241         final int parentRight = parent.getWidth() - parent.getPaddingRight();
1242         final int parentBottom = parent.getHeight() - parent.getPaddingBottom();
1243 
1244         final int childLeft = child.getLeft() - child.getScrollX();
1245         final int childTop = child.getTop() - child.getScrollY();
1246         final int childRight = child.getRight() - child.getScrollX();
1247         final int childBottom = child.getBottom() - child.getScrollY();
1248 
1249         if (childLeft >= parentLeft && childRight <= parentRight
1250                 && childTop >= parentTop && childBottom <= parentBottom) {
1251             return true;
1252         }
1253         return false;
1254     }
1255 }
1256