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