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