BaseRecyclerViewInstrumentationTest.java revision 178c4e15a0ebb501d2bb4f3ac3176e4c44954954
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
17package android.support.v7.widget;
18
19import static android.support.v7.widget.RecyclerView.SCROLL_STATE_IDLE;
20
21import org.hamcrest.CoreMatchers;
22import org.hamcrest.MatcherAssert;
23import org.junit.After;
24import org.junit.Before;
25import org.junit.Rule;
26
27import android.app.Instrumentation;
28import android.graphics.Rect;
29import android.os.Looper;
30import android.support.test.InstrumentationRegistry;
31import android.support.test.rule.ActivityTestRule;
32import android.support.v4.view.ViewCompat;
33import android.support.v7.recyclerview.test.SameActivityTestRule;
34import android.util.Log;
35import android.view.LayoutInflater;
36import android.view.View;
37import android.view.ViewGroup;
38import android.widget.FrameLayout;
39import android.widget.TextView;
40
41import java.lang.reflect.InvocationTargetException;
42import java.lang.reflect.Method;
43import java.util.ArrayList;
44import java.util.HashSet;
45import java.util.List;
46import java.util.Set;
47import java.util.concurrent.CountDownLatch;
48import java.util.concurrent.TimeUnit;
49import java.util.concurrent.atomic.AtomicBoolean;
50import java.util.concurrent.atomic.AtomicInteger;
51import java.util.concurrent.locks.ReentrantLock;
52import android.support.v7.recyclerview.test.R;
53
54import static org.junit.Assert.*;
55
56import static java.util.concurrent.TimeUnit.SECONDS;
57
58abstract 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
82    public BaseRecyclerViewInstrumentationTest() {
83        this(false);
84    }
85
86    public BaseRecyclerViewInstrumentationTest(boolean debug) {
87        mDebug = debug;
88    }
89
90    void checkForMainThreadException() throws Throwable {
91        if (!mIgnoreMainThreadException && mMainThreadException != null) {
92            throw mMainThreadException;
93        }
94    }
95
96    public void setIgnoreMainThreadException(boolean ignoreMainThreadException) {
97        mIgnoreMainThreadException = ignoreMainThreadException;
98    }
99
100    public Throwable getMainThreadException() {
101        return mMainThreadException;
102    }
103
104    protected TestActivity getActivity() {
105        return mActivityRule.getActivity();
106    }
107
108    @Before
109    public final void setUpInsThread() throws Exception {
110        mInstrumentationThread = Thread.currentThread();
111        Item.idCounter.set(0);
112    }
113
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
127    public boolean canReUseActivity() {
128        return true;
129    }
130
131    protected void enableAccessibility()
132            throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
133        Method getUIAutomation = Instrumentation.class.getMethod("getUiAutomation");
134        getUIAutomation.invoke(InstrumentationRegistry.getInstrumentation());
135    }
136
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
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
162    protected WrappedRecyclerView inflateWrappedRV() {
163        return (WrappedRecyclerView)
164                LayoutInflater.from(getActivity()).inflate(R.layout.wrapped_test_rv,
165                        getRecyclerViewContainer(), false);
166    }
167
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
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
206    public Instrumentation getInstrumentation() {
207        return InstrumentationRegistry.getInstrumentation();
208    }
209
210    @After
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
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
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
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
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
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
328    public void setRecyclerView(final RecyclerView recyclerView) throws Throwable {
329        setRecyclerView(recyclerView, true);
330    }
331    public void setRecyclerView(final RecyclerView recyclerView, boolean assignDummyPool)
332            throws Throwable {
333        setRecyclerView(recyclerView, assignDummyPool, true);
334    }
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
385    protected FrameLayout getRecyclerViewContainer() {
386        return getActivity().getContainer();
387    }
388
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
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
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
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
448    void smoothScrollToPosition(final int position) throws Throwable {
449        smoothScrollToPosition(position, true);
450    }
451
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
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
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
532        public TestViewHolder(View itemView) {
533            super(itemView);
534            itemView.setFocusable(true);
535        }
536
537        @Override
538        public String toString() {
539            return super.toString() + " item:" + mBoundItem + ", data:" + mData;
540        }
541
542        public Object getData() {
543            return mData;
544        }
545
546        public void setData(Object data) {
547            mData = data;
548        }
549    }
550    class DumbLayoutManager extends TestLayoutManager {
551        @Override
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
566        public void expectLayouts(int count) {
567            layoutLatch = new CountDownLatch(count);
568        }
569
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
583        public boolean isSupportsPredictive() {
584            return mSupportsPredictive;
585        }
586
587        public void setSupportsPredictive(boolean supportsPredictive) {
588            mSupportsPredictive = supportsPredictive;
589        }
590
591        @Override
592        public boolean supportsPredictiveItemAnimations() {
593            return mSupportsPredictive;
594        }
595
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
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
607        public RecyclerView.LayoutParams generateDefaultLayoutParams() {
608            return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
609                    ViewGroup.LayoutParams.WRAP_CONTENT);
610        }
611
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
630        RecyclerView.LayoutParams getLp(View v) {
631            return (RecyclerView.LayoutParams) v.getLayoutParams();
632        }
633
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
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
682        public boolean canScrollHorizontally() {
683            return true;
684        }
685
686        @Override
687        public boolean canScrollVertically() {
688            return true;
689        }
690
691        @Override
692        public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
693                RecyclerView.State state) {
694            mScrollHorizontallyAmount += dx;
695            return dx;
696        }
697
698        @Override
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
709        public void setRecyclerView(RecyclerView recyclerView) {
710            super.setRecyclerView(recyclerView);
711        }
712
713        @Override
714        public void dispatchAttachedToWindow(RecyclerView view) {
715            super.dispatchAttachedToWindow(view);
716        }
717
718        @Override
719        public void dispatchDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) {
720            super.dispatchDetachedFromWindow(view, recycler);
721        }
722
723        @Override
724        public void setExactMeasureSpecsFrom(RecyclerView recyclerView) {
725            super.setExactMeasureSpecsFrom(recyclerView);
726        }
727
728        @Override
729        public void setMeasureSpecs(int wSpec, int hSpec) {
730            super.setMeasureSpecs(wSpec, hSpec);
731        }
732
733        @Override
734        public void setMeasuredDimensionFromChildren(int widthSpec, int heightSpec) {
735            super.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
736        }
737
738        @Override
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
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
751        public void removeAndRecycleScrapInt(RecyclerView.Recycler recycler) {
752            super.removeAndRecycleScrapInt(recycler);
753        }
754
755        @Override
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
772        Item(int adapterIndex, String text) {
773            mAdapterIndex = adapterIndex;
774            mText = text;
775        }
776
777        @Override
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
795        public TestAdapter(int count) {
796            mItems = new ArrayList<Item>(count);
797            addItems(0, count, DEFAULT_ITEM_PREFIX);
798        }
799
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
807        public int getItemViewType(int position) {
808            return getItemAt(position).mType;
809        }
810
811        @Override
812        public void onViewAttachedToWindow(TestViewHolder holder) {
813            super.onViewAttachedToWindow(holder);
814            mAttachmentCounter.onViewAttached(holder);
815        }
816
817        @Override
818        public void onViewDetachedFromWindow(TestViewHolder holder) {
819            super.onViewDetachedFromWindow(holder);
820            mAttachmentCounter.onViewDetached(holder);
821        }
822
823        @Override
824        public void onAttachedToRecyclerView(RecyclerView recyclerView) {
825            super.onAttachedToRecyclerView(recyclerView);
826            mAttachmentCounter.onAttached(recyclerView);
827        }
828
829        @Override
830        public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
831            super.onDetachedFromRecyclerView(recyclerView);
832            mAttachmentCounter.onDetached(recyclerView);
833        }
834
835        @Override
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
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
853        public Item getItemAt(int position) {
854            return mItems.get(position);
855        }
856
857        @Override
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
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         */
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
892        public long getItemId(int position) {
893            return hasStableIds() ? mItems.get(position).mId : super.getItemId(position);
894        }
895
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         */
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
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
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
931        public void addAndNotify(final int start, final int count) throws Throwable {
932            addAndNotify(new int[]{start, count});
933        }
934
935        public void addAndNotify(final int[]... startCountTuples) throws Throwable {
936            new AddRemoveRunnable(startCountTuples).runOnMainThread();
937        }
938
939        public void dispatchDataSetChanged() throws Throwable {
940            runTestOnUiThread(new Runnable() {
941                @Override
942                public void run() {
943                    notifyDataSetChanged();
944                }
945            });
946        }
947
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
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
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         */
984        public void addDeleteAndNotify(final int[]... startCountTuples) throws Throwable {
985            new AddRemoveRunnable(startCountTuples).runOnMainThread();
986        }
987
988        @Override
989        public int getItemCount() {
990            return mItems.size();
991        }
992
993        /**
994         * Uses notifyDataSetChanged
995         */
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         */
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         */
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
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
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
1052        public ViewAttachDetachCounter getCounter() {
1053            return mAttachmentCounter;
1054        }
1055
1056        private class AddRemoveRunnable implements Runnable {
1057            final String mNewItemPrefix;
1058            final int[][] mStartCountTuples;
1059
1060            public AddRemoveRunnable(String newItemPrefix, int[]... startCountTuples) {
1061                mNewItemPrefix = newItemPrefix;
1062                mStartCountTuples = startCountTuples;
1063            }
1064
1065            public AddRemoveRunnable(int[][] startCountTuples) {
1066                this("new item ", startCountTuples);
1067            }
1068
1069            public void runOnMainThread() throws Throwable {
1070                if (Looper.myLooper() == Looper.getMainLooper()) {
1071                    run();
1072                } else {
1073                    runTestOnUiThread(this);
1074                }
1075            }
1076
1077            @Override
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
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
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
1106    public boolean isMainThread() {
1107        return Looper.myLooper() == Looper.getMainLooper();
1108    }
1109
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
1124        TargetTuple(int position, int layoutDirection) {
1125            this.mPosition = position;
1126            this.mLayoutDirection = layoutDirection;
1127        }
1128
1129        @Override
1130        public String toString() {
1131            return "TargetTuple{" +
1132                    "mPosition=" + mPosition +
1133                    ", mLayoutDirection=" + mLayoutDirection +
1134                    '}';
1135        }
1136    }
1137
1138    public interface AttachDetachCountingAdapter {
1139
1140        ViewAttachDetachCounter getCounter();
1141    }
1142
1143    public class ViewAttachDetachCounter {
1144
1145        Set<RecyclerView.ViewHolder> mAttachedSet = new HashSet<RecyclerView.ViewHolder>();
1146
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
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
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
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
1187        public void onDetached(RecyclerView recyclerView) {
1188            validateRemaining(recyclerView);
1189        }
1190    }
1191}
1192