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