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 android.graphics.Rect;
20import android.os.Looper;
21import android.support.v4.view.ViewCompat;
22import android.test.ActivityInstrumentationTestCase2;
23import android.util.Log;
24import android.view.View;
25import android.view.ViewGroup;
26import android.widget.FrameLayout;
27import android.widget.TextView;
28
29import java.util.ArrayList;
30import java.util.HashSet;
31import java.util.List;
32import java.util.Set;
33import java.util.concurrent.CountDownLatch;
34import java.util.concurrent.TimeUnit;
35import java.util.concurrent.atomic.AtomicInteger;
36
37abstract public class BaseRecyclerViewInstrumentationTest extends
38        ActivityInstrumentationTestCase2<TestActivity> {
39
40    private static final String TAG = "RecyclerViewTest";
41
42    private boolean mDebug;
43
44    protected RecyclerView mRecyclerView;
45
46    protected AdapterHelper mAdapterHelper;
47
48    Throwable mainThreadException;
49
50    public BaseRecyclerViewInstrumentationTest() {
51        this(false);
52    }
53
54    public BaseRecyclerViewInstrumentationTest(boolean debug) {
55        super("android.support.v7.recyclerview", TestActivity.class);
56        mDebug = debug;
57    }
58
59    void checkForMainThreadException() throws Throwable {
60        if (mainThreadException != null) {
61            throw mainThreadException;
62        }
63    }
64
65    void setHasTransientState(final View view, final boolean value) {
66        try {
67            runTestOnUiThread(new Runnable() {
68                @Override
69                public void run() {
70                    ViewCompat.setHasTransientState(view, value);
71                }
72            });
73        } catch (Throwable throwable) {
74            Log.e(TAG, "", throwable);
75        }
76    }
77
78    void setAdapter(final RecyclerView.Adapter adapter) throws Throwable {
79        runTestOnUiThread(new Runnable() {
80            @Override
81            public void run() {
82                mRecyclerView.setAdapter(adapter);
83            }
84        });
85    }
86
87    void swapAdapter(final RecyclerView.Adapter adapter,
88            final boolean removeAndRecycleExistingViews) throws Throwable {
89        runTestOnUiThread(new Runnable() {
90            @Override
91            public void run() {
92                try {
93                    mRecyclerView.swapAdapter(adapter, removeAndRecycleExistingViews);
94                } catch (Throwable t) {
95                    postExceptionToInstrumentation(t);
96                }
97            }
98        });
99        checkForMainThreadException();
100    }
101
102    void postExceptionToInstrumentation(Throwable t) {
103        if (mainThreadException != null) {
104            Log.e(TAG, "receiving another main thread exception. dropping.", t);
105        } else {
106            Log.e(TAG, "captured exception on main thread", t);
107            mainThreadException = t;
108        }
109
110        if (mRecyclerView != null && mRecyclerView
111                .getLayoutManager() instanceof TestLayoutManager) {
112            TestLayoutManager lm = (TestLayoutManager) mRecyclerView.getLayoutManager();
113            // finish all layouts so that we get the correct exception
114            while (lm.layoutLatch.getCount() > 0) {
115                lm.layoutLatch.countDown();
116            }
117        }
118    }
119
120    @Override
121    protected void tearDown() throws Exception {
122        if (mRecyclerView != null) {
123            try {
124                removeRecyclerView();
125            } catch (Throwable throwable) {
126                throwable.printStackTrace();
127            }
128        }
129        getInstrumentation().waitForIdleSync();
130        super.tearDown();
131
132        try {
133            checkForMainThreadException();
134        } catch (Exception e) {
135            throw e;
136        } catch (Throwable throwable) {
137            throw new Exception(throwable);
138        }
139    }
140
141    public Rect getDecoratedRecyclerViewBounds() {
142        return new Rect(
143                mRecyclerView.getPaddingLeft(),
144                mRecyclerView.getPaddingTop(),
145                mRecyclerView.getPaddingLeft() + mRecyclerView.getWidth(),
146                mRecyclerView.getPaddingTop() + mRecyclerView.getHeight()
147        );
148    }
149
150    public void removeRecyclerView() throws Throwable {
151        if (mRecyclerView == null) {
152            return;
153        }
154        if (!isMainThread()) {
155            getInstrumentation().waitForIdleSync();
156        }
157        runTestOnUiThread(new Runnable() {
158            @Override
159            public void run() {
160                try {
161                    final RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
162                    if (adapter instanceof AttachDetachCountingAdapter) {
163                        ((AttachDetachCountingAdapter) adapter).getCounter()
164                                .validateRemaining(mRecyclerView);
165                    }
166                    getActivity().mContainer.removeAllViews();
167                } catch (Throwable t) {
168                    postExceptionToInstrumentation(t);
169                }
170            }
171        });
172        mRecyclerView = null;
173    }
174
175    void waitForAnimations(int seconds) throws InterruptedException {
176        final CountDownLatch latch = new CountDownLatch(2);
177        boolean running = mRecyclerView.mItemAnimator
178                .isRunning(new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
179                    @Override
180                    public void onAnimationsFinished() {
181                        latch.countDown();
182                    }
183                });
184        if (running) {
185            latch.await(seconds, TimeUnit.SECONDS);
186        }
187    }
188
189    public void setRecyclerView(final RecyclerView recyclerView) throws Throwable {
190        setRecyclerView(recyclerView, true);
191    }
192    public void setRecyclerView(final RecyclerView recyclerView, boolean assignDummyPool)
193            throws Throwable {
194        setRecyclerView(recyclerView, true, true);
195    }
196    public void setRecyclerView(final RecyclerView recyclerView, boolean assignDummyPool,
197            boolean addPositionCheckItemAnimator)
198            throws Throwable {
199        mRecyclerView = recyclerView;
200        if (assignDummyPool) {
201            RecyclerView.RecycledViewPool pool = new RecyclerView.RecycledViewPool() {
202                @Override
203                public RecyclerView.ViewHolder getRecycledView(int viewType) {
204                    RecyclerView.ViewHolder viewHolder = super.getRecycledView(viewType);
205                    if (viewHolder == null) {
206                        return null;
207                    }
208                    viewHolder.addFlags(RecyclerView.ViewHolder.FLAG_BOUND);
209                    viewHolder.mPosition = 200;
210                    viewHolder.mOldPosition = 300;
211                    viewHolder.mPreLayoutPosition = 500;
212                    return viewHolder;
213                }
214
215                @Override
216                public void putRecycledView(RecyclerView.ViewHolder scrap) {
217                    super.putRecycledView(scrap);
218                }
219            };
220            mRecyclerView.setRecycledViewPool(pool);
221        }
222        if (addPositionCheckItemAnimator) {
223            mRecyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
224                @Override
225                public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
226                        RecyclerView.State state) {
227                    RecyclerView.ViewHolder vh = parent.getChildViewHolder(view);
228                    if (!vh.isRemoved()) {
229                        assertNotSame("If getItemOffsets is called, child should have a valid"
230                                            + " adapter position unless it is removed",
231                                    vh.getAdapterPosition(), RecyclerView.NO_POSITION);
232                    }
233                }
234            });
235        }
236        mAdapterHelper = recyclerView.mAdapterHelper;
237        runTestOnUiThread(new Runnable() {
238            @Override
239            public void run() {
240                getActivity().mContainer.addView(recyclerView);
241            }
242        });
243    }
244
245    protected FrameLayout getRecyclerViewContainer() {
246        return getActivity().mContainer;
247    }
248
249    public void requestLayoutOnUIThread(final View view) {
250        try {
251            runTestOnUiThread(new Runnable() {
252                @Override
253                public void run() {
254                    view.requestLayout();
255                }
256            });
257        } catch (Throwable throwable) {
258            Log.e(TAG, "", throwable);
259        }
260    }
261
262    public void scrollBy(final int dt) {
263        try {
264            runTestOnUiThread(new Runnable() {
265                @Override
266                public void run() {
267                    if (mRecyclerView.getLayoutManager().canScrollHorizontally()) {
268                        mRecyclerView.scrollBy(dt, 0);
269                    } else {
270                        mRecyclerView.scrollBy(0, dt);
271                    }
272
273                }
274            });
275        } catch (Throwable throwable) {
276            Log.e(TAG, "", throwable);
277        }
278    }
279
280    void scrollToPosition(final int position) throws Throwable {
281        runTestOnUiThread(new Runnable() {
282            @Override
283            public void run() {
284                mRecyclerView.getLayoutManager().scrollToPosition(position);
285            }
286        });
287    }
288
289    void smoothScrollToPosition(final int position)
290            throws Throwable {
291        Log.d(TAG, "SMOOTH scrolling to " + position);
292        runTestOnUiThread(new Runnable() {
293            @Override
294            public void run() {
295                mRecyclerView.smoothScrollToPosition(position);
296            }
297        });
298        while (mRecyclerView.getLayoutManager().isSmoothScrolling() ||
299                mRecyclerView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) {
300            if (mDebug) {
301                Log.d(TAG, "SMOOTH scrolling step");
302            }
303            Thread.sleep(200);
304        }
305        Log.d(TAG, "SMOOTH scrolling done");
306        getInstrumentation().waitForIdleSync();
307    }
308
309    class TestViewHolder extends RecyclerView.ViewHolder {
310
311        Item mBoundItem;
312
313        public TestViewHolder(View itemView) {
314            super(itemView);
315            itemView.setFocusable(true);
316        }
317
318        @Override
319        public String toString() {
320            return super.toString() + " item:" + mBoundItem;
321        }
322    }
323
324    class TestLayoutManager extends RecyclerView.LayoutManager {
325
326        CountDownLatch layoutLatch;
327
328        public void expectLayouts(int count) {
329            layoutLatch = new CountDownLatch(count);
330        }
331
332        public void waitForLayout(long timeout, TimeUnit timeUnit) throws Throwable {
333            layoutLatch.await(timeout * (mDebug ? 100 : 1), timeUnit);
334            assertEquals("all expected layouts should be executed at the expected time",
335                    0, layoutLatch.getCount());
336            getInstrumentation().waitForIdleSync();
337        }
338
339        public void assertLayoutCount(int count, String msg, long timeout) throws Throwable {
340            layoutLatch.await(timeout, TimeUnit.SECONDS);
341            assertEquals(msg, count, layoutLatch.getCount());
342        }
343
344        public void assertNoLayout(String msg, long timeout) throws Throwable {
345            layoutLatch.await(timeout, TimeUnit.SECONDS);
346            assertFalse(msg, layoutLatch.getCount() == 0);
347        }
348
349        public void waitForLayout(long timeout) throws Throwable {
350            waitForLayout(timeout * (mDebug ? 10000 : 1), TimeUnit.SECONDS);
351        }
352
353        @Override
354        public RecyclerView.LayoutParams generateDefaultLayoutParams() {
355            return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
356                    ViewGroup.LayoutParams.WRAP_CONTENT);
357        }
358
359        void assertVisibleItemPositions() {
360            int i = getChildCount();
361            TestAdapter testAdapter = (TestAdapter) mRecyclerView.getAdapter();
362            while (i-- > 0) {
363                View view = getChildAt(i);
364                RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) view.getLayoutParams();
365                Item item = ((TestViewHolder) lp.mViewHolder).mBoundItem;
366                if (mDebug) {
367                    Log.d(TAG, "testing item " + i);
368                }
369                if (!lp.isItemRemoved()) {
370                    RecyclerView.ViewHolder vh = mRecyclerView.getChildViewHolder(view);
371                    assertSame("item position in LP should match adapter value :" + vh,
372                            testAdapter.mItems.get(vh.mPosition), item);
373                }
374            }
375        }
376
377        RecyclerView.LayoutParams getLp(View v) {
378            return (RecyclerView.LayoutParams) v.getLayoutParams();
379        }
380
381        void layoutRange(RecyclerView.Recycler recycler, int start, int end) {
382            assertScrap(recycler);
383            if (mDebug) {
384                Log.d(TAG, "will layout items from " + start + " to " + end);
385            }
386            int diff = end > start ? 1 : -1;
387            int top = 0;
388            for (int i = start; i != end; i+=diff) {
389                if (mDebug) {
390                    Log.d(TAG, "laying out item " + i);
391                }
392                View view = recycler.getViewForPosition(i);
393                assertNotNull("view should not be null for valid position. "
394                        + "got null view at position " + i, view);
395                if (!mRecyclerView.mState.isPreLayout()) {
396                    RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) view
397                            .getLayoutParams();
398                    assertFalse("In post layout, getViewForPosition should never return a view "
399                            + "that is removed", layoutParams != null
400                            && layoutParams.isItemRemoved());
401
402                }
403                assertEquals("getViewForPosition should return correct position",
404                        i, getPosition(view));
405                addView(view);
406
407                measureChildWithMargins(view, 0, 0);
408                layoutDecorated(view, 0, top, getDecoratedMeasuredWidth(view)
409                        , top + getDecoratedMeasuredHeight(view));
410                top += view.getMeasuredHeight();
411            }
412        }
413
414        private void assertScrap(RecyclerView.Recycler recycler) {
415            if (mRecyclerView.getAdapter() != null &&
416                    !mRecyclerView.getAdapter().hasStableIds()) {
417                for (RecyclerView.ViewHolder viewHolder : recycler.getScrapList()) {
418                    assertFalse("Invalid scrap should be no kept", viewHolder.isInvalid());
419                }
420            }
421        }
422
423        @Override
424        public boolean canScrollHorizontally() {
425            return true;
426        }
427
428        @Override
429        public boolean canScrollVertically() {
430            return true;
431        }
432
433        @Override
434        public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
435                RecyclerView.State state) {
436            return dx;
437        }
438
439        @Override
440        public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
441                RecyclerView.State state) {
442            return dy;
443        }
444    }
445
446    static class Item {
447        final static AtomicInteger idCounter = new AtomicInteger(0);
448        final public int mId = idCounter.incrementAndGet();
449
450        int mAdapterIndex;
451
452        final String mText;
453
454        Item(int adapterIndex, String text) {
455            mAdapterIndex = adapterIndex;
456            mText = text;
457        }
458
459        @Override
460        public String toString() {
461            return "Item{" +
462                    "mId=" + mId +
463                    ", originalIndex=" + mAdapterIndex +
464                    ", text='" + mText + '\'' +
465                    '}';
466        }
467    }
468
469    class TestAdapter extends RecyclerView.Adapter<TestViewHolder>
470            implements AttachDetachCountingAdapter {
471
472        ViewAttachDetachCounter mAttachmentCounter = new ViewAttachDetachCounter();
473        List<Item> mItems;
474
475        TestAdapter(int count) {
476            mItems = new ArrayList<Item>(count);
477            for (int i = 0; i < count; i++) {
478                mItems.add(new Item(i, "Item " + i));
479            }
480        }
481
482        @Override
483        public void onViewAttachedToWindow(TestViewHolder holder) {
484            super.onViewAttachedToWindow(holder);
485            mAttachmentCounter.onViewAttached(holder);
486        }
487
488        @Override
489        public void onViewDetachedFromWindow(TestViewHolder holder) {
490            super.onViewDetachedFromWindow(holder);
491            mAttachmentCounter.onViewDetached(holder);
492        }
493
494        @Override
495        public void onAttachedToRecyclerView(RecyclerView recyclerView) {
496            super.onAttachedToRecyclerView(recyclerView);
497            mAttachmentCounter.onAttached(recyclerView);
498        }
499
500        @Override
501        public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
502            super.onDetachedFromRecyclerView(recyclerView);
503            mAttachmentCounter.onDetached(recyclerView);
504        }
505
506        @Override
507        public TestViewHolder onCreateViewHolder(ViewGroup parent,
508                int viewType) {
509            return new TestViewHolder(new TextView(parent.getContext()));
510        }
511
512        @Override
513        public void onBindViewHolder(TestViewHolder holder, int position) {
514            final Item item = mItems.get(position);
515            ((TextView) (holder.itemView)).setText(item.mText + "(" + item.mAdapterIndex + ")");
516            holder.mBoundItem = item;
517        }
518
519        public void deleteAndNotify(final int start, final int count) throws Throwable {
520            deleteAndNotify(new int[]{start, count});
521        }
522
523        /**
524         * Deletes items in the given ranges.
525         * <p>
526         * Note that each operation affects the one after so you should offset them properly.
527         * <p>
528         * For example, if adapter has 5 items (A,B,C,D,E), and then you call this method with
529         * <code>[1, 2],[2, 1]</code>, it will first delete items B,C and the new adapter will be
530         * A D E. Then it will delete 2,1 which means it will delete E.
531         */
532        public void deleteAndNotify(final int[]... startCountTuples) throws Throwable {
533            for (int[] tuple : startCountTuples) {
534                tuple[1] = -tuple[1];
535            }
536            new AddRemoveRunnable(startCountTuples).runOnMainThread();
537        }
538
539        @Override
540        public long getItemId(int position) {
541            return hasStableIds() ? mItems.get(position).mId : super.getItemId(position);
542        }
543
544        public void offsetOriginalIndices(int start, int offset) {
545            for (int i = start; i < mItems.size(); i++) {
546                mItems.get(i).mAdapterIndex += offset;
547            }
548        }
549
550        /**
551         * @param start inclusive
552         * @param end exclusive
553         * @param offset
554         */
555        public void offsetOriginalIndicesBetween(int start, int end, int offset) {
556            for (int i = start; i < end && i < mItems.size(); i++) {
557                mItems.get(i).mAdapterIndex += offset;
558            }
559        }
560
561        public void addAndNotify(final int start, final int count) throws Throwable {
562            addAndNotify(new int[]{start, count});
563        }
564
565        public void addAndNotify(final int[]... startCountTuples) throws Throwable {
566            new AddRemoveRunnable(startCountTuples).runOnMainThread();
567        }
568
569        public void dispatchDataSetChanged() throws Throwable {
570            runTestOnUiThread(new Runnable() {
571                @Override
572                public void run() {
573                    notifyDataSetChanged();
574                }
575            });
576        }
577
578        public void changeAndNotify(final int start, final int count) throws Throwable {
579            runTestOnUiThread(new Runnable() {
580                @Override
581                public void run() {
582                    notifyItemRangeChanged(start, count);
583                }
584            });
585        }
586
587        public void changePositionsAndNotify(final int... positions) throws Throwable {
588            runTestOnUiThread(new Runnable() {
589                @Override
590                public void run() {
591                    for (int i = 0; i < positions.length; i += 1) {
592                        TestAdapter.super.notifyItemRangeChanged(positions[i], 1);
593                    }
594                }
595            });
596        }
597
598        /**
599         * Similar to other methods but negative count means delete and position count means add.
600         * <p>
601         * For instance, calling this method with <code>[1,1], [2,-1]</code> it will first add an
602         * item to index 1, then remove an item from index 2 (updated index 2)
603         */
604        public void addDeleteAndNotify(final int[]... startCountTuples) throws Throwable {
605            new AddRemoveRunnable(startCountTuples).runOnMainThread();
606        }
607
608        @Override
609        public int getItemCount() {
610            return mItems.size();
611        }
612
613        /**
614         * Uses notifyDataSetChanged
615         */
616        public void moveItems(boolean notifyChange, int[]... fromToTuples) throws Throwable {
617            for (int i = 0; i < fromToTuples.length; i += 1) {
618                int[] tuple = fromToTuples[i];
619                moveItem(tuple[0], tuple[1], false);
620            }
621            if (notifyChange) {
622                dispatchDataSetChanged();
623            }
624        }
625
626        /**
627         * Uses notifyDataSetChanged
628         */
629        public void moveItem(final int from, final int to, final boolean notifyChange)
630                throws Throwable {
631            runTestOnUiThread(new Runnable() {
632                @Override
633                public void run() {
634                    Item item = mItems.remove(from);
635                    mItems.add(to, item);
636                    offsetOriginalIndices(from, to - 1);
637                    item.mAdapterIndex = to;
638                    if (notifyChange) {
639                        notifyDataSetChanged();
640                    }
641                }
642            });
643        }
644
645        /**
646         * Uses notifyItemMoved
647         */
648        public void moveAndNotify(final int from, final int to) throws Throwable {
649            runTestOnUiThread(new Runnable() {
650                @Override
651                public void run() {
652                    Item item = mItems.remove(from);
653                    mItems.add(to, item);
654                    offsetOriginalIndices(from, to - 1);
655                    item.mAdapterIndex = to;
656                    notifyItemMoved(from, to);
657                }
658            });
659        }
660
661
662
663        @Override
664        public ViewAttachDetachCounter getCounter() {
665            return mAttachmentCounter;
666        }
667
668
669        private class AddRemoveRunnable implements Runnable {
670            final int[][] mStartCountTuples;
671
672            public AddRemoveRunnable(int[][] startCountTuples) {
673                mStartCountTuples = startCountTuples;
674            }
675
676            public void runOnMainThread() throws Throwable {
677                if (Looper.myLooper() == Looper.getMainLooper()) {
678                    run();
679                } else {
680                    runTestOnUiThread(this);
681                }
682            }
683
684            @Override
685            public void run() {
686                for (int[] tuple : mStartCountTuples) {
687                    if (tuple[1] < 0) {
688                        delete(tuple);
689                    } else {
690                        add(tuple);
691                    }
692                }
693            }
694
695            private void add(int[] tuple) {
696                // offset others
697                offsetOriginalIndices(tuple[0], tuple[1]);
698                for (int i = 0; i < tuple[1]; i++) {
699                    mItems.add(tuple[0], new Item(i, "new item " + i));
700                }
701                notifyItemRangeInserted(tuple[0], tuple[1]);
702            }
703
704            private void delete(int[] tuple) {
705                final int count = -tuple[1];
706                offsetOriginalIndices(tuple[0] + count, tuple[1]);
707                for (int i = 0; i < count; i++) {
708                    mItems.remove(tuple[0]);
709                }
710                notifyItemRangeRemoved(tuple[0], count);
711            }
712        }
713    }
714
715    public boolean isMainThread() {
716        return Looper.myLooper() == Looper.getMainLooper();
717    }
718
719    @Override
720    public void runTestOnUiThread(Runnable r) throws Throwable {
721        if (Looper.myLooper() == Looper.getMainLooper()) {
722            r.run();
723        } else {
724            super.runTestOnUiThread(r);
725        }
726    }
727
728    static class TargetTuple {
729
730        final int mPosition;
731
732        final int mLayoutDirection;
733
734        TargetTuple(int position, int layoutDirection) {
735            this.mPosition = position;
736            this.mLayoutDirection = layoutDirection;
737        }
738
739        @Override
740        public String toString() {
741            return "TargetTuple{" +
742                    "mPosition=" + mPosition +
743                    ", mLayoutDirection=" + mLayoutDirection +
744                    '}';
745        }
746    }
747
748    public interface AttachDetachCountingAdapter {
749
750        ViewAttachDetachCounter getCounter();
751    }
752
753    public class ViewAttachDetachCounter {
754
755        Set<RecyclerView.ViewHolder> mAttachedSet = new HashSet<RecyclerView.ViewHolder>();
756
757        public void validateRemaining(RecyclerView recyclerView) {
758            final int childCount = recyclerView.getChildCount();
759            for (int i = 0; i < childCount; i++) {
760                View view = recyclerView.getChildAt(i);
761                RecyclerView.ViewHolder vh = recyclerView.getChildViewHolder(view);
762                assertTrue("remaining view should be in attached set " + vh,
763                        mAttachedSet.contains(vh));
764            }
765            assertEquals("there should not be any views left in attached set",
766                    childCount, mAttachedSet.size());
767        }
768
769        public void onViewDetached(RecyclerView.ViewHolder viewHolder) {
770            try {
771                assertTrue("view holder should be in attached set",
772                        mAttachedSet.remove(viewHolder));
773            } catch (Throwable t) {
774                postExceptionToInstrumentation(t);
775            }
776        }
777
778        public void onViewAttached(RecyclerView.ViewHolder viewHolder) {
779            try {
780                assertTrue("view holder should not be in attached set",
781                        mAttachedSet.add(viewHolder));
782            } catch (Throwable t) {
783                postExceptionToInstrumentation(t);
784            }
785        }
786
787        public void onAttached(RecyclerView recyclerView) {
788            // when a new RV is attached, clear the set and add all view holders
789            mAttachedSet.clear();
790            final int childCount = recyclerView.getChildCount();
791            for (int i = 0; i < childCount; i ++) {
792                View view = recyclerView.getChildAt(i);
793                mAttachedSet.add(recyclerView.getChildViewHolder(view));
794            }
795        }
796
797        public void onDetached(RecyclerView recyclerView) {
798            validateRemaining(recyclerView);
799        }
800    }
801}
802