Merge "GridLayoutManager: Fix WindowAlignment algorithm"
diff --git a/v17/leanback/res/layout/lb_vertical_grid.xml b/v17/leanback/res/layout/lb_vertical_grid.xml
index 5dfe0c9..7154e48 100644
--- a/v17/leanback/res/layout/lb_vertical_grid.xml
+++ b/v17/leanback/res/layout/lb_vertical_grid.xml
@@ -18,6 +18,7 @@
 <android.support.v17.leanback.widget.VerticalGridView
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/browse_grid"
-    android:layout_width="match_parent"
+    android:layout_width="wrap_content"
     android:layout_height="match_parent"
+    android:layout_gravity="center"
     style="?attr/itemsVerticalGridStyle" />
diff --git a/v17/leanback/res/values/dimens.xml b/v17/leanback/res/values/dimens.xml
index bf5e179..3330e11 100644
--- a/v17/leanback/res/values/dimens.xml
+++ b/v17/leanback/res/values/dimens.xml
@@ -24,6 +24,7 @@
     <dimen name="lb_browse_rows_margin_start">238dp</dimen>
     <dimen name="lb_browse_rows_margin_top">167dp</dimen>
     <dimen name="lb_browse_rows_fading_edge">16dp</dimen>
+    <dimen name="lb_vertical_grid_padding_bottom">87dp</dimen>
 
     <dimen name="lb_browse_title_height">60dp</dimen>
     <dimen name="lb_browse_title_icon_height">52dp</dimen>
diff --git a/v17/leanback/res/values/styles.xml b/v17/leanback/res/values/styles.xml
index a3a0559..8e940b5 100644
--- a/v17/leanback/res/values/styles.xml
+++ b/v17/leanback/res/values/styles.xml
@@ -152,11 +152,12 @@
         <item name="android:focusableInTouchMode">true</item>
         <item name="android:paddingLeft">?attr/browsePaddingLeft</item>
         <item name="android:paddingRight">?attr/browsePaddingRight</item>
-        <item name="android:paddingBottom">@dimen/lb_browse_item_vertical_margin</item>
+        <item name="android:paddingBottom">@dimen/lb_vertical_grid_padding_bottom</item>
         <item name="android:paddingTop">?attr/browseRowsMarginTop</item>
         <item name="android:gravity">center_horizontal</item>
         <item name="horizontalMargin">@dimen/lb_browse_item_horizontal_margin</item>
         <item name="verticalMargin">@dimen/lb_browse_item_vertical_margin</item>
+        <item name="columnWidth">wrap_content</item>
         <item name="focusOutFront">true</item>
     </style>
 
diff --git a/v4/java/android/support/v4/app/NotificationCompat.java b/v4/java/android/support/v4/app/NotificationCompat.java
index caec297..58acacf 100644
--- a/v4/java/android/support/v4/app/NotificationCompat.java
+++ b/v4/java/android/support/v4/app/NotificationCompat.java
@@ -1334,7 +1334,7 @@
      * <br>
      * This class is a "rebuilder": It attaches to a Builder object and modifies its behavior, like so:
      * <pre class="prettyprint">
-     * Notification noti = new Notification.Builder()
+     * Notification notif = new Notification.Builder(mContext)
      *     .setContentTitle(&quot;New photo from &quot; + sender.toString())
      *     .setContentText(subject)
      *     .setSmallIcon(R.drawable.new_post)
@@ -1403,7 +1403,7 @@
      * <br>
      * This class is a "rebuilder": It attaches to a Builder object and modifies its behavior, like so:
      * <pre class="prettyprint">
-     * Notification noti = new Notification.Builder()
+     * Notification notif = new Notification.Builder(mContext)
      *     .setContentTitle(&quot;New mail from &quot; + sender.toString())
      *     .setContentText(subject)
      *     .setSmallIcon(R.drawable.new_mail)
diff --git a/v4/java/android/support/v4/app/NotificationCompatSideChannelService.java b/v4/java/android/support/v4/app/NotificationCompatSideChannelService.java
index 2917e11..b0127de 100644
--- a/v4/java/android/support/v4/app/NotificationCompatSideChannelService.java
+++ b/v4/java/android/support/v4/app/NotificationCompatSideChannelService.java
@@ -19,6 +19,7 @@
 import android.app.Notification;
 import android.app.Service;
 import android.content.Intent;
+import android.os.Build;
 import android.os.IBinder;
 import android.os.RemoteException;
 
@@ -41,9 +42,17 @@
  *
  */
 public abstract class NotificationCompatSideChannelService extends Service {
+    // In support lib, we cannot reference version codes >= 4 from android.os.Build.
+    private static final int BUILD_VERSION_CODE_KITKAT_WATCH = 20;
+
     @Override
     public IBinder onBind(Intent intent) {
         if (intent.getAction().equals(NotificationManagerCompat.ACTION_BIND_SIDE_CHANNEL)) {
+            // Group support is the only current reason to use side channel,
+            // so disallow clients to bind for side channel on devices past KITKAT_WATCH for now.
+            if (Build.VERSION.SDK_INT >= BUILD_VERSION_CODE_KITKAT_WATCH) {
+                return null;
+            }
             return new NotificationSideChannelStub();
         }
         return null;
diff --git a/v4/java/android/support/v4/widget/ExploreByTouchHelper.java b/v4/java/android/support/v4/widget/ExploreByTouchHelper.java
index b894399..7adbc6f 100644
--- a/v4/java/android/support/v4/widget/ExploreByTouchHelper.java
+++ b/v4/java/android/support/v4/widget/ExploreByTouchHelper.java
@@ -577,7 +577,8 @@
      * @param x The view-relative x coordinate
      * @param y The view-relative y coordinate
      * @return virtual view identifier for the logical item under
-     *         coordinates (x,y)
+     *         coordinates (x,y) or {@link View#NO_ID} if there is no item at
+     *         the given coordinates
      */
     protected abstract int getVirtualViewAt(float x, float y);
 
diff --git a/v7/palette/src/android/support/v7/graphics/Palette.java b/v7/palette/src/android/support/v7/graphics/Palette.java
index 9e57760..cff1f0b 100644
--- a/v7/palette/src/android/support/v7/graphics/Palette.java
+++ b/v7/palette/src/android/support/v7/graphics/Palette.java
@@ -47,7 +47,6 @@
  *
  * <pre>
  * Palette.generateAsync(bitmap, new Palette.PaletteAsyncListener() {
- *     @Override
  *     public void onGenerated(Palette palette) {
  *         // Do something with colors...
  *     }
diff --git a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
index eda0583..f7946a3 100644
--- a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
+++ b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
@@ -110,9 +110,18 @@
             if (!mAdapterHelper.hasPendingUpdates()) {
                 return;
             }
-            eatRequestLayout();
-            mAdapterHelper.preProcess();
-            resumeRequestLayout(true);
+            if (mDataSetHasChangedAfterLayout) {
+                dispatchLayout();
+            } else {
+                eatRequestLayout();
+                mAdapterHelper.preProcess();
+                if (!mEatRequestLayout) {
+                    // We run this after pre-processing is complete so that ViewHolders have their
+                    // final adapter positions. No need to run it if a layout is already requested.
+                    rebindInvalidViewHolders();
+                }
+                resumeRequestLayout(true);
+            }
         }
     };
 
@@ -132,6 +141,12 @@
     private boolean mAdapterUpdateDuringMeasure;
     private final boolean mPostUpdatesOnAnimation;
 
+    /**
+     * Set to true when an adapter data set changed notification is received.
+     * In that case, we cannot run any animations since we don't know what happened.
+     */
+    private boolean mDataSetHasChangedAfterLayout = false;
+
     private EdgeEffectCompat mLeftGlow, mTopGlow, mRightGlow, mBottomGlow;
 
     ItemAnimator mItemAnimator = new DefaultItemAnimator();
@@ -1453,17 +1468,26 @@
         }
 
         eatRequestLayout();
-        saveOldPositions();
         // simple animations are a subset of advanced animations (which will cause a
         // prelayout step)
         boolean animateChangesSimple = mItemAnimator != null && mItemsAddedOrRemoved
-                && !mItemsChanged;
+                && !mItemsChanged && !mDataSetHasChangedAfterLayout;
         final boolean animateChangesAdvanced = animateChangesSimple &&
                 predictiveItemAnimationsEnabled();
         mItemsAddedOrRemoved = mItemsChanged = false;
         ArrayMap<View, Rect> appearingViewInitialBounds = null;
         mState.mInPreLayout = animateChangesAdvanced;
         mState.mItemCount = mAdapter.getItemCount();
+
+        if (mDataSetHasChangedAfterLayout) {
+            // Processing these items have no value since data set changed unexpectedly.
+            // Instead, we just reset it.
+            // TODO consider handling updates that arrived before notifyDataSetChanged is called.
+            mAdapterHelper.reset();
+            markKnownViewsInvalid();
+            mLayout.onItemsChanged(this);
+        }
+
         if (animateChangesSimple) {
             // Step 0: Find out where all non-removed items are, pre-layout
             mState.mPreLayoutHolderMap.clear();
@@ -1482,7 +1506,9 @@
             // items back to the container). This gives the pre-layout position of APPEARING views
             // which come into existence as part of the real layout.
 
-            // make sure any pending data updates are flushed before laying out
+            // Save old positions so that LayoutManager can run its mapping logic.
+            saveOldPositions();
+            // Make sure any pending data updates are flushed before laying out.
             mAdapterHelper.preProcess();
             mInPreLayout = true;
             final boolean didStructureChange = mState.mStructureChanged;
@@ -1508,14 +1534,14 @@
                             child.getRight(), child.getBottom()));
                 }
             }
-        }
-        clearOldPositions();
-        if (animateChangesAdvanced) {
+            clearOldPositions();
             mAdapterHelper.consumePostponedUpdates();
         } else {
+            clearOldPositions();
             mAdapterHelper.consumeUpdatesInOnePass();
         }
         mState.mItemCount = mAdapter.getItemCount();
+        mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;
 
         // Step 2: Run layout
         mState.mInPreLayout = false;
@@ -1592,7 +1618,7 @@
         resumeRequestLayout(false);
         mLayout.removeAndRecycleScrapInt(mRecycler, !animateChangesAdvanced);
         mState.mPreviousLayoutItemCount = mState.mItemCount;
-        mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;
+        mDataSetHasChangedAfterLayout = false;
     }
 
     private void animateAppearance(ViewHolder itemHolder, Rect beforeBounds, int afterLeft,
@@ -1842,14 +1868,33 @@
             }
 
             if (holder.mPosition >= positionStart && holder.mPosition < positionEnd) {
+                // We re-bind these view holders after pre-processing is complete so that
+                // ViewHolders have their final positions assigned.
                 holder.addFlags(ViewHolder.FLAG_UPDATE);
-                // Binding an attached view will request a layout if needed.
-                mAdapter.bindViewHolder(holder, holder.getPosition());
             }
         }
         mRecycler.viewRangeUpdate(positionStart, itemCount);
     }
 
+    void rebindInvalidViewHolders() {
+        final int childCount = getChildCount();
+        for (int i = 0; i < getChildCount(); i++) {
+            final ViewHolder holder = getChildViewHolderInt(getChildAt(i));
+            // validate type is correct
+            if (holder != null && !holder.isRemoved() && holder.isInvalid()) {
+                final int type = mAdapter.getItemViewType(holder.mPosition);
+                if (holder.getItemViewType() == type) {
+                    // Binding an attached view will request a layout if needed.
+                    mAdapter.bindViewHolder(holder, holder.mPosition);
+                } else {
+                    // binding to a new view will need re-layout anyways. We can as well trigger
+                    // it here so that it happens during layout
+                    requestLayout();
+                }
+            }
+        }
+    }
+
     /**
      * Mark all known views as invalid. Used in response to a, "the whole world might have changed"
      * data change event.
@@ -2246,13 +2291,16 @@
         @Override
         public void onChanged() {
             if (mAdapter.hasStableIds()) {
-                // TODO Determine what actually changed
-                markKnownViewsInvalid();
+                // TODO Determine what actually changed.
+                // This is more important to implement now since this callback will disable all
+                // animations because we cannot rely on positions.
                 mState.mStructureChanged = true;
-                requestLayout();
+                mDataSetHasChangedAfterLayout = true;
             } else {
-                markKnownViewsInvalid();
                 mState.mStructureChanged = true;
+                mDataSetHasChangedAfterLayout = true;
+            }
+            if (!mAdapterHelper.hasPendingUpdates()) {
                 requestLayout();
             }
         }
@@ -3666,7 +3714,12 @@
             final int childCount = getChildCount();
             for (int i = 0; i < childCount; i++) {
                 View child = getChildAt(i);
-                if (getPosition(child) == position) {
+                ViewHolder vh = getChildViewHolderInt(child);
+                if (vh == null) {
+                    continue;
+                }
+                if (vh.getPosition() == position &&
+                        (mRecyclerView.mState.isPreLayout() || !vh.isRemoved())) {
                     return child;
                 }
             }
@@ -4391,6 +4444,15 @@
         }
 
         /**
+         * Called when {@link Adapter#notifyDataSetChanged()} is triggered instead of giving
+         * detailed information on what has actually changed.
+         *
+         * @param recyclerView
+         */
+        public void onItemsChanged(RecyclerView recyclerView) {
+        }
+
+        /**
          * Called when items have been added to the adapter. The LayoutManager may choose to
          * requestLayout if the inserted items would require refreshing the currently visible set
          * of child views. (e.g. currently empty space would be filled by appended items, etc.)
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
index 65d07f0..d2d8f8b 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
@@ -40,6 +40,8 @@
 
     protected AdapterHelper mAdapterHelper;
 
+    Throwable mainThreadException;
+
     public BaseRecyclerViewInstrumentationTest() {
         this(false);
     }
@@ -49,6 +51,27 @@
         mDebug = debug;
     }
 
+    void checkForMainThreadException() throws Throwable {
+        if (mainThreadException != null) {
+            throw mainThreadException;
+        }
+    }
+
+    void postExceptionToInstrumentation(Throwable t) {
+        if (mDebug) {
+            Log.e(TAG, "captured exception on main thread", t);
+        }
+        mainThreadException = t;
+        if (mRecyclerView != null && mRecyclerView
+                .getLayoutManager() instanceof TestLayoutManager) {
+            TestLayoutManager lm = (TestLayoutManager) mRecyclerView.getLayoutManager();
+            // finish all layouts so that we get the correct exception
+            while (lm.layoutLatch.getCount() > 0) {
+                lm.layoutLatch.countDown();
+            }
+        }
+    }
+
     @Override
     protected void tearDown() throws Exception {
         if (mRecyclerView != null) {
@@ -208,7 +231,8 @@
             if (mDebug) {
                 Log.d(TAG, "will layout items from " + start + " to " + end);
             }
-            for (int i = start; i < end; i++) {
+            int diff = end > start ? 1 : -1;
+            for (int i = start; i != end; i+=diff) {
                 if (mDebug) {
                     Log.d(TAG, "laying out item " + i);
                 }
@@ -222,7 +246,7 @@
                 }
 
                 measureChildWithMargins(view, 0, 0);
-                layoutDecorated(view, 0, (i - start) * 10, getDecoratedMeasuredWidth(view)
+                layoutDecorated(view, 0, Math.abs(i - start) * 10, getDecoratedMeasuredWidth(view)
                         , getDecoratedMeasuredHeight(view));
             }
             return skippedAdd;
@@ -313,6 +337,15 @@
             });
         }
 
+        public void notifyItemChange(final int start, final int count) throws Throwable {
+            runTestOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    notifyItemRangeChanged(start, count);
+                }
+            });
+        }
+
         /**
          * Similar to other methods but negative count means delete and position count means add.
          * <p>
@@ -383,4 +416,4 @@
             super.runTestOnUiThread(r);
         }
     }
-}
\ No newline at end of file
+}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java
index 662371d..96c9202 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java
@@ -38,8 +38,6 @@
 
     private static final String TAG = "RecyclerViewAnimationsTest";
 
-    Throwable mainThreadException;
-
     AnimationLayoutManager mLayoutManager;
 
     TestAdapter mTestAdapter;
@@ -53,12 +51,6 @@
         super.setUp();
     }
 
-    void checkForMainThreadException() throws Throwable {
-        if (mainThreadException != null) {
-            throw mainThreadException;
-        }
-    }
-
     RecyclerView setupBasic(int itemCount) throws Throwable {
         return setupBasic(itemCount, 0, itemCount);
     }
@@ -91,12 +83,38 @@
         recyclerView.waitForDraw(1);
         mLayoutManager.mOnLayoutCallbacks.reset();
         getInstrumentation().waitForIdleSync();
-        assertEquals("extra layouts should not happend", 1, mLayoutManager.getTotalLayoutCount());
+        assertEquals("extra layouts should not happen", 1, mLayoutManager.getTotalLayoutCount());
         assertEquals("all expected children should be laid out", firstLayoutItemCount,
                 mLayoutManager.getChildCount());
         return recyclerView;
     }
 
+    public void testNotifyDataSetChanged() throws Throwable {
+        setupBasic(10, 3, 4);
+        int layoutCount = mLayoutManager.mTotalLayoutCount;
+        mLayoutManager.expectLayouts(1);
+        runTestOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    mTestAdapter.deleteAndNotify(4, 1);
+                    mTestAdapter.notifyChange();
+                } catch (Throwable throwable) {
+                    throwable.printStackTrace();
+                }
+
+            }
+        });
+        mLayoutManager.waitForLayout(2);
+        getInstrumentation().waitForIdleSync();
+        assertEquals("on notify data set changed, predictive animations should not run",
+                layoutCount + 1, mLayoutManager.mTotalLayoutCount);
+        mLayoutManager.expectLayouts(2);
+        mTestAdapter.addAndNotify(4, 2);
+        // make sure animations recover
+        mLayoutManager.waitForLayout(2);
+    }
+
 
     public void testGetItemForDeletedView() throws Throwable {
         getItemForDeletedViewTest(false);
@@ -820,20 +838,6 @@
 
     }
 
-    private void postExceptionToInstrumentation(Throwable t) {
-        if (DEBUG) {
-            Log.e(TAG, "captured exception on main thread", t);
-        }
-        mainThreadException = t;
-        if (mLayoutManager instanceof TestLayoutManager) {
-            TestLayoutManager lm = mLayoutManager;
-            // finish all layouts so that we get the correct exception
-            while (lm.layoutLatch.getCount() > 0) {
-                lm.layoutLatch.countDown();
-            }
-        }
-    }
-
     abstract class AdapterOps {
 
         final public void run(TestAdapter adapter) throws Throwable {
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
index c662e31..64985ed 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
@@ -17,6 +17,8 @@
 
 package android.support.v7.widget;
 
+import android.view.View;
+
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -29,6 +31,88 @@
         super(DEBUG);
     }
 
+    public void testFindViewById() throws Throwable {
+        findViewByIdTest(false);
+        removeRecyclerView();
+        findViewByIdTest(true);
+    }
+
+    public void findViewByIdTest(final boolean supportPredictive) throws Throwable {
+        final RecyclerView recyclerView = new RecyclerView(getActivity());
+        final int initialAdapterSize = 20;
+        final TestAdapter adapter = new TestAdapter(initialAdapterSize);
+        final int deleteStart = 6;
+        final int deleteCount = 5;
+        recyclerView.setAdapter(adapter);
+        final AtomicBoolean assertPositions = new AtomicBoolean(false);
+        TestLayoutManager lm = new TestLayoutManager() {
+            @Override
+            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+                super.onLayoutChildren(recycler, state);
+                if (assertPositions.get()) {
+                    if (state.isPreLayout()) {
+                        for (int i = 0; i < deleteStart; i ++) {
+                            View view = findViewByPosition(i);
+                            assertNotNull("find view by position for existing items should work "
+                                    + "fine", view);
+                            assertFalse("view should not be marked as removed",
+                                    ((RecyclerView.LayoutParams) view.getLayoutParams())
+                                            .isItemRemoved());
+                        }
+                        for (int i = 0;  i < deleteCount; i++) {
+                            View view = findViewByPosition(i + deleteStart);
+                            assertNotNull("find view by position should work fine for removed "
+                                    + "views in pre-layout", view);
+                            assertTrue("view should be marked as removed",
+                                    ((RecyclerView.LayoutParams) view.getLayoutParams())
+                                            .isItemRemoved());
+                        }
+                        for (int i = deleteStart + deleteCount; i < 20; i++) {
+                            View view = findViewByPosition(i);
+                            assertNotNull(view);
+                            assertFalse("view should not be marked as removed",
+                                    ((RecyclerView.LayoutParams) view.getLayoutParams())
+                                            .isItemRemoved());
+                        }
+                    } else {
+                        for (int i = 0; i < initialAdapterSize - deleteCount; i ++) {
+                            View view = findViewByPosition(i);
+                            assertNotNull("find view by position for existing items should work "
+                                    + "fine", view);
+                            TestViewHolder viewHolder =
+                                    (TestViewHolder) mRecyclerView.getChildViewHolder(view);
+                            assertSame("should be the correct item " + viewHolder
+                                    ,viewHolder.mBindedItem,
+                                    adapter.mItems.get(viewHolder.mPosition));
+                            assertFalse("view should not be marked as removed",
+                                    ((RecyclerView.LayoutParams) view.getLayoutParams())
+                                            .isItemRemoved());
+                        }
+                    }
+
+                }
+                detachAndScrapAttachedViews(recycler);
+                layoutRange(recycler, state.getItemCount() - 1, -1);
+                layoutLatch.countDown();
+            }
+
+            @Override
+            public boolean supportsPredictiveItemAnimations() {
+                return supportPredictive;
+            }
+        };
+        recyclerView.setLayoutManager(lm);
+        lm.expectLayouts(1);
+        setRecyclerView(recyclerView);
+        lm.waitForLayout(2);
+        getInstrumentation().waitForIdleSync();
+
+        assertPositions.set(true);
+        lm.expectLayouts(supportPredictive ? 2 : 1);
+        adapter.deleteAndNotify(new int[]{deleteStart, deleteCount - 1}, new int[]{deleteStart, 1});
+        lm.waitForLayout(2);
+    }
+
     public void testTypeForCache() throws Throwable {
         final AtomicInteger viewType = new AtomicInteger(1);
         final TestAdapter adapter = new TestAdapter(100) {
@@ -85,6 +169,62 @@
         });
     }
 
+    public void testTypeForExistingViews() throws Throwable {
+        final AtomicInteger viewType = new AtomicInteger(1);
+        final int invalidatedCount = 2;
+        final int layoutStart = 2;
+        final TestAdapter adapter = new TestAdapter(100) {
+            @Override
+            public int getItemViewType(int position) {
+                return viewType.get();
+            }
+
+            @Override
+            public void onBindViewHolder(TestViewHolder holder,
+                    int position) {
+                super.onBindViewHolder(holder, position);
+                if (position >= layoutStart && position < invalidatedCount + layoutStart) {
+                    try {
+                        assertEquals("holder type should match current view type at position " +
+                                position, viewType.get(), holder.getItemViewType());
+                    } catch (Throwable t) {
+                        postExceptionToInstrumentation(t);
+                    }
+                }
+            }
+
+            @Override
+            public long getItemId(int position) {
+                return mItems.get(position).mId;
+            }
+        };
+        adapter.setHasStableIds(true);
+
+        final int childCount = 10;
+        final TestLayoutManager lm = new TestLayoutManager() {
+            @Override
+            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+                super.onLayoutChildren(recycler, state);
+                detachAndScrapAttachedViews(recycler);
+                layoutRange(recycler, layoutStart, layoutStart + childCount);
+                layoutLatch.countDown();
+            }
+        };
+        final RecyclerView recyclerView = new RecyclerView(getActivity());
+        recyclerView.setAdapter(adapter);
+        recyclerView.setLayoutManager(lm);
+        lm.expectLayouts(1);
+        setRecyclerView(recyclerView);
+        lm.waitForLayout(2);
+        getInstrumentation().waitForIdleSync();
+        viewType.incrementAndGet();
+        lm.expectLayouts(1);
+        adapter.notifyItemChange(layoutStart, invalidatedCount);
+        lm.waitForLayout(2);
+        checkForMainThreadException();
+    }
+
+
     public void testState() throws Throwable {
         final TestAdapter adapter = new TestAdapter(10);
         final RecyclerView recyclerView = new RecyclerView(getActivity());
@@ -147,4 +287,4 @@
 
     }
 
-}
\ No newline at end of file
+}