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