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