BaseRecyclerViewInstrumentationTest.java revision dcc9af3f0c93d51cd8d0c694f1c0cdcd2d7ae405
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 boolean requestFocus(final View view) { 190 final boolean[] result = new boolean[1]; 191 getActivity().runOnUiThread(new Runnable() { 192 @Override 193 public void run() { 194 result[0] = view.requestFocus(); 195 } 196 }); 197 return result[0]; 198 } 199 200 public void setRecyclerView(final RecyclerView recyclerView) throws Throwable { 201 setRecyclerView(recyclerView, true); 202 } 203 public void setRecyclerView(final RecyclerView recyclerView, boolean assignDummyPool) 204 throws Throwable { 205 setRecyclerView(recyclerView, true, true); 206 } 207 public void setRecyclerView(final RecyclerView recyclerView, boolean assignDummyPool, 208 boolean addPositionCheckItemAnimator) 209 throws Throwable { 210 mRecyclerView = recyclerView; 211 if (assignDummyPool) { 212 RecyclerView.RecycledViewPool pool = new RecyclerView.RecycledViewPool() { 213 @Override 214 public RecyclerView.ViewHolder getRecycledView(int viewType) { 215 RecyclerView.ViewHolder viewHolder = super.getRecycledView(viewType); 216 if (viewHolder == null) { 217 return null; 218 } 219 viewHolder.addFlags(RecyclerView.ViewHolder.FLAG_BOUND); 220 viewHolder.mPosition = 200; 221 viewHolder.mOldPosition = 300; 222 viewHolder.mPreLayoutPosition = 500; 223 return viewHolder; 224 } 225 226 @Override 227 public void putRecycledView(RecyclerView.ViewHolder scrap) { 228 assertNull(scrap.mOwnerRecyclerView); 229 super.putRecycledView(scrap); 230 } 231 }; 232 mRecyclerView.setRecycledViewPool(pool); 233 } 234 if (addPositionCheckItemAnimator) { 235 mRecyclerView.addItemDecoration(new RecyclerView.ItemDecoration() { 236 @Override 237 public void getItemOffsets(Rect outRect, View view, RecyclerView parent, 238 RecyclerView.State state) { 239 RecyclerView.ViewHolder vh = parent.getChildViewHolder(view); 240 if (!vh.isRemoved()) { 241 assertNotSame("If getItemOffsets is called, child should have a valid" 242 + " adapter position unless it is removed : " + vh, 243 vh.getAdapterPosition(), RecyclerView.NO_POSITION); 244 } 245 } 246 }); 247 } 248 mAdapterHelper = recyclerView.mAdapterHelper; 249 runTestOnUiThread(new Runnable() { 250 @Override 251 public void run() { 252 getActivity().mContainer.addView(recyclerView); 253 } 254 }); 255 } 256 257 protected FrameLayout getRecyclerViewContainer() { 258 return getActivity().mContainer; 259 } 260 261 public void requestLayoutOnUIThread(final View view) { 262 try { 263 runTestOnUiThread(new Runnable() { 264 @Override 265 public void run() { 266 view.requestLayout(); 267 } 268 }); 269 } catch (Throwable throwable) { 270 Log.e(TAG, "", throwable); 271 } 272 } 273 274 public void scrollBy(final int dt) { 275 try { 276 runTestOnUiThread(new Runnable() { 277 @Override 278 public void run() { 279 if (mRecyclerView.getLayoutManager().canScrollHorizontally()) { 280 mRecyclerView.scrollBy(dt, 0); 281 } else { 282 mRecyclerView.scrollBy(0, dt); 283 } 284 285 } 286 }); 287 } catch (Throwable throwable) { 288 Log.e(TAG, "", throwable); 289 } 290 } 291 292 void scrollToPosition(final int position) throws Throwable { 293 runTestOnUiThread(new Runnable() { 294 @Override 295 public void run() { 296 mRecyclerView.getLayoutManager().scrollToPosition(position); 297 } 298 }); 299 } 300 301 void smoothScrollToPosition(final int position) 302 throws Throwable { 303 if (mDebug) { 304 Log.d(TAG, "SMOOTH scrolling to " + position); 305 } 306 runTestOnUiThread(new Runnable() { 307 @Override 308 public void run() { 309 mRecyclerView.smoothScrollToPosition(position); 310 } 311 }); 312 getInstrumentation().waitForIdleSync(); 313 Thread.sleep(200); //give scroller some time so start 314 while (mRecyclerView.getLayoutManager().isSmoothScrolling() || 315 mRecyclerView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) { 316 if (mDebug) { 317 Log.d(TAG, "SMOOTH scrolling step"); 318 } 319 Thread.sleep(200); 320 } 321 if (mDebug) { 322 Log.d(TAG, "SMOOTH scrolling done"); 323 } 324 getInstrumentation().waitForIdleSync(); 325 } 326 327 class TestViewHolder extends RecyclerView.ViewHolder { 328 329 Item mBoundItem; 330 331 public TestViewHolder(View itemView) { 332 super(itemView); 333 itemView.setFocusable(true); 334 } 335 336 @Override 337 public String toString() { 338 return super.toString() + " item:" + mBoundItem; 339 } 340 } 341 342 class TestLayoutManager extends RecyclerView.LayoutManager { 343 344 CountDownLatch layoutLatch; 345 346 public void expectLayouts(int count) { 347 layoutLatch = new CountDownLatch(count); 348 } 349 350 public void waitForLayout(long timeout, TimeUnit timeUnit) throws Throwable { 351 layoutLatch.await(timeout * (mDebug ? 100 : 1), timeUnit); 352 assertEquals("all expected layouts should be executed at the expected time", 353 0, layoutLatch.getCount()); 354 getInstrumentation().waitForIdleSync(); 355 } 356 357 public void assertLayoutCount(int count, String msg, long timeout) throws Throwable { 358 layoutLatch.await(timeout, TimeUnit.SECONDS); 359 assertEquals(msg, count, layoutLatch.getCount()); 360 } 361 362 public void assertNoLayout(String msg, long timeout) throws Throwable { 363 layoutLatch.await(timeout, TimeUnit.SECONDS); 364 assertFalse(msg, layoutLatch.getCount() == 0); 365 } 366 367 public void waitForLayout(long timeout) throws Throwable { 368 waitForLayout(timeout * (mDebug ? 10000 : 1), TimeUnit.SECONDS); 369 } 370 371 @Override 372 public RecyclerView.LayoutParams generateDefaultLayoutParams() { 373 return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 374 ViewGroup.LayoutParams.WRAP_CONTENT); 375 } 376 377 void assertVisibleItemPositions() { 378 int i = getChildCount(); 379 TestAdapter testAdapter = (TestAdapter) mRecyclerView.getAdapter(); 380 while (i-- > 0) { 381 View view = getChildAt(i); 382 RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) view.getLayoutParams(); 383 Item item = ((TestViewHolder) lp.mViewHolder).mBoundItem; 384 if (mDebug) { 385 Log.d(TAG, "testing item " + i); 386 } 387 if (!lp.isItemRemoved()) { 388 RecyclerView.ViewHolder vh = mRecyclerView.getChildViewHolder(view); 389 assertSame("item position in LP should match adapter value :" + vh, 390 testAdapter.mItems.get(vh.mPosition), item); 391 } 392 } 393 } 394 395 RecyclerView.LayoutParams getLp(View v) { 396 return (RecyclerView.LayoutParams) v.getLayoutParams(); 397 } 398 399 void layoutRange(RecyclerView.Recycler recycler, int start, int end) { 400 assertScrap(recycler); 401 if (mDebug) { 402 Log.d(TAG, "will layout items from " + start + " to " + end); 403 } 404 int diff = end > start ? 1 : -1; 405 int top = 0; 406 for (int i = start; i != end; i+=diff) { 407 if (mDebug) { 408 Log.d(TAG, "laying out item " + i); 409 } 410 View view = recycler.getViewForPosition(i); 411 assertNotNull("view should not be null for valid position. " 412 + "got null view at position " + i, view); 413 if (!mRecyclerView.mState.isPreLayout()) { 414 RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) view 415 .getLayoutParams(); 416 assertFalse("In post layout, getViewForPosition should never return a view " 417 + "that is removed", layoutParams != null 418 && layoutParams.isItemRemoved()); 419 420 } 421 assertEquals("getViewForPosition should return correct position", 422 i, getPosition(view)); 423 addView(view); 424 425 measureChildWithMargins(view, 0, 0); 426 layoutDecorated(view, 0, top, getDecoratedMeasuredWidth(view) 427 , top + getDecoratedMeasuredHeight(view)); 428 top += view.getMeasuredHeight(); 429 } 430 } 431 432 private void assertScrap(RecyclerView.Recycler recycler) { 433 if (mRecyclerView.getAdapter() != null && 434 !mRecyclerView.getAdapter().hasStableIds()) { 435 for (RecyclerView.ViewHolder viewHolder : recycler.getScrapList()) { 436 assertFalse("Invalid scrap should be no kept", viewHolder.isInvalid()); 437 } 438 } 439 } 440 441 @Override 442 public boolean canScrollHorizontally() { 443 return true; 444 } 445 446 @Override 447 public boolean canScrollVertically() { 448 return true; 449 } 450 451 @Override 452 public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, 453 RecyclerView.State state) { 454 return dx; 455 } 456 457 @Override 458 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 459 RecyclerView.State state) { 460 return dy; 461 } 462 } 463 464 static class Item { 465 final static AtomicInteger idCounter = new AtomicInteger(0); 466 final public int mId = idCounter.incrementAndGet(); 467 468 int mAdapterIndex; 469 470 final String mText; 471 472 Item(int adapterIndex, String text) { 473 mAdapterIndex = adapterIndex; 474 mText = text; 475 } 476 477 @Override 478 public String toString() { 479 return "Item{" + 480 "mId=" + mId + 481 ", originalIndex=" + mAdapterIndex + 482 ", text='" + mText + '\'' + 483 '}'; 484 } 485 } 486 487 class TestAdapter extends RecyclerView.Adapter<TestViewHolder> 488 implements AttachDetachCountingAdapter { 489 490 ViewAttachDetachCounter mAttachmentCounter = new ViewAttachDetachCounter(); 491 List<Item> mItems; 492 493 TestAdapter(int count) { 494 mItems = new ArrayList<Item>(count); 495 for (int i = 0; i < count; i++) { 496 mItems.add(new Item(i, "Item " + i)); 497 } 498 } 499 500 @Override 501 public void onViewAttachedToWindow(TestViewHolder holder) { 502 super.onViewAttachedToWindow(holder); 503 mAttachmentCounter.onViewAttached(holder); 504 } 505 506 @Override 507 public void onViewDetachedFromWindow(TestViewHolder holder) { 508 super.onViewDetachedFromWindow(holder); 509 mAttachmentCounter.onViewDetached(holder); 510 } 511 512 @Override 513 public void onAttachedToRecyclerView(RecyclerView recyclerView) { 514 super.onAttachedToRecyclerView(recyclerView); 515 mAttachmentCounter.onAttached(recyclerView); 516 } 517 518 @Override 519 public void onDetachedFromRecyclerView(RecyclerView recyclerView) { 520 super.onDetachedFromRecyclerView(recyclerView); 521 mAttachmentCounter.onDetached(recyclerView); 522 } 523 524 @Override 525 public TestViewHolder onCreateViewHolder(ViewGroup parent, 526 int viewType) { 527 return new TestViewHolder(new TextView(parent.getContext())); 528 } 529 530 @Override 531 public void onBindViewHolder(TestViewHolder holder, int position) { 532 assertNotNull(holder.mOwnerRecyclerView); 533 assertEquals(position, holder.getAdapterPosition()); 534 final Item item = mItems.get(position); 535 ((TextView) (holder.itemView)).setText(item.mText + "(" + item.mAdapterIndex + ")"); 536 holder.mBoundItem = item; 537 } 538 539 @Override 540 public void onViewRecycled(TestViewHolder holder) { 541 super.onViewRecycled(holder); 542 final int adapterPosition = holder.getAdapterPosition(); 543 final boolean shouldHavePosition = !holder.isRemoved() && holder.isBound() && 544 !holder.isAdapterPositionUnknown() && !holder.isInvalid(); 545 String log = "Position check for " + holder.toString(); 546 assertEquals(log, shouldHavePosition, adapterPosition != RecyclerView.NO_POSITION); 547 if (shouldHavePosition) { 548 assertTrue(log, mItems.size() > adapterPosition); 549 assertSame(log, holder.mBoundItem, mItems.get(adapterPosition)); 550 } 551 } 552 553 public void deleteAndNotify(final int start, final int count) throws Throwable { 554 deleteAndNotify(new int[]{start, count}); 555 } 556 557 /** 558 * Deletes items in the given ranges. 559 * <p> 560 * Note that each operation affects the one after so you should offset them properly. 561 * <p> 562 * For example, if adapter has 5 items (A,B,C,D,E), and then you call this method with 563 * <code>[1, 2],[2, 1]</code>, it will first delete items B,C and the new adapter will be 564 * A D E. Then it will delete 2,1 which means it will delete E. 565 */ 566 public void deleteAndNotify(final int[]... startCountTuples) throws Throwable { 567 for (int[] tuple : startCountTuples) { 568 tuple[1] = -tuple[1]; 569 } 570 new AddRemoveRunnable(startCountTuples).runOnMainThread(); 571 } 572 573 @Override 574 public long getItemId(int position) { 575 return hasStableIds() ? mItems.get(position).mId : super.getItemId(position); 576 } 577 578 public void offsetOriginalIndices(int start, int offset) { 579 for (int i = start; i < mItems.size(); i++) { 580 mItems.get(i).mAdapterIndex += offset; 581 } 582 } 583 584 /** 585 * @param start inclusive 586 * @param end exclusive 587 * @param offset 588 */ 589 public void offsetOriginalIndicesBetween(int start, int end, int offset) { 590 for (int i = start; i < end && i < mItems.size(); i++) { 591 mItems.get(i).mAdapterIndex += offset; 592 } 593 } 594 595 public void addAndNotify(final int start, final int count) throws Throwable { 596 addAndNotify(new int[]{start, count}); 597 } 598 599 public void addAndNotify(final int[]... startCountTuples) throws Throwable { 600 new AddRemoveRunnable(startCountTuples).runOnMainThread(); 601 } 602 603 public void dispatchDataSetChanged() throws Throwable { 604 runTestOnUiThread(new Runnable() { 605 @Override 606 public void run() { 607 notifyDataSetChanged(); 608 } 609 }); 610 } 611 612 public void changeAndNotify(final int start, final int count) throws Throwable { 613 runTestOnUiThread(new Runnable() { 614 @Override 615 public void run() { 616 notifyItemRangeChanged(start, count); 617 } 618 }); 619 } 620 621 public void changePositionsAndNotify(final int... positions) throws Throwable { 622 runTestOnUiThread(new Runnable() { 623 @Override 624 public void run() { 625 for (int i = 0; i < positions.length; i += 1) { 626 TestAdapter.super.notifyItemRangeChanged(positions[i], 1); 627 } 628 } 629 }); 630 } 631 632 /** 633 * Similar to other methods but negative count means delete and position count means add. 634 * <p> 635 * For instance, calling this method with <code>[1,1], [2,-1]</code> it will first add an 636 * item to index 1, then remove an item from index 2 (updated index 2) 637 */ 638 public void addDeleteAndNotify(final int[]... startCountTuples) throws Throwable { 639 new AddRemoveRunnable(startCountTuples).runOnMainThread(); 640 } 641 642 @Override 643 public int getItemCount() { 644 return mItems.size(); 645 } 646 647 /** 648 * Uses notifyDataSetChanged 649 */ 650 public void moveItems(boolean notifyChange, int[]... fromToTuples) throws Throwable { 651 for (int i = 0; i < fromToTuples.length; i += 1) { 652 int[] tuple = fromToTuples[i]; 653 moveItem(tuple[0], tuple[1], false); 654 } 655 if (notifyChange) { 656 dispatchDataSetChanged(); 657 } 658 } 659 660 /** 661 * Uses notifyDataSetChanged 662 */ 663 public void moveItem(final int from, final int to, final boolean notifyChange) 664 throws Throwable { 665 runTestOnUiThread(new Runnable() { 666 @Override 667 public void run() { 668 Item item = mItems.remove(from); 669 mItems.add(to, item); 670 offsetOriginalIndices(from, to - 1); 671 item.mAdapterIndex = to; 672 if (notifyChange) { 673 notifyDataSetChanged(); 674 } 675 } 676 }); 677 } 678 679 /** 680 * Uses notifyItemMoved 681 */ 682 public void moveAndNotify(final int from, final int to) throws Throwable { 683 runTestOnUiThread(new Runnable() { 684 @Override 685 public void run() { 686 Item item = mItems.remove(from); 687 mItems.add(to, item); 688 offsetOriginalIndices(from, to - 1); 689 item.mAdapterIndex = to; 690 notifyItemMoved(from, to); 691 } 692 }); 693 } 694 695 696 697 @Override 698 public ViewAttachDetachCounter getCounter() { 699 return mAttachmentCounter; 700 } 701 702 703 private class AddRemoveRunnable implements Runnable { 704 final int[][] mStartCountTuples; 705 706 public AddRemoveRunnable(int[][] startCountTuples) { 707 mStartCountTuples = startCountTuples; 708 } 709 710 public void runOnMainThread() throws Throwable { 711 if (Looper.myLooper() == Looper.getMainLooper()) { 712 run(); 713 } else { 714 runTestOnUiThread(this); 715 } 716 } 717 718 @Override 719 public void run() { 720 for (int[] tuple : mStartCountTuples) { 721 if (tuple[1] < 0) { 722 delete(tuple); 723 } else { 724 add(tuple); 725 } 726 } 727 } 728 729 private void add(int[] tuple) { 730 // offset others 731 offsetOriginalIndices(tuple[0], tuple[1]); 732 for (int i = 0; i < tuple[1]; i++) { 733 mItems.add(tuple[0], new Item(i, "new item " + i)); 734 } 735 notifyItemRangeInserted(tuple[0], tuple[1]); 736 } 737 738 private void delete(int[] tuple) { 739 final int count = -tuple[1]; 740 offsetOriginalIndices(tuple[0] + count, tuple[1]); 741 for (int i = 0; i < count; i++) { 742 mItems.remove(tuple[0]); 743 } 744 notifyItemRangeRemoved(tuple[0], count); 745 } 746 } 747 } 748 749 public boolean isMainThread() { 750 return Looper.myLooper() == Looper.getMainLooper(); 751 } 752 753 @Override 754 public void runTestOnUiThread(Runnable r) throws Throwable { 755 if (Looper.myLooper() == Looper.getMainLooper()) { 756 r.run(); 757 } else { 758 super.runTestOnUiThread(r); 759 } 760 } 761 762 static class TargetTuple { 763 764 final int mPosition; 765 766 final int mLayoutDirection; 767 768 TargetTuple(int position, int layoutDirection) { 769 this.mPosition = position; 770 this.mLayoutDirection = layoutDirection; 771 } 772 773 @Override 774 public String toString() { 775 return "TargetTuple{" + 776 "mPosition=" + mPosition + 777 ", mLayoutDirection=" + mLayoutDirection + 778 '}'; 779 } 780 } 781 782 public interface AttachDetachCountingAdapter { 783 784 ViewAttachDetachCounter getCounter(); 785 } 786 787 public class ViewAttachDetachCounter { 788 789 Set<RecyclerView.ViewHolder> mAttachedSet = new HashSet<RecyclerView.ViewHolder>(); 790 791 public void validateRemaining(RecyclerView recyclerView) { 792 final int childCount = recyclerView.getChildCount(); 793 for (int i = 0; i < childCount; i++) { 794 View view = recyclerView.getChildAt(i); 795 RecyclerView.ViewHolder vh = recyclerView.getChildViewHolder(view); 796 assertTrue("remaining view should be in attached set " + vh, 797 mAttachedSet.contains(vh)); 798 } 799 assertEquals("there should not be any views left in attached set", 800 childCount, mAttachedSet.size()); 801 } 802 803 public void onViewDetached(RecyclerView.ViewHolder viewHolder) { 804 try { 805 assertTrue("view holder should be in attached set", 806 mAttachedSet.remove(viewHolder)); 807 } catch (Throwable t) { 808 postExceptionToInstrumentation(t); 809 } 810 } 811 812 public void onViewAttached(RecyclerView.ViewHolder viewHolder) { 813 try { 814 assertTrue("view holder should not be in attached set", 815 mAttachedSet.add(viewHolder)); 816 } catch (Throwable t) { 817 postExceptionToInstrumentation(t); 818 } 819 } 820 821 public void onAttached(RecyclerView recyclerView) { 822 // when a new RV is attached, clear the set and add all view holders 823 mAttachedSet.clear(); 824 final int childCount = recyclerView.getChildCount(); 825 for (int i = 0; i < childCount; i ++) { 826 View view = recyclerView.getChildAt(i); 827 mAttachedSet.add(recyclerView.getChildViewHolder(view)); 828 } 829 } 830 831 public void onDetached(RecyclerView recyclerView) { 832 validateRemaining(recyclerView); 833 } 834 } 835} 836