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