Utility class to calculate difference between two lists.

Change-Id: I8a5b661f4ad6dc0851f72fae63a2187f91fc8cb8
diff --git a/api/current.txt b/api/current.txt
index c7303c6..d72ea41 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -8807,6 +8807,41 @@
     field public static final int HINT_SCROLL_NONE = 0; // 0x0
   }
 
+  public class BatchingListUpdateCallback implements android.support.v7.util.ListUpdateCallback {
+    ctor public BatchingListUpdateCallback(android.support.v7.util.ListUpdateCallback);
+    method public void dispatchLastEvent();
+    method public void onChanged(int, int, java.lang.Object);
+    method public void onInserted(int, int);
+    method public void onMoved(int, int);
+    method public void onRemoved(int, int);
+  }
+
+  public class DiffUtil {
+    method public static android.support.v7.util.DiffUtil.DiffResult calculateDiff(android.support.v7.util.DiffUtil.Callback);
+    method public static android.support.v7.util.DiffUtil.DiffResult calculateDiff(android.support.v7.util.DiffUtil.Callback, boolean);
+  }
+
+  public static abstract class DiffUtil.Callback {
+    ctor public DiffUtil.Callback();
+    method public abstract boolean areContentsTheSame(int, int);
+    method public abstract boolean areItemsTheSame(int, int);
+    method public java.lang.Object getChangePayload(int, int);
+    method public abstract int getNewListSize();
+    method public abstract int getOldListSize();
+  }
+
+  public static class DiffUtil.DiffResult {
+    method public void dispatchUpdatesTo(android.support.v7.widget.RecyclerView.Adapter);
+    method public void dispatchUpdatesTo(android.support.v7.util.ListUpdateCallback);
+  }
+
+  public abstract interface ListUpdateCallback {
+    method public abstract void onChanged(int, int, java.lang.Object);
+    method public abstract void onInserted(int, int);
+    method public abstract void onMoved(int, int);
+    method public abstract void onRemoved(int, int);
+  }
+
   public class SortedList {
     ctor public SortedList(java.lang.Class<T>, android.support.v7.util.SortedList.Callback<T>);
     ctor public SortedList(java.lang.Class<T>, android.support.v7.util.SortedList.Callback<T>, int);
@@ -8839,15 +8874,13 @@
     method public void onRemoved(int, int);
   }
 
-  public static abstract class SortedList.Callback implements java.util.Comparator {
+  public static abstract class SortedList.Callback implements java.util.Comparator android.support.v7.util.ListUpdateCallback {
     ctor public SortedList.Callback();
     method public abstract boolean areContentsTheSame(T2, T2);
     method public abstract boolean areItemsTheSame(T2, T2);
     method public abstract int compare(T2, T2);
     method public abstract void onChanged(int, int);
-    method public abstract void onInserted(int, int);
-    method public abstract void onMoved(int, int);
-    method public abstract void onRemoved(int, int);
+    method public void onChanged(int, int, java.lang.Object);
   }
 
 }
diff --git a/samples/Support7Demos/AndroidManifest.xml b/samples/Support7Demos/AndroidManifest.xml
index a75879e..066afc3 100644
--- a/samples/Support7Demos/AndroidManifest.xml
+++ b/samples/Support7Demos/AndroidManifest.xml
@@ -407,6 +407,15 @@
             </intent-filter>
         </activity>
 
+        <activity android:name=".util.DiffUtilActivity"
+                  android:label="@string/diff_util_activity"
+                  android:theme="@style/Theme.AppCompat">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="com.example.android.supportv7.SAMPLE_CODE" />
+            </intent-filter>
+        </activity>
+
         <activity android:name=".widget.GridLayoutManagerActivity"
                   android:label="@string/grid_layout_manager"
                   android:theme="@style/Theme.AppCompat">
diff --git a/samples/Support7Demos/res/values/strings.xml b/samples/Support7Demos/res/values/strings.xml
index edafbd1..4bf046e 100644
--- a/samples/Support7Demos/res/values/strings.xml
+++ b/samples/Support7Demos/res/values/strings.xml
@@ -167,6 +167,7 @@
     <string name="palette_all_colors">Full color palette</string>
     <string name="search_hint">Search...</string>
     <string name="sorted_list_activity">RecyclerView/Sorted List</string>
+    <string name="diff_util_activity">RecyclerView/Diff Util</string>
     <string name="add_new_item">Add New Item</string>
     <string name="start_action_mode">Start Action Mode</string>
 
diff --git a/samples/Support7Demos/src/com/example/android/supportv7/util/DiffUtilActivity.java b/samples/Support7Demos/src/com/example/android/supportv7/util/DiffUtilActivity.java
new file mode 100644
index 0000000..304f4af
--- /dev/null
+++ b/samples/Support7Demos/src/com/example/android/supportv7/util/DiffUtilActivity.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.example.android.supportv7.util;
+
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v4.util.Pair;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.util.DiffUtil;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.Toast;
+
+import com.example.android.supportv7.Cheeses;
+import com.example.android.supportv7.widget.adapter.SimpleStringAdapter;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * A sample activity that demonstrates usage if {@link android.support.v7.util.DiffUtil} with
+ * a RecyclerView.
+ */
+public class DiffUtilActivity extends AppCompatActivity {
+    private Random mRandom = new Random(System.nanoTime());
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        LinearLayout ll = new LinearLayout(this);
+        RecyclerView rv = new RecyclerView(this);
+        Button shuffle = new Button(this);
+        shuffle.setText("Shuffle");
+        ll.addView(shuffle);
+        ll.addView(rv);
+        rv.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+                ViewGroup.LayoutParams.MATCH_PARENT));
+        rv.setLayoutManager(new LinearLayoutManager(this));
+        List<String> cheeseList = createRandomCheeseList(Collections.<String>emptyList(), 50);
+        final SimpleStringAdapter adapter =
+                new SimpleStringAdapter(this, cheeseList.toArray(new String[cheeseList.size()]));
+        rv.setAdapter(adapter);
+        final AtomicBoolean refreshingList = new AtomicBoolean(false);
+        shuffle.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                if (refreshingList.getAndSet(true)) {
+                    // already refreshing, do not allow modifications
+                    return;
+                }
+                //noinspection unchecked
+                new AsyncTask<List<String>, Void, Pair<List<String>, DiffUtil.DiffResult>>() {
+
+                    @Override
+                    protected Pair<List<String>, DiffUtil.DiffResult> doInBackground(
+                            List<String>... lists) {
+                        List<String> oldList = lists[0];
+                        List<String> newList = createRandomCheeseList(oldList, 5);
+                        DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(
+                                new MyCallback(oldList, newList));
+                        //noinspection unchecked
+                        return new Pair(newList, diffResult);
+                    }
+
+                    @Override
+                    protected void onPostExecute(
+                            Pair<List<String>, DiffUtil.DiffResult> resultPair) {
+                        refreshingList.set(false);
+                        adapter.setValues(resultPair.first);
+                        resultPair.second.dispatchUpdatesTo(adapter);
+                        Toast.makeText(DiffUtilActivity.this, "new list size "
+                                + resultPair.first.size(), Toast.LENGTH_SHORT).show();
+                    }
+                }.execute(adapter.getValues());
+
+            }
+        });
+        setContentView(ll);
+    }
+
+    private static class MyCallback extends DiffUtil.Callback {
+        private final List<String> mOld;
+        private final List<String> mNew;
+
+        public MyCallback(List<String> old, List<String> aNew) {
+            mOld = old;
+            mNew = aNew;
+        }
+
+        @Override
+        public int getOldListSize() {
+            return mOld.size();
+        }
+
+        @Override
+        public int getNewListSize() {
+            return mNew.size();
+        }
+
+        @Override
+        public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
+            // for strings, content equality is the same as identitiy equality since we don't have
+            // duplicates in this sample.
+            return mOld.get(oldItemPosition).equals(mNew.get(newItemPosition));
+        }
+
+        @Override
+        public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
+            return mOld.get(oldItemPosition).equals(mNew.get(newItemPosition));
+        }
+    }
+
+    private List<String> createRandomCheeseList(List<String> seed, int iterations) {
+        List<String> output = new ArrayList<>();
+        output.addAll(seed);
+        for (int i = 0; i < iterations; i++) {
+            switch (mRandom.nextInt(3)) {
+                case 0: //add
+                    output.add(mRandom.nextInt(1 + output.size()), getRandomCheese(output));
+                    break;
+                case 1: // remove
+                    if (output.size() > 0) {
+                        output.remove(mRandom.nextInt(output.size()));
+                    }
+                    break;
+                case 2: // move
+                    if (output.size() > 0) {
+                        int from = mRandom.nextInt(output.size());
+                        int to = mRandom.nextInt(output.size());
+                        output.add(to, output.remove(from));
+                    }
+                    break;
+            }
+        }
+        return output;
+    }
+
+    private String getRandomCheese(List<String> excludes) {
+        String chosen = Cheeses.sCheeseStrings[mRandom.nextInt(Cheeses.sCheeseStrings.length)];
+        while (excludes.contains(chosen)) {
+            chosen = Cheeses.sCheeseStrings[mRandom.nextInt(Cheeses.sCheeseStrings.length)];
+        }
+        return chosen;
+    }
+}
diff --git a/samples/Support7Demos/src/com/example/android/supportv7/widget/adapter/SimpleStringAdapter.java b/samples/Support7Demos/src/com/example/android/supportv7/widget/adapter/SimpleStringAdapter.java
index 49f1e196..04161ea 100644
--- a/samples/Support7Demos/src/com/example/android/supportv7/widget/adapter/SimpleStringAdapter.java
+++ b/samples/Support7Demos/src/com/example/android/supportv7/widget/adapter/SimpleStringAdapter.java
@@ -25,12 +25,13 @@
 
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.List;
 
 public class SimpleStringAdapter extends RecyclerView.Adapter<SimpleStringAdapter.ViewHolder> {
 
     private int mBackground;
 
-    private ArrayList<String> mValues;
+    private List<String> mValues;
 
     public static class ViewHolder extends RecyclerView.ViewHolder {
         public String mBoundString;
@@ -110,4 +111,12 @@
     public int getItemCount() {
         return mValues.size();
     }
+
+    public List<String> getValues() {
+        return mValues;
+    }
+
+    public void setValues(List<String> values) {
+        mValues = values;
+    }
 }
diff --git a/v7/recyclerview/build.gradle b/v7/recyclerview/build.gradle
index 7a6391e..52ec972 100644
--- a/v7/recyclerview/build.gradle
+++ b/v7/recyclerview/build.gradle
@@ -13,6 +13,7 @@
         exclude module: 'support-annotations'
     }
     testCompile 'junit:junit:4.12'
+    testCompile "org.mockito:mockito-core:1.9.5"
     androidTestCompile "org.mockito:mockito-core:1.9.5"
     androidTestCompile "com.google.dexmaker:dexmaker:1.2"
     androidTestCompile "com.google.dexmaker:dexmaker-mockito:1.2"
@@ -32,7 +33,7 @@
         main.res.srcDirs 'res', 'res-public'
 
         androidTest.setRoot('tests')
-        test.java.srcDir 'jvm-tests'
+        test.java.srcDir 'jvm-tests/src'
         androidTest.java.srcDir 'tests/src'
         androidTest.res.srcDir 'tests/res'
         androidTest.manifest.srcFile 'tests/AndroidManifest.xml'
diff --git a/v7/recyclerview/jvm-tests/src/android/support/v7/util/BatchingListUpdateCallbackTest.java b/v7/recyclerview/jvm-tests/src/android/support/v7/util/BatchingListUpdateCallbackTest.java
new file mode 100644
index 0000000..851bad6
--- /dev/null
+++ b/v7/recyclerview/jvm-tests/src/android/support/v7/util/BatchingListUpdateCallbackTest.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v7.util;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import static org.mockito.Mockito.*;
+
+@RunWith(JUnit4.class)
+@SmallTest
+public class BatchingListUpdateCallbackTest {
+    BatchingListUpdateCallback mBatching;
+    ListUpdateCallback mCallback;
+
+    @Before
+    public void setup() {
+        mCallback = mock(ListUpdateCallback.class);
+        mBatching = new BatchingListUpdateCallback(mCallback);
+    }
+
+    @Test
+    public void addSimple() {
+        mBatching.onInserted(3, 2);
+        mBatching.dispatchLastEvent();
+        verify(mCallback).onInserted(3, 2);
+        verifyNoMoreInteractions(mCallback);
+    }
+
+    @Test
+    public void addToSamePos() {
+        mBatching.onInserted(3, 2);
+        mBatching.onInserted(3, 1);
+        mBatching.dispatchLastEvent();
+        verify(mCallback).onInserted(3, 3);
+        verifyNoMoreInteractions(mCallback);
+    }
+
+    @Test
+    public void addInsidePrevious() {
+        mBatching.onInserted(3, 5);
+        mBatching.onInserted(5, 1);
+        mBatching.dispatchLastEvent();
+        verify(mCallback).onInserted(3, 6);
+        verifyNoMoreInteractions(mCallback);
+    }
+
+    @Test
+    public void addBefore() {
+        mBatching.onInserted(3, 5);
+        mBatching.onInserted(2, 1);
+        mBatching.dispatchLastEvent();
+        verify(mCallback).onInserted(3, 5);
+        verify(mCallback).onInserted(2, 1);
+        verifyNoMoreInteractions(mCallback);
+    }
+
+    @Test
+    public void removeSimple() {
+        mBatching.onRemoved(3, 2);
+        mBatching.dispatchLastEvent();
+        verify(mCallback).onRemoved(3, 2);
+        verifyNoMoreInteractions(mCallback);
+    }
+
+    @Test
+    public void removeSamePosition() {
+        mBatching.onRemoved(3, 2);
+        mBatching.onRemoved(3, 1);
+        mBatching.dispatchLastEvent();
+        verify(mCallback).onRemoved(3, 3);
+        verifyNoMoreInteractions(mCallback);
+    }
+
+    @Test
+    public void removeInside() {
+        mBatching.onRemoved(3, 5);
+        mBatching.onRemoved(4, 2);
+        mBatching.dispatchLastEvent();
+        verify(mCallback).onRemoved(3, 5);
+        verify(mCallback).onRemoved(4, 2);
+        verifyNoMoreInteractions(mCallback);
+    }
+
+    @Test
+    public void removeBefore() {
+        mBatching.onRemoved(3, 2);
+        mBatching.onRemoved(2, 1);
+        mBatching.dispatchLastEvent();
+        verify(mCallback).onRemoved(2, 3);
+        verifyNoMoreInteractions(mCallback);
+    }
+
+    @Test
+    public void removeBefore2() {
+        mBatching.onRemoved(3, 2);
+        mBatching.onRemoved(2, 4);
+        mBatching.dispatchLastEvent();
+        verify(mCallback).onRemoved(2, 6);
+        verifyNoMoreInteractions(mCallback);
+    }
+
+    @Test
+    public void removeBefore3() {
+        mBatching.onRemoved(3, 2);
+        mBatching.onRemoved(1, 1);
+        mBatching.dispatchLastEvent();
+        verify(mCallback).onRemoved(3, 2);
+        verify(mCallback).onRemoved(1, 1);
+        verifyNoMoreInteractions(mCallback);
+    }
+
+    @Test
+    public void moveSimple() {
+        mBatching.onMoved(3, 2);
+        mBatching.dispatchLastEvent();
+        verify(mCallback).onMoved(3, 2);
+        verifyNoMoreInteractions(mCallback);
+    }
+
+    @Test
+    public void moveTwice() {
+        mBatching.onMoved(3, 2);
+        mBatching.onMoved(5, 6);
+        mBatching.dispatchLastEvent();
+        verify(mCallback).onMoved(3, 2);
+        verify(mCallback).onMoved(5, 6);
+        verifyNoMoreInteractions(mCallback);
+    }
+
+    @Test
+    public void changeSimple() {
+        mBatching.onChanged(3, 2, null);
+        mBatching.dispatchLastEvent();
+        verify(mCallback).onChanged(3, 2, null);
+        verifyNoMoreInteractions(mCallback);
+    }
+
+    @Test
+    public void changeConsecutive() {
+        mBatching.onChanged(3, 2, null);
+        mBatching.onChanged(5, 2, null);
+        mBatching.dispatchLastEvent();
+        verify(mCallback).onChanged(3, 4, null);
+        verifyNoMoreInteractions(mCallback);
+    }
+
+    @Test
+    public void changeTheSame() {
+        mBatching.onChanged(3, 2, null);
+        mBatching.onChanged(4, 2, null);
+        mBatching.dispatchLastEvent();
+        verify(mCallback).onChanged(3, 3, null);
+        verifyNoMoreInteractions(mCallback);
+    }
+
+    @Test
+    public void changeTheSame2() {
+        mBatching.onChanged(3, 2, null);
+        mBatching.onChanged(3, 2, null);
+        mBatching.dispatchLastEvent();
+        verify(mCallback).onChanged(3, 2, null);
+        verifyNoMoreInteractions(mCallback);
+    }
+
+    @Test
+    public void changeBefore() {
+        mBatching.onChanged(3, 2, null);
+        mBatching.onChanged(2, 1, null);
+        mBatching.dispatchLastEvent();
+        verify(mCallback).onChanged(2, 3, null);
+        verifyNoMoreInteractions(mCallback);
+    }
+
+    @Test
+    public void changeBeforeOverlap() {
+        mBatching.onChanged(3, 2, null);
+        mBatching.onChanged(2, 2, null);
+        mBatching.dispatchLastEvent();
+        verify(mCallback).onChanged(2, 3, null);
+        verifyNoMoreInteractions(mCallback);
+    }
+
+    @Test
+    public void changeSimpleWithPayload() {
+        Object payload = new Object();
+        mBatching.onChanged(3, 2, payload);
+        mBatching.dispatchLastEvent();
+        verify(mCallback).onChanged(3, 2, payload);
+    }
+
+    @Test
+    public void changeConsecutiveWithPayload() {
+        Object payload = new Object();
+        mBatching.onChanged(3, 2, payload);
+        mBatching.onChanged(5, 2, payload);
+        mBatching.dispatchLastEvent();
+        verify(mCallback).onChanged(3, 4, payload);
+        verifyNoMoreInteractions(mCallback);
+    }
+
+    @Test
+    public void changeTheSameWithPayload() {
+        Object payload = new Object();
+        mBatching.onChanged(3, 2, payload);
+        mBatching.onChanged(4, 2, payload);
+        mBatching.dispatchLastEvent();
+        verify(mCallback).onChanged(3, 3, payload);
+        verifyNoMoreInteractions(mCallback);
+    }
+
+    @Test
+    public void changeTheSame2WithPayload() {
+        Object payload = new Object();
+        mBatching.onChanged(3, 2, payload);
+        mBatching.onChanged(3, 2, payload);
+        mBatching.dispatchLastEvent();
+        verify(mCallback).onChanged(3, 2, payload);
+        verifyNoMoreInteractions(mCallback);
+    }
+
+    @Test
+    public void changeBeforeWithPayload() {
+        Object payload = new Object();
+        mBatching.onChanged(3, 2, payload);
+        mBatching.onChanged(2, 1, payload);
+        mBatching.dispatchLastEvent();
+        verify(mCallback).onChanged(2, 3, payload);
+        verifyNoMoreInteractions(mCallback);
+    }
+
+    @Test
+    public void changeBeforeOverlapWithPayload() {
+        Object payload = new Object();
+        mBatching.onChanged(3, 2, payload);
+        mBatching.onChanged(2, 2, payload);
+        mBatching.dispatchLastEvent();
+        verify(mCallback).onChanged(2, 3, payload);
+        verifyNoMoreInteractions(mCallback);
+    }
+
+    @Test
+    public void changeWithNewPayload() {
+        Object payload1 = new Object();
+        Object payload2 = new Object();
+        mBatching.onChanged(3, 2, payload1);
+        mBatching.onChanged(2, 2, payload2);
+        mBatching.dispatchLastEvent();
+        verify(mCallback).onChanged(3, 2, payload1);
+        verify(mCallback).onChanged(2, 2, payload2);
+        verifyNoMoreInteractions(mCallback);
+    }
+
+    @Test
+    public void changeWithEmptyPayload() {
+        Object payload = new Object();
+        mBatching.onChanged(3, 2, payload);
+        mBatching.onChanged(2, 2, null);
+        mBatching.dispatchLastEvent();
+        verify(mCallback).onChanged(3, 2, payload);
+        verify(mCallback).onChanged(2, 2, null);
+        verifyNoMoreInteractions(mCallback);
+    }
+
+    @Test
+    public void changeWithEmptyPayload2() {
+        Object payload = new Object();
+        mBatching.onChanged(3, 2, null);
+        mBatching.onChanged(2, 2, payload);
+        mBatching.dispatchLastEvent();
+        verify(mCallback).onChanged(3, 2, null);
+        verify(mCallback).onChanged(2, 2, payload);
+        verifyNoMoreInteractions(mCallback);
+    }
+}
diff --git a/v7/recyclerview/jvm-tests/src/android/support/v7/util/DiffUtilTest.java b/v7/recyclerview/jvm-tests/src/android/support/v7/util/DiffUtilTest.java
new file mode 100644
index 0000000..bf9c7b8
--- /dev/null
+++ b/v7/recyclerview/jvm-tests/src/android/support/v7/util/DiffUtilTest.java
@@ -0,0 +1,511 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v7.util;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.support.annotation.Nullable;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import org.hamcrest.CoreMatchers;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+import java.util.UUID;
+
+@RunWith(JUnit4.class)
+@SmallTest
+public class DiffUtilTest {
+    private static Random sRand = new Random(System.nanoTime());
+    private List<Item> mBefore = new ArrayList<>();
+    private List<Item> mAfter = new ArrayList<>();
+    private StringBuilder mLog = new StringBuilder();
+
+    private DiffUtil.Callback mCallback = new DiffUtil.Callback() {
+        @Override
+        public int getOldListSize() {
+            return mBefore.size();
+        }
+
+        @Override
+        public int getNewListSize() {
+            return mAfter.size();
+        }
+
+        @Override
+        public boolean areItemsTheSame(int oldItemIndex, int newItemIndex) {
+            return mBefore.get(oldItemIndex).id == mAfter.get(newItemIndex).id;
+        }
+
+        @Override
+        public boolean areContentsTheSame(int oldItemIndex, int newItemIndex) {
+            assertThat(mBefore.get(oldItemIndex).id,
+                    CoreMatchers.equalTo(mAfter.get(newItemIndex).id));
+            return mBefore.get(oldItemIndex).data.equals(mAfter.get(newItemIndex).data);
+        }
+
+        @Nullable
+        @Override
+        public Object getChangePayload(int oldItemIndex, int newItemIndex) {
+            assertThat(mBefore.get(oldItemIndex).id,
+                    CoreMatchers.equalTo(mAfter.get(newItemIndex).id));
+            assertThat(mBefore.get(oldItemIndex).data,
+                    not(CoreMatchers.equalTo(mAfter.get(newItemIndex).data)));
+            return mAfter.get(newItemIndex).payload;
+        }
+    };
+
+    @Rule
+    public TestWatcher mLogOnExceptionWatcher = new TestWatcher() {
+        @Override
+        protected void failed(Throwable e, Description description) {
+            System.err.println(mLog.toString());
+        }
+    };
+
+
+    @Test
+    public void testNoChange() {
+        initWithSize(5);
+        check();
+    }
+
+    @Test
+    public void testAddItems() {
+        initWithSize(2);
+        add(1);
+        check();
+    }
+
+    @Test
+    @LargeTest
+    public void testRandom() {
+        for (int x = 0; x < 100; x++) {
+            for (int i = 0; i < 100; i++) {
+                for (int j = 2; j < 40; j++) {
+                    testRandom(i, j);
+                }
+            }
+        }
+    }
+
+    @Test
+    public void testGen2() {
+        initWithSize(5);
+        add(5);
+        delete(3);
+        delete(1);
+        check();
+    }
+
+    @Test
+    public void testGen3() {
+        initWithSize(5);
+        add(0);
+        delete(1);
+        delete(3);
+        check();
+    }
+
+    @Test
+    public void testGen4() {
+        initWithSize(5);
+        add(5);
+        add(1);
+        add(4);
+        add(4);
+        check();
+    }
+
+    @Test
+    public void testGen5() {
+        initWithSize(5);
+        delete(0);
+        delete(2);
+        add(0);
+        add(2);
+        check();
+    }
+
+    @Test
+    public void testGen6() {
+        initWithSize(2);
+        delete(0);
+        delete(0);
+        check();
+    }
+
+    @Test
+    public void testGen7() {
+        initWithSize(3);
+        move(2, 0);
+        delete(2);
+        add(2);
+        check();
+    }
+
+    @Test
+    public void testGen8() {
+        initWithSize(3);
+        delete(1);
+        add(0);
+        move(2, 0);
+        check();
+    }
+
+    @Test
+    public void testGen9() {
+        initWithSize(2);
+        add(2);
+        move(0, 2);
+        check();
+    }
+
+    @Test
+    public void testGen10() {
+        initWithSize(3);
+        move(0, 1);
+        move(1, 2);
+        add(0);
+        check();
+    }
+
+    @Test
+    public void testGen11() {
+        initWithSize(4);
+        move(2, 0);
+        move(2, 3);
+        check();
+    }
+
+    @Test
+    public void testGen12() {
+        initWithSize(4);
+        move(3, 0);
+        move(2, 1);
+        check();
+    }
+
+    @Test
+    public void testGen13() {
+        initWithSize(4);
+        move(3, 2);
+        move(0, 3);
+        check();
+    }
+
+    @Test
+    public void testGen14() {
+        initWithSize(4);
+        move(3, 2);
+        add(4);
+        move(0, 4);
+        check();
+    }
+
+    @Test
+    public void testAdd1() {
+        initWithSize(1);
+        add(1);
+        check();
+    }
+
+    @Test
+    public void testMove1() {
+        initWithSize(3);
+        move(0, 2);
+        check();
+    }
+
+    @Test
+    public void tmp() {
+        initWithSize(4);
+        move(0, 2);
+        check();
+    }
+
+    @Test
+    public void testUpdate1() {
+        initWithSize(3);
+        update(2);
+        check();
+    }
+
+    @Test
+    public void testUpdate2() {
+        initWithSize(2);
+        add(1);
+        update(1);
+        update(2);
+        check();
+    }
+
+    @Test
+    public void testDisableMoveDetection() {
+        initWithSize(5);
+        move(0, 4);
+        List<Item> applied = applyUpdates(mBefore, DiffUtil.calculateDiff(mCallback, false));
+        assertThat(applied.size(), is(5));
+        assertThat(applied.get(4).newItem, is(true));
+        assertThat(applied.contains(mBefore.get(0)), is(false));
+    }
+
+    private void testRandom(int initialSize, int operationCount) {
+        mLog.setLength(0);
+        initWithSize(initialSize);
+        for (int i = 0; i < operationCount; i++) {
+            int op = sRand.nextInt(5);
+            switch (op) {
+                case 0:
+                    add(sRand.nextInt(mAfter.size() + 1));
+                    break;
+                case 1:
+                    if (!mAfter.isEmpty()) {
+                        delete(sRand.nextInt(mAfter.size()));
+                    }
+                    break;
+                case 2:
+                    // move
+                    if (mAfter.size() > 0) {
+                        move(sRand.nextInt(mAfter.size()), sRand.nextInt(mAfter.size()));
+                    }
+                    break;
+                case 3:
+                    // update
+                    if (mAfter.size() > 0) {
+                        update(sRand.nextInt(mAfter.size()));
+                    }
+                    break;
+                case 4:
+                    // update with payload
+                    if (mAfter.size() > 0) {
+                        updateWithPayload(sRand.nextInt(mAfter.size()));
+                    }
+                    break;
+            }
+        }
+        check();
+    }
+
+    private void check() {
+        DiffUtil.DiffResult result = DiffUtil.calculateDiff(mCallback);
+        log("before", mBefore);
+        log("after", mAfter);
+        log("snakes", result.getSnakes());
+
+        List<Item> applied = applyUpdates(mBefore, result);
+        assertEquals(applied, mAfter);
+    }
+
+    private void initWithSize(int size) {
+        mBefore.clear();
+        mAfter.clear();
+        for (int i = 0; i < size; i++) {
+            mBefore.add(new Item(false));
+        }
+        mAfter.addAll(mBefore);
+        mLog.append("initWithSize(" + size + ");\n");
+    }
+
+    private void log(String title, List<?> items) {
+        mLog.append(title).append(":").append(items.size()).append("\n");
+        for (Object item : items) {
+            mLog.append("  ").append(item).append("\n");
+        }
+    }
+
+    private void assertEquals(List<Item> applied, List<Item> after) {
+        log("applied", applied);
+
+        String report = mLog.toString();
+        assertThat(report, applied.size(), is(after.size()));
+        for (int i = 0; i < after.size(); i++) {
+            Item item = applied.get(i);
+            if (after.get(i).newItem) {
+                assertThat(report, item.newItem, is(true));
+            } else if (after.get(i).changed) {
+                assertThat(report, item.newItem, is(false));
+                assertThat(report, item.changed, is(true));
+                assertThat(report, item.id, is(after.get(i).id));
+                assertThat(report, item.payload, is(after.get(i).payload));
+            } else {
+                assertThat(report, item, equalTo(after.get(i)));
+            }
+        }
+    }
+
+    private List<Item> applyUpdates(List<Item> before, DiffUtil.DiffResult result) {
+        final List<Item> target = new ArrayList<>();
+        target.addAll(before);
+        result.dispatchUpdatesTo(new ListUpdateCallback() {
+            @Override
+            public void onInserted(int position, int count) {
+                for (int i = 0; i < count; i++) {
+                    target.add(i + position, new Item(true));
+                }
+            }
+
+            @Override
+            public void onRemoved(int position, int count) {
+                for (int i = 0; i < count; i++) {
+                    target.remove(position);
+                }
+            }
+
+            @Override
+            public void onMoved(int fromPosition, int toPosition) {
+                Item item = target.remove(fromPosition);
+                target.add(toPosition, item);
+            }
+
+            @Override
+            public void onChanged(int position, int count, Object payload) {
+                for (int i = 0; i < count; i++) {
+                    int positionInList = position + i;
+                    Item existing = target.get(positionInList);
+                    // make sure we don't update same item twice in callbacks
+                    assertThat(existing.changed, is(false));
+                    assertThat(existing.newItem, is(false));
+                    assertThat(existing.payload, is(nullValue()));
+                    Item replica = new Item(existing);
+                    replica.payload = (String) payload;
+                    replica.changed = true;
+                    target.remove(positionInList);
+                    target.add(positionInList, replica);
+                }
+            }
+        });
+        return target;
+    }
+
+    private void add(int index) {
+        mAfter.add(index, new Item(true));
+        mLog.append("add(").append(index).append(");\n");
+    }
+
+    private void delete(int index) {
+        mAfter.remove(index);
+        mLog.append("delete(").append(index).append(");\n");
+    }
+
+    private void update(int index) {
+        Item existing = mAfter.get(index);
+        if (existing.newItem) {
+            return;//new item cannot be changed
+        }
+        Item replica = new Item(existing);
+        replica.changed = true;
+        // clean the payload since this might be after an updateWithPayload call
+        replica.payload = null;
+        replica.data = UUID.randomUUID().toString();
+        mAfter.remove(index);
+        mAfter.add(index, replica);
+        mLog.append("update(").append(index).append(");\n");
+    }
+
+    private void updateWithPayload(int index) {
+        Item existing = mAfter.get(index);
+        if (existing.newItem) {
+            return;//new item cannot be changed
+        }
+        Item replica = new Item(existing);
+        replica.changed = true;
+        replica.data = UUID.randomUUID().toString();
+        replica.payload = UUID.randomUUID().toString();
+        mAfter.remove(index);
+        mAfter.add(index, replica);
+        mLog.append("update(").append(index).append(");\n");
+    }
+
+    private void move(int from, int to) {
+        Item removed = mAfter.remove(from);
+        mAfter.add(to, removed);
+        mLog.append("move(").append(from).append(",").append(to).append(");\n");
+    }
+
+    static class Item {
+        static long idCounter = 0;
+        final long id;
+        final boolean newItem;
+        boolean changed = false;
+        String payload;
+
+        String data = UUID.randomUUID().toString();
+
+        public Item(boolean newItem) {
+            id = idCounter++;
+            this.newItem = newItem;
+        }
+
+        public Item(Item other) {
+            id = other.id;
+            newItem = other.newItem;
+            changed = other.changed;
+            payload = other.payload;
+            data = other.data;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+
+            Item item = (Item) o;
+
+            if (id != item.id) return false;
+            if (newItem != item.newItem) return false;
+            if (changed != item.changed) return false;
+            if (payload != null ? !payload.equals(item.payload) : item.payload != null) {
+                return false;
+            }
+            return data.equals(item.data);
+
+        }
+
+        @Override
+        public int hashCode() {
+            int result = (int) (id ^ (id >>> 32));
+            result = 31 * result + (newItem ? 1 : 0);
+            result = 31 * result + (changed ? 1 : 0);
+            result = 31 * result + (payload != null ? payload.hashCode() : 0);
+            result = 31 * result + data.hashCode();
+            return result;
+        }
+
+        @Override
+        public String toString() {
+            return "Item{" +
+                    "id=" + id +
+                    ", newItem=" + newItem +
+                    ", changed=" + changed +
+                    ", payload='" + payload + '\'' +
+                    ", data='" + data + '\'' +
+                    '}';
+        }
+    }
+}
diff --git a/v7/recyclerview/jvm-tests/src/android/support/v7/util/SortedListAdapterCallbackWrapperTest.java b/v7/recyclerview/jvm-tests/src/android/support/v7/util/SortedListAdapterCallbackWrapperTest.java
deleted file mode 100644
index e803d2a..0000000
--- a/v7/recyclerview/jvm-tests/src/android/support/v7/util/SortedListAdapterCallbackWrapperTest.java
+++ /dev/null
@@ -1,340 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.v7.util;
-
-import junit.framework.TestCase;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import android.test.suitebuilder.annotation.SmallTest;
-
-import static android.support.v7.util.SortedList.BatchedCallback.TYPE_NONE;
-import static android.support.v7.util.SortedList.BatchedCallback.TYPE_ADD;
-import static android.support.v7.util.SortedList.BatchedCallback.TYPE_REMOVE;
-import static android.support.v7.util.SortedList.BatchedCallback.TYPE_CHANGE;
-import static android.support.v7.util.SortedList.BatchedCallback.TYPE_MOVE;
-
-@RunWith(JUnit4.class)
-@SmallTest
-public class SortedListAdapterCallbackWrapperTest extends TestCase {
-
-    private int lastReceivedType = TYPE_NONE;
-    private int lastReceivedPosition = -1;
-    private int lastReceivedCount = -1;
-
-    private SortedList.Callback<Object> mCallback = new SortedList.Callback<Object>() {
-        @Override
-        public int compare(Object o1, Object o2) {
-            return 0;
-        }
-
-        @Override
-        public void onInserted(int position, int count) {
-            lastReceivedType = TYPE_ADD;
-            lastReceivedPosition = position;
-            lastReceivedCount = count;
-        }
-
-        @Override
-        public void onRemoved(int position, int count) {
-            lastReceivedType = TYPE_REMOVE;
-            lastReceivedPosition = position;
-            lastReceivedCount = count;
-        }
-
-        @Override
-        public void onMoved(int fromPosition, int toPosition) {
-            lastReceivedType = TYPE_MOVE;
-            lastReceivedPosition = fromPosition;
-            lastReceivedCount = toPosition;
-        }
-
-        @Override
-        public void onChanged(int position, int count) {
-            lastReceivedType = TYPE_CHANGE;
-            lastReceivedPosition = position;
-            lastReceivedCount = count;
-        }
-
-        @Override
-        public boolean areContentsTheSame(Object oldItem, Object newItem) {
-            return false;
-        }
-
-        @Override
-        public boolean areItemsTheSame(Object item1, Object item2) {
-            return false;
-        }
-    };
-
-    private SortedList.BatchedCallback<Object> mBatched =
-            new SortedList.BatchedCallback<Object>(mCallback);
-
-    @Test
-    public void testAdd() throws Throwable {
-        mBatched.onInserted(0, 3);
-        assertPending(TYPE_ADD, 0, 3);
-        assertLast(TYPE_NONE, -1, -1);
-        mBatched.dispatchLastEvent();
-        assertLast(TYPE_ADD, 0, 3);
-        assertPending(TYPE_NONE, -1, -1);
-    }
-
-    @Test
-    public void testRemove() throws Throwable {
-        mBatched.onRemoved(0, 3);
-        assertPending(TYPE_REMOVE, 0, 3);
-        assertLast(TYPE_NONE, -1, -1);
-        mBatched.dispatchLastEvent();
-        assertLast(TYPE_REMOVE, 0, 3);
-        assertPending(TYPE_NONE, -1, -1);
-    }
-
-    @Test
-    public void testChange() throws Throwable {
-        mBatched.onChanged(0, 3);
-        assertPending(TYPE_CHANGE, 0, 3);
-        assertLast(TYPE_NONE, -1, -1);
-        mBatched.dispatchLastEvent();
-        assertLast(TYPE_CHANGE, 0, 3);
-        assertPending(TYPE_NONE, -1, -1);
-    }
-
-    @Test
-    public void testMove() throws Throwable {
-        mBatched.onMoved(0, 3);
-        assertLast(TYPE_MOVE, 0, 3);
-        assertPending(TYPE_NONE, -1, -1);
-    }
-
-    @Test
-    public void testBatchAdd1() throws Throwable {
-        mBatched.onInserted(3, 5);
-        mBatched.onInserted(3, 2);
-        assertLast(TYPE_NONE, -1, -1);
-        mBatched.dispatchLastEvent();
-        assertLast(TYPE_ADD, 3, 7);
-        assertPending(TYPE_NONE, -1, -1);
-    }
-
-    @Test
-    public void testBatchAdd2() throws Throwable {
-        mBatched.onInserted(3, 5);
-        mBatched.onInserted(1, 2);
-        assertLast(TYPE_ADD, 3, 5);
-        mBatched.dispatchLastEvent();
-        assertLast(TYPE_ADD, 1, 2);
-        assertPending(TYPE_NONE, -1, -1);
-    }
-
-    @Test
-    public void testBatchAdd3() throws Throwable {
-        mBatched.onInserted(3, 5);
-        mBatched.onInserted(8, 2);
-        assertLast(TYPE_NONE, -1, -1);
-        mBatched.dispatchLastEvent();
-        assertLast(TYPE_ADD, 3, 7);
-        assertPending(TYPE_NONE, -1, -1);
-    }
-
-    @Test
-    public void testBatchAdd4() throws Throwable {
-        mBatched.onInserted(3, 5);
-        mBatched.onInserted(9, 2);
-        assertLast(TYPE_ADD, 3, 5);
-        mBatched.dispatchLastEvent();
-        assertLast(TYPE_ADD, 9, 2);
-        assertPending(TYPE_NONE, -1, -1);
-    }
-
-    @Test
-    public void testBatchAdd5() throws Throwable {
-        mBatched.onInserted(3, 5);
-        mBatched.onInserted(4, 1);
-        assertLast(TYPE_NONE, -1, -1);
-        mBatched.dispatchLastEvent();
-        assertLast(TYPE_ADD, 3, 6);
-        assertPending(TYPE_NONE, -1, -1);
-    }
-
-    @Test
-    public void testBatchAdd6() throws Throwable {
-        mBatched.onInserted(3, 5);
-        mBatched.onInserted(4, 1);
-        assertLast(TYPE_NONE, -1, -1);
-        mBatched.onInserted(4, 1);
-        mBatched.dispatchLastEvent();
-        assertLast(TYPE_ADD, 3, 7);
-        assertPending(TYPE_NONE, -1, -1);
-    }
-
-    @Test
-    public void testBatchAddLoop() throws Throwable {
-        for (int i = 0; i < 10; i++) {
-            mBatched.onInserted(4 + i, 1);
-            assertLast(TYPE_NONE, -1, -1);
-            assertPending(TYPE_ADD, 4, i + 1);
-        }
-        mBatched.dispatchLastEvent();
-        assertLast(TYPE_ADD, 4, 10);
-    }
-
-    @Test
-    public void testBatchAddReverseLoop() throws Throwable {
-        for (int i = 10; i >= 0; i--) {
-            mBatched.onInserted(4, 1);
-            assertLast(TYPE_NONE, -1, -1);
-            assertPending(TYPE_ADD, 4, 10 - i + 1);
-        }
-        mBatched.dispatchLastEvent();
-        assertLast(TYPE_ADD, 4, 11);
-    }
-
-    @Test
-    public void testBadBatchAddReverseLoop() throws Throwable {
-        for (int i = 10; i >= 0; i--) {
-            mBatched.onInserted(4 + i, 1);
-            if (i < 10) {
-                assertLast(TYPE_ADD, 4 + i + 1, 1);
-            }
-
-        }
-        mBatched.dispatchLastEvent();
-        assertLast(TYPE_ADD, 4, 1);
-    }
-
-    @Test
-    public void testBatchRemove1() throws Throwable {
-        mBatched.onRemoved(3, 5);
-        mBatched.onRemoved(3, 1);
-        assertLast(TYPE_NONE, -1, -1);
-        mBatched.dispatchLastEvent();
-        assertLast(TYPE_REMOVE, 3, 6);
-        assertPending(TYPE_NONE, -1, -1);
-    }
-
-    @Test
-    public void testBatchRemove2() throws Throwable {
-        mBatched.onRemoved(3, 5);
-        mBatched.onRemoved(4, 1);
-        assertLast(TYPE_REMOVE, 3, 5);
-        mBatched.dispatchLastEvent();
-        assertLast(TYPE_REMOVE, 4, 1);
-        assertPending(TYPE_NONE, -1, -1);
-    }
-
-    @Test
-    public void testBatchRemove3() throws Throwable {
-        mBatched.onRemoved(3, 5);
-        mBatched.onRemoved(2, 3);
-        assertLast(TYPE_REMOVE, 3, 5);
-        mBatched.dispatchLastEvent();
-        assertLast(TYPE_REMOVE, 2, 3);
-        assertPending(TYPE_NONE, -1, -1);
-    }
-
-    @Test
-    public void testBatchChange1() throws Throwable {
-        mBatched.onChanged(3, 5);
-        mBatched.onChanged(3, 1);
-        assertPending(TYPE_CHANGE, 3, 5);
-        assertLast(TYPE_NONE, -1, -1);
-        mBatched.dispatchLastEvent();
-        assertLast(TYPE_CHANGE, 3, 5);
-        assertPending(TYPE_NONE, -1, -1);
-    }
-
-    @Test
-    public void testBatchChange2() throws Throwable {
-        mBatched.onChanged(3, 5);
-        mBatched.onChanged(2, 7);
-        assertPending(TYPE_CHANGE, 2, 7);
-        assertLast(TYPE_NONE, -1, -1);
-        mBatched.dispatchLastEvent();
-        assertLast(TYPE_CHANGE, 2, 7);
-        assertPending(TYPE_NONE, -1, -1);
-    }
-
-    @Test
-    public void testBatchChange3() throws Throwable {
-        mBatched.onChanged(3, 5);
-        mBatched.onChanged(2, 1);
-        assertLast(TYPE_NONE, -1, -1);
-        mBatched.onChanged(8, 2);
-        assertLast(TYPE_NONE, -1, -1);
-        assertPending(TYPE_CHANGE, 2, 8);
-    }
-
-    @Test
-    public void testBatchChange4() throws Throwable {
-        mBatched.onChanged(3, 5);
-        mBatched.onChanged(1, 1);
-        assertLast(TYPE_CHANGE, 3, 5);
-        assertPending(TYPE_CHANGE, 1, 1);
-    }
-
-    @Test
-    public void testBatchChange5() throws Throwable {
-        mBatched.onChanged(3, 5);
-        mBatched.onChanged(9, 1);
-        assertLast(TYPE_CHANGE, 3, 5);
-        assertPending(TYPE_CHANGE, 9, 1);
-    }
-
-    private void assertLast(int type, int position, int count) throws Throwable {
-        try {
-            assertEquals(lastReceivedType, type);
-            if (position >= 0) {
-                assertEquals(lastReceivedPosition, position);
-            }
-            if (count >= 0) {
-                assertEquals(lastReceivedCount, count);
-            }
-        } catch (Throwable t) {
-            throw new Throwable("last event: expected=" + log(type, position, count)
-                    + " found=" + log(lastReceivedType, lastReceivedPosition,
-                    lastReceivedCount), t);
-        }
-    }
-
-    private void assertPending(int type, int position, int count) throws Throwable {
-        try {
-            assertEquals(mBatched.mLastEventType, type);
-            if (position >= 0) {
-                assertEquals(mBatched.mLastEventPosition, position);
-            }
-            if (count >= 0) {
-                assertEquals(mBatched.mLastEventCount, count);
-            }
-        } catch (Throwable t) {
-            throw new Throwable("pending event: expected=" + log(type, position, count)
-                    + " found=" + log(mBatched.mLastEventType, mBatched.mLastEventPosition,
-                    mBatched.mLastEventCount), t);
-        }
-    }
-
-    private String log(int type, int position, int count) {
-        return TYPES_NAMES[type]
-                + ", p:" + position
-                + ", c:" + count;
-    }
-
-    private static final String[] TYPES_NAMES = new String[]{"none", "add", "remove", "change",
-            "move"};
-}
diff --git a/v7/recyclerview/src/android/support/v7/util/BatchingListUpdateCallback.java b/v7/recyclerview/src/android/support/v7/util/BatchingListUpdateCallback.java
new file mode 100644
index 0000000..c8bc1a4
--- /dev/null
+++ b/v7/recyclerview/src/android/support/v7/util/BatchingListUpdateCallback.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v7.util;
+
+/**
+ * Wraps a {@link ListUpdateCallback} callback and batches operations that can be merged.
+ * <p>
+ * For instance, when 2 add operations comes that adds 2 consecutive elements,
+ * BatchingListUpdateCallback merges them and calls the wrapped callback only once.
+ * <p>
+ * This is a general purpose class and is also used by
+ * {@link android.support.v7.util.DiffUtil.DiffResult DiffResult} and
+ * {@link SortedList} to minimize the number of updates that are dispatched.
+ * <p>
+ * If you use this class to batch updates, you must call {@link #dispatchLastEvent()} when the
+ * stream of update events drain.
+ */
+public class BatchingListUpdateCallback implements ListUpdateCallback {
+    private static final int TYPE_NONE = 0;
+    private static final int TYPE_ADD = 1;
+    private static final int TYPE_REMOVE = 2;
+    private static final int TYPE_CHANGE = 3;
+
+    final ListUpdateCallback mWrapped;
+
+    int mLastEventType = TYPE_NONE;
+    int mLastEventPosition = -1;
+    int mLastEventCount = -1;
+    Object mLastEventPayload = null;
+
+    public BatchingListUpdateCallback(ListUpdateCallback callback) {
+        mWrapped = callback;
+    }
+
+    /**
+     * BatchingListUpdateCallback holds onto the last event to see if it can be merged with the
+     * next one. When stream of events finish, you should call this method to dispatch the last
+     * event.
+     */
+    public void dispatchLastEvent() {
+        if (mLastEventType == TYPE_NONE) {
+            return;
+        }
+        switch (mLastEventType) {
+            case TYPE_ADD:
+                mWrapped.onInserted(mLastEventPosition, mLastEventCount);
+                break;
+            case TYPE_REMOVE:
+                mWrapped.onRemoved(mLastEventPosition, mLastEventCount);
+                break;
+            case TYPE_CHANGE:
+                mWrapped.onChanged(mLastEventPosition, mLastEventCount, mLastEventPayload);
+                break;
+        }
+        mLastEventPayload = null;
+        mLastEventType = TYPE_NONE;
+    }
+
+    @Override
+    public void onInserted(int position, int count) {
+        if (mLastEventType == TYPE_ADD && position >= mLastEventPosition
+                && position <= mLastEventPosition + mLastEventCount) {
+            mLastEventCount += count;
+            mLastEventPosition = Math.min(position, mLastEventPosition);
+            return;
+        }
+        dispatchLastEvent();
+        mLastEventPosition = position;
+        mLastEventCount = count;
+        mLastEventType = TYPE_ADD;
+    }
+
+    @Override
+    public void onRemoved(int position, int count) {
+        if (mLastEventType == TYPE_REMOVE && mLastEventPosition >= position &&
+                mLastEventPosition <= position + count) {
+            mLastEventCount += count;
+            mLastEventPosition = position;
+            return;
+        }
+        dispatchLastEvent();
+        mLastEventPosition = position;
+        mLastEventCount = count;
+        mLastEventType = TYPE_REMOVE;
+    }
+
+    @Override
+    public void onMoved(int fromPosition, int toPosition) {
+        dispatchLastEvent(); // moves are not merged
+        mWrapped.onMoved(fromPosition, toPosition);
+    }
+
+    @Override
+    public void onChanged(int position, int count, Object payload) {
+        if (mLastEventType == TYPE_CHANGE &&
+                !(position > mLastEventPosition + mLastEventCount
+                        || position + count < mLastEventPosition || mLastEventPayload != payload)) {
+            // take potential overlap into account
+            int previousEnd = mLastEventPosition + mLastEventCount;
+            mLastEventPosition = Math.min(position, mLastEventPosition);
+            mLastEventCount = Math.max(previousEnd, position + count) - mLastEventPosition;
+            return;
+        }
+        dispatchLastEvent();
+        mLastEventPosition = position;
+        mLastEventCount = count;
+        mLastEventPayload = payload;
+        mLastEventType = TYPE_CHANGE;
+    }
+}
diff --git a/v7/recyclerview/src/android/support/v7/util/DiffUtil.java b/v7/recyclerview/src/android/support/v7/util/DiffUtil.java
new file mode 100644
index 0000000..6f0a078
--- /dev/null
+++ b/v7/recyclerview/src/android/support/v7/util/DiffUtil.java
@@ -0,0 +1,856 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v7.util;
+
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.v7.widget.RecyclerView;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * DiffUtil is a utility class that can calculate the difference between two lists and output a
+ * list of update operations that converts the first list into the second one.
+ * <p>
+ * It can be used to calculate updates for a RecyclerView Adapter.
+ * <p>
+ * DiffUtil uses Eugene W. Myers's difference algorithm to calculate the minimal number of updates
+ * to convert one list into another. Myers's algorithm does not handle items that are moved so
+ * DiffUtil runs a second pass on the result to detect items that were moved.
+ * <p>
+ * If the lists are large, this operation may take significant time so you are advised to run this
+ * on a background thread, get the {@link DiffResult} then apply it on the RecyclerView on the main
+ * thread.
+ * <p>
+ * This algorithm is optimized for space and uses O(N) space to find the minimal
+ * number of addition and removal operations between the two lists. It has O(N + D^2) expected time
+ * performance where D is the length of the edit script.
+ * <p>
+ * If move detection is enabled, it takes an additional O(N^2) time where N is the total number of
+ * added and removed items. If your lists are already sorted by the same constraint (e.g. a created
+ * timestamp for a list of posts), you can disable move detection to improve performance.
+ * <p>
+ * The actual runtime of the algorithm significantly depends on the number of changes in the list
+ * and the cost of your comparison methods. Below are some average run times for reference:
+ * (The test list is composed of random UUID Strings and the tests are run on Nexus 5X with M)
+ * <ul>
+ *     <li>100 items and 10 modifications: avg: 0.39 ms, median: 0.35 ms
+ *     <li>100 items and 100 modifications: 3.82 ms, median: 3.75 ms
+ *     <li>100 items and 100 modifications without moves: 2.09 ms, median: 2.06 ms
+ *     <li>1000 items and 50 modifications: avg: 4.67 ms, median: 4.59 ms
+ *     <li>1000 items and 50 modifications without moves: avg: 3.59 ms, median: 3.50 ms
+ *     <li>1000 items and 200 modifications: 27.07 ms, median: 26.92 ms
+ *     <li>1000 items and 200 modifications without moves: 13.54 ms, median: 13.36 ms
+ * </ul>
+ * <p>
+ * Due to implementation constraints, the max size of the list can be 2^26.
+ */
+public class DiffUtil {
+
+    private DiffUtil() {
+        // utility class, no instance.
+    }
+
+    private static final Comparator<Snake> SNAKE_COMPARATOR = new Comparator<Snake>() {
+        @Override
+        public int compare(Snake o1, Snake o2) {
+            int cmpX = o1.x - o2.x;
+            return cmpX == 0 ? o1.y - o2.y : cmpX;
+        }
+    };
+
+    // Myers' algorithm uses two lists as axis labels. In DiffUtil's implementation, `x` axis is
+    // used for old list and `y` axis is used for new list.
+
+    /**
+     * Calculates the list of update operations that can covert one list into the other one.
+     *
+     * @param cb The callback that acts as a gateway to the backing list data
+     *
+     * @return A DiffResult that contains the information about the edit sequence to convert the
+     * old list into the new list.
+     */
+    public static DiffResult calculateDiff(Callback cb) {
+        return calculateDiff(cb, true);
+    }
+
+    /**
+     * Calculates the list of update operations that can covert one list into the other one.
+     * <p>
+     * If your old and new lists are sorted by the same constraint and items never move (swap
+     * positions), you can disable move detection which takes <code>O(N^2)</code> time where
+     * N is the number of added, moved, removed items.
+     *
+     * @param cb The callback that acts as a gateway to the backing list data
+     * @param detectMoves True if DiffUtil should try to detect moved items, false otherwise.
+     *
+     * @return A DiffResult that contains the information about the edit sequence to convert the
+     * old list into the new list.
+     */
+    public static DiffResult calculateDiff(Callback cb, boolean detectMoves) {
+        final int oldSize = cb.getOldListSize();
+        final int newSize = cb.getNewListSize();
+
+        final List<Snake> snakes = new ArrayList<>();
+
+        // instead of a recursive implementation, we keep our own stack to avoid potential stack
+        // overflow exceptions
+        final List<Range> stack = new ArrayList<>();
+
+        stack.add(new Range(0, oldSize, 0, newSize));
+
+        final int max = oldSize + newSize + Math.abs(oldSize - newSize);
+        // allocate forward and backward k-lines. K lines are diagonal lines in the matrix. (see the
+        // paper for details)
+        // These arrays lines keep the max reachable position for each k-line.
+        final int[] forward = new int[max * 2];
+        final int[] backward = new int[max * 2];
+
+        // We pool the ranges to avoid allocations for each recursive call.
+        final List<Range> rangePool = new ArrayList<>();
+        while (!stack.isEmpty()) {
+            final Range range = stack.remove(stack.size() - 1);
+            final Snake snake = diffPartial(cb, range.oldListStart, range.oldListEnd,
+                    range.newListStart, range.newListEnd, forward, backward, max);
+            if (snake != null) {
+                if (snake.size > 0) {
+                    snakes.add(snake);
+                }
+                // offset the snake to convert its coordinates from the Range's area to global
+                snake.x += range.oldListStart;
+                snake.y += range.newListStart;
+
+                // add new ranges for left and right
+                final Range left = rangePool.isEmpty() ? new Range() : rangePool.remove(
+                        rangePool.size() - 1);
+                left.oldListStart = range.oldListStart;
+                left.newListStart = range.newListStart;
+                if (snake.reverse) {
+                    left.oldListEnd = snake.x;
+                    left.newListEnd = snake.y;
+                } else {
+                    if (snake.removal) {
+                        left.oldListEnd = snake.x - 1;
+                        left.newListEnd = snake.y;
+                    } else {
+                        left.oldListEnd = snake.x;
+                        left.newListEnd = snake.y - 1;
+                    }
+                }
+                stack.add(left);
+
+                // re-use range for right
+                //noinspection UnnecessaryLocalVariable
+                final Range right = range;
+                if (snake.reverse) {
+                    if (snake.removal) {
+                        right.oldListStart = snake.x + snake.size + 1;
+                        right.newListStart = snake.y + snake.size;
+                    } else {
+                        right.oldListStart = snake.x + snake.size;
+                        right.newListStart = snake.y + snake.size + 1;
+                    }
+                } else {
+                    right.oldListStart = snake.x + snake.size;
+                    right.newListStart = snake.y + snake.size;
+                }
+                stack.add(right);
+            } else {
+                rangePool.add(range);
+            }
+
+        }
+        // sort snakes
+        Collections.sort(snakes, SNAKE_COMPARATOR);
+
+        return new DiffResult(cb, snakes, forward, backward, detectMoves);
+
+    }
+
+    private static Snake diffPartial(Callback cb, int startOld, int endOld,
+            int startNew, int endNew, int[] forward, int[] backward, int kOffset) {
+        final int oldSize = endOld - startOld;
+        final int newSize = endNew - startNew;
+
+        if (endOld - startOld < 1 || endNew - startNew < 1) {
+            return null;
+        }
+
+        final int delta = oldSize - newSize;
+        final int dLimit = (oldSize + newSize + 1) / 2;
+        Arrays.fill(forward, kOffset - dLimit - 1, kOffset + dLimit + 1, 0);
+        Arrays.fill(backward, kOffset - dLimit - 1 + delta, kOffset + dLimit + 1 + delta, oldSize);
+        final boolean checkInFwd = delta % 2 != 0;
+        for (int d = 0; d <= dLimit; d++) {
+            for (int k = -d; k <= d; k += 2) {
+                // find forward path
+                // we can reach k from k - 1 or k + 1. Check which one is further in the graph
+                int x;
+                final boolean removal;
+                if (k == -d || k != d && forward[kOffset + k - 1] < forward[kOffset + k + 1]) {
+                    x = forward[kOffset + k + 1];
+                    removal = false;
+                } else {
+                    x = forward[kOffset + k - 1] + 1;
+                    removal = true;
+                }
+                // set y based on x
+                int y = x - k;
+                // move diagonal as long as items match
+                while (x < oldSize && y < newSize
+                        && cb.areItemsTheSame(startOld + x, startNew + y)) {
+                    x++;
+                    y++;
+                }
+                forward[kOffset + k] = x;
+                if (checkInFwd && k >= delta - d + 1 && k <= delta + d - 1) {
+                    if (forward[kOffset + k] >= backward[kOffset + k]) {
+                        Snake outSnake = new Snake();
+                        outSnake.x = backward[kOffset + k];
+                        outSnake.y = outSnake.x - k;
+                        outSnake.size = forward[kOffset + k] - backward[kOffset + k];
+                        outSnake.removal = removal;
+                        outSnake.reverse = false;
+                        return outSnake;
+                    }
+                }
+            }
+            for (int k = -d; k <= d; k += 2) {
+                // find reverse path at k + delta, in reverse
+                final int backwardK = k + delta;
+                int x;
+                final boolean removal;
+                if (backwardK == d + delta || backwardK != -d + delta
+                        && backward[kOffset + backwardK - 1] < backward[kOffset + backwardK + 1]) {
+                    x = backward[kOffset + backwardK - 1];
+                    removal = false;
+                } else {
+                    x = backward[kOffset + backwardK + 1] - 1;
+                    removal = true;
+                }
+
+                // set y based on x
+                int y = x - backwardK;
+                // move diagonal as long as items match
+                while (x > 0 && y > 0
+                        && cb.areItemsTheSame(startOld + x - 1, startNew + y - 1)) {
+                    x--;
+                    y--;
+                }
+                backward[kOffset + backwardK] = x;
+                if (!checkInFwd && k + delta >= -d && k + delta <= d) {
+                    if (forward[kOffset + backwardK] >= backward[kOffset + backwardK]) {
+                        Snake outSnake = new Snake();
+                        outSnake.x = backward[kOffset + backwardK];
+                        outSnake.y = outSnake.x - backwardK;
+                        outSnake.size =
+                                forward[kOffset + backwardK] - backward[kOffset + backwardK];
+                        outSnake.removal = removal;
+                        outSnake.reverse = true;
+                        return outSnake;
+                    }
+                }
+            }
+        }
+        throw new IllegalStateException("DiffUtil hit an unexpected case while trying to calculate"
+                + " the optimal path. Please make sure your data is not changing during the"
+                + " diff calculation.");
+    }
+
+    /**
+     * A Callback class used by DiffUtil while calculating the diff between two lists.
+     */
+    public abstract static class Callback {
+        /**
+         * Returns the size of the old list.
+         *
+         * @return The size of the old list.
+         */
+        public abstract int getOldListSize();
+
+        /**
+         * Returns the size of the new list.
+         *
+         * @return The size of the new list.
+         */
+        public abstract int getNewListSize();
+
+        /**
+         * Called by the DiffUtil to decide whether two object represent the same Item.
+         * <p>
+         * For example, if your items have unique ids, this method should check their id equality.
+         *
+         * @param oldItemPosition The position of the item in the old list
+         * @param newItemPosition The position of the item in the new list
+         * @return True if the two items represent the same object or false if they are different.
+         */
+        public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition);
+
+        /**
+         * Called by the DiffUtil when it wants to check whether two items have the same data.
+         * DiffUtil uses this information to detect if the contents of an item has changed.
+         * <p>
+         * DiffUtil uses this method to check equality instead of {@link Object#equals(Object)}
+         * so that you can change its behavior depending on your UI.
+         * For example, if you are using DiffUtil with a
+         * {@link android.support.v7.widget.RecyclerView.Adapter RecyclerView.Adapter}, you should
+         * return whether the items' visual representations are the same.
+         * <p>
+         * This method is called only if {@link #areItemsTheSame(int, int)} returns
+         * {@code true} for these items.
+         *
+         * @param oldItemPosition The position of the item in the old list
+         * @param newItemPosition The position of the item in the new list which replaces the
+         *                        oldItem
+         * @return True if the contents of the items are the same or false if they are different.
+         */
+        public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition);
+
+        /**
+         * When {@link #areItemsTheSame(int, int)} returns {@code true} for two items and
+         * {@link #areContentsTheSame(int, int)} returns false for them, DiffUtil
+         * calls this method to get a payload about the change.
+         * <p>
+         * For example, if you are using DiffUtil with {@link RecyclerView}, you can return the
+         * particular field that changed in the item and your
+         * {@link android.support.v7.widget.RecyclerView.ItemAnimator ItemAnimator} can use that
+         * information to run the correct animation.
+         * <p>
+         * Default implementation returns {@code null}.
+         *
+         * @param oldItemPosition The position of the item in the old list
+         * @param newItemPosition The position of the item in the new list
+         *
+         * @return A payload object that represents the change between the two items.
+         */
+        @Nullable
+        public Object getChangePayload(int oldItemPosition, int newItemPosition) {
+            return null;
+        }
+    }
+
+    /**
+     * Snakes represent a match between two lists. It is optionally prefixed or postfixed with an
+     * add or remove operation. See the Myers' paper for details.
+     */
+    static class Snake {
+        /**
+         * Position in the old list
+         */
+        int x;
+
+        /**
+         * Position in the new list
+         */
+        int y;
+
+        /**
+         * Number of matches. Might be 0.
+         */
+        int size;
+
+        /**
+         * If true, this is a removal from the original list followed by {@code size} matches.
+         * If false, this is an addition from the new list followed by {@code size} matches.
+         */
+        boolean removal;
+
+        /**
+         * If true, the addition or removal is at the end of the snake.
+         * If false, the addition or removal is at the beginning of the snake.
+         */
+        boolean reverse;
+    }
+
+    /**
+     * Represents a range in two lists that needs to be solved.
+     * <p>
+     * This internal class is used when running Myers' algorithm without recursion.
+     */
+    static class Range {
+
+        int oldListStart, oldListEnd;
+
+        int newListStart, newListEnd;
+
+        public Range() {
+        }
+
+        public Range(int oldListStart, int oldListEnd, int newListStart, int newListEnd) {
+            this.oldListStart = oldListStart;
+            this.oldListEnd = oldListEnd;
+            this.newListStart = newListStart;
+            this.newListEnd = newListEnd;
+        }
+    }
+
+    /**
+     * This class holds the information about the result of a
+     * {@link DiffUtil#calculateDiff(Callback, boolean)} call.
+     * <p>
+     * You can consume the updates in a DiffResult via
+     * {@link #dispatchUpdatesTo(ListUpdateCallback)} or directly stream the results into a
+     * {@link RecyclerView.Adapter} via {@link #dispatchUpdatesTo(RecyclerView.Adapter)}.
+     */
+    public static class DiffResult {
+        /**
+         * While reading the flags below, keep in mind that when multiple items move in a list,
+         * Myers's may pick any of them as the anchor item and consider that one NOT_CHANGED while
+         * picking others as additions and removals. This is completely fine as we later detect
+         * all moves.
+         * <p>
+         * Below, when an item is mentioned to stay in the same "location", it means we won't
+         * dispatch a move/add/remove for it, it DOES NOT mean the item is still in the same
+         * position.
+         */
+        // item stayed the same.
+        private static final int FLAG_NOT_CHANGED = 1;
+        // item stayed in the same location but changed.
+        private static final int FLAG_CHANGED = FLAG_NOT_CHANGED << 1;
+        // Item has moved and also changed.
+        private static final int FLAG_MOVED_CHANGED = FLAG_CHANGED << 1;
+        // Item has moved but did not change.
+        private static final int FLAG_MOVED_NOT_CHANGED = FLAG_MOVED_CHANGED << 1;
+        // Ignore this update.
+        // If this is an addition from the new list, it means the item is actually removed from an
+        // earlier position and its move will be dispatched when we process the matching removal
+        // from the old list.
+        // If this is a removal from the old list, it means the item is actually added back to an
+        // earlier index in the new list and we'll dispatch its move when we are processing that
+        // addition.
+        private static final int FLAG_IGNORE = FLAG_MOVED_NOT_CHANGED << 1;
+
+        // since we are re-using the int arrays that were created in the Myers' step, we mask
+        // change flags
+        private static final int FLAG_OFFSET = 5;
+
+        private static final int FLAG_MASK = (1 << FLAG_OFFSET) - 1;
+
+        // The Myers' snakes. At this point, we only care about their diagonal sections.
+        private final List<Snake> mSnakes;
+
+        // The list to keep oldItemStatuses. As we traverse old items, we assign flags to them
+        // which also includes whether they were a real removal or a move (and its new index).
+        private final int[] mOldItemStatuses;
+        // The list to keep newItemStatuses. As we traverse new items, we assign flags to them
+        // which also includes whether they were a real addition or a move(and its old index).
+        private final int[] mNewItemStatuses;
+        // The callback that was given to calcualte diff method.
+        private final Callback mCallback;
+
+        private final int mOldListSize;
+
+        private final int mNewListSize;
+
+        private final boolean mDetectMoves;
+
+        /**
+         * @param callback The callback that was used to calculate the diff
+         * @param snakes The list of Myers' snakes
+         * @param oldItemStatuses An int[] that can be re-purposed to keep metadata
+         * @param newItemStatuses An int[] that can be re-purposed to keep metadata
+         * @param detectMoves True if this DiffResult will try to detect moved items
+         */
+        DiffResult(Callback callback, List<Snake> snakes, int[] oldItemStatuses,
+                int[] newItemStatuses, boolean detectMoves) {
+            mSnakes = snakes;
+            mOldItemStatuses = oldItemStatuses;
+            mNewItemStatuses = newItemStatuses;
+            Arrays.fill(mOldItemStatuses, 0);
+            Arrays.fill(mNewItemStatuses, 0);
+            mCallback = callback;
+            mOldListSize = callback.getOldListSize();
+            mNewListSize = callback.getNewListSize();
+            mDetectMoves = detectMoves;
+            addRootSnake();
+            findMatchingItems();
+        }
+
+        /**
+         * We always add a Snake to 0/0 so that we can run loops from end to beginning and be done
+         * when we run out of snakes.
+         */
+        private void addRootSnake() {
+            Snake firstSnake = mSnakes.isEmpty() ? null : mSnakes.get(0);
+            if (firstSnake == null || firstSnake.x != 0 || firstSnake.y != 0) {
+                Snake root = new Snake();
+                root.x = 0;
+                root.y = 0;
+                root.removal = false;
+                root.size = 0;
+                root.reverse = false;
+                mSnakes.add(0, root);
+            }
+        }
+
+        /**
+         * This method traverses each addition / removal and tries to match it to a previous
+         * removal / addition. This is how we detect move operations.
+         * <p>
+         * This class also flags whether an item has been changed or not.
+         * <p>
+         * DiffUtil does this pre-processing so that if it is running on a big list, it can be moved
+         * to background thread where most of the expensive stuff will be calculated and kept in
+         * the statuses maps. DiffResult uses this pre-calculated information while dispatching
+         * the updates (which is probably being called on the main thread).
+         */
+        private void findMatchingItems() {
+            int posOld = mOldListSize;
+            int posNew = mNewListSize;
+            // traverse the matrix from right bottom to 0,0.
+            for (int i = mSnakes.size() - 1; i >= 0; i--) {
+                final Snake snake = mSnakes.get(i);
+                final int endX = snake.x + snake.size;
+                final int endY = snake.y + snake.size;
+                if (mDetectMoves) {
+                    while (posOld > endX) {
+                        // this is a removal. Check remaining snakes to see if this was added before
+                        findAddition(posOld, posNew, i);
+                        posOld--;
+                    }
+                    while (posNew > endY) {
+                        // this is an addition. Check remaining snakes to see if this was removed
+                        // before
+                        findRemoval(posOld, posNew, i);
+                        posNew--;
+                    }
+                }
+                for (int j = 0; j < snake.size; j++) {
+                    // matching items. Check if it is changed or not
+                    final int oldItemPos = snake.x + j;
+                    final int newItemPos = snake.y + j;
+                    final boolean theSame = mCallback
+                            .areContentsTheSame(oldItemPos, newItemPos);
+                    final int changeFlag = theSame ? FLAG_NOT_CHANGED : FLAG_CHANGED;
+                    mOldItemStatuses[oldItemPos] = (newItemPos << FLAG_OFFSET) | changeFlag;
+                    mNewItemStatuses[newItemPos] = (oldItemPos << FLAG_OFFSET) | changeFlag;
+                }
+                posOld = snake.x;
+                posNew = snake.y;
+            }
+        }
+
+        private void findAddition(int x, int y, int snakeIndex) {
+            if (mOldItemStatuses[x - 1] != 0) {
+                return; // already set by a latter item
+            }
+            findMatchingItem(x, y, snakeIndex, false);
+        }
+
+        private void findRemoval(int x, int y, int snakeIndex) {
+            if (mNewItemStatuses[y - 1] != 0) {
+                return; // already set by a latter item
+            }
+            findMatchingItem(x, y, snakeIndex, true);
+        }
+
+        /**
+         * Finds a matching item that is before the given coordinates in the matrix
+         * (before : left and above).
+         *
+         * @param x The x position in the matrix (position in the old list)
+         * @param y The y position in the matrix (position in the new list)
+         * @param snakeIndex The current snake index
+         * @param removal True if we are looking for a removal, false otherwise
+         *
+         * @return True if such item is found.
+         */
+        private boolean findMatchingItem(final int x, final int y, final int snakeIndex,
+                final boolean removal) {
+            final int myItemPos;
+            int curX;
+            int curY;
+            if (removal) {
+                myItemPos = y - 1;
+                curX = x;
+                curY = y - 1;
+            } else {
+                myItemPos = x - 1;
+                curX = x - 1;
+                curY = y;
+            }
+            for (int i = snakeIndex; i >= 0; i--) {
+                final Snake snake = mSnakes.get(i);
+                final int endX = snake.x + snake.size;
+                final int endY = snake.y + snake.size;
+                if (removal) {
+                    // check removals for a match
+                    for (int pos = curX - 1; pos >= endX; pos--) {
+                        if (mCallback.areItemsTheSame(pos, myItemPos)) {
+                            // found!
+                            final boolean theSame = mCallback.areContentsTheSame(pos, myItemPos);
+                            final int changeFlag = theSame ? FLAG_MOVED_NOT_CHANGED
+                                    : FLAG_MOVED_CHANGED;
+                            mNewItemStatuses[myItemPos] = (pos << FLAG_OFFSET) | FLAG_IGNORE;
+                            mOldItemStatuses[pos] = (myItemPos << FLAG_OFFSET) | changeFlag;
+                            return true;
+                        }
+                    }
+                } else {
+                    // check for additions for a match
+                    for (int pos = curY - 1; pos >= endY; pos--) {
+                        if (mCallback.areItemsTheSame(myItemPos, pos)) {
+                            // found
+                            final boolean theSame = mCallback.areContentsTheSame(myItemPos, pos);
+                            final int changeFlag = theSame ? FLAG_MOVED_NOT_CHANGED
+                                    : FLAG_MOVED_CHANGED;
+                            mOldItemStatuses[x - 1] = (pos << FLAG_OFFSET) | FLAG_IGNORE;
+                            mNewItemStatuses[pos] = ((x - 1) << FLAG_OFFSET) | changeFlag;
+                            return true;
+                        }
+                    }
+                }
+                curX = snake.x;
+                curY = snake.y;
+            }
+            return false;
+        }
+
+        /**
+         * Dispatches the update events to the given adapter.
+         * <p>
+         * For example, if you have an {@link android.support.v7.widget.RecyclerView.Adapter Adapter}
+         * that is backed by a {@link List}, you can swap the list with the new one then call this
+         * method to dispatch all updates to the RecyclerView.
+         * <pre>
+         *     List oldList = mAdapter.getData();
+         *     DiffResult result = DiffUtil.calculateDiff(new MyCallback(oldList, newList));
+         *     mAdapter.setData(newList);
+         *     result.dispatchUpdatesTo(mAdapter);
+         * </pre>
+         * <p>
+         * Note that the RecyclerView requires you to dispatch adapter updates immediately when you
+         * change the data (you cannot defer {@code notify*} calls). The usage above adheres to this
+         * rule because updates are sent to the adapter right after the backing data is changed,
+         * before RecyclerView tries to read it.
+         * <p>
+         * On the other hand, if you have another
+         * {@link android.support.v7.widget.RecyclerView.AdapterDataObserver AdapterDataObserver}
+         * that tries to process events synchronously, this may confuse that observer because the
+         * list is instantly moved to its final state while the adapter updates are dispatched later
+         * on, one by one. If you have such an
+         * {@link android.support.v7.widget.RecyclerView.AdapterDataObserver AdapterDataObserver},
+         * you can use
+         * {@link #dispatchUpdatesTo(ListUpdateCallback)} to handle each modification
+         * manually.
+         *
+         * @param adapter A RecyclerView adapter which was displaying the old list and will start
+         *                displaying the new list.
+         */
+        public void dispatchUpdatesTo(final RecyclerView.Adapter adapter) {
+            dispatchUpdatesTo(new ListUpdateCallback() {
+                @Override
+                public void onInserted(int position, int count) {
+                    adapter.notifyItemRangeInserted(position, count);
+                }
+
+                @Override
+                public void onRemoved(int position, int count) {
+                    adapter.notifyItemRangeRemoved(position, count);
+                }
+
+                @Override
+                public void onMoved(int fromPosition, int toPosition) {
+                    adapter.notifyItemMoved(fromPosition, toPosition);
+                }
+
+                @Override
+                public void onChanged(int position, int count, Object payload) {
+                    adapter.notifyItemRangeChanged(position, count, payload);
+                }
+            });
+        }
+
+        /**
+         * Dispatches update operations to the given Callback.
+         * <p>
+         * These updates are atomic such that the first update call effects every update call that
+         * comes after it (the same as RecyclerView).
+         *
+         * @param updateCallback The callback to receive the update operations.
+         * @see #dispatchUpdatesTo(RecyclerView.Adapter)
+         */
+        public void dispatchUpdatesTo(ListUpdateCallback updateCallback) {
+            final BatchingListUpdateCallback batchingCallback;
+            if (updateCallback instanceof BatchingListUpdateCallback) {
+                batchingCallback = (BatchingListUpdateCallback) updateCallback;
+            } else {
+                batchingCallback = new BatchingListUpdateCallback(updateCallback);
+                // replace updateCallback with a batching callback and override references to
+                // updateCallback so that we don't call it directly by mistake
+                //noinspection UnusedAssignment
+                updateCallback = batchingCallback;
+            }
+            // These are add/remove ops that are converted to moves. We track their positions until
+            // their respective update operations are processed.
+            final List<PostponedUpdate> postponedUpdates = new ArrayList<>();
+            int posOld = mOldListSize;
+            int posNew = mNewListSize;
+            for (int snakeIndex = mSnakes.size() - 1; snakeIndex >= 0; snakeIndex--) {
+                final Snake snake = mSnakes.get(snakeIndex);
+                final int snakeSize = snake.size;
+                final int endX = snake.x + snakeSize;
+                final int endY = snake.y + snakeSize;
+                if (endX < posOld) {
+                    dispatchRemovals(postponedUpdates, batchingCallback, endX, posOld - endX, endX);
+                }
+
+                if (endY < posNew) {
+                    dispatchAdditions(postponedUpdates, batchingCallback, endX, posNew - endY,
+                            endY);
+                }
+                for (int i = snakeSize - 1; i >= 0; i--) {
+                    if ((mOldItemStatuses[snake.x + i] & FLAG_MASK) == FLAG_CHANGED) {
+                        batchingCallback.onChanged(snake.x + i, 1,
+                                mCallback.getChangePayload(snake.x + i, snake.y + i));
+                    }
+                }
+                posOld = snake.x;
+                posNew = snake.y;
+            }
+            batchingCallback.dispatchLastEvent();
+        }
+
+        private static PostponedUpdate removePostponedUpdate(List<PostponedUpdate> updates,
+                int pos, boolean removal) {
+            for (int i = updates.size() - 1; i >= 0; i--) {
+                final PostponedUpdate update = updates.get(i);
+                if (update.posInOwnerList == pos && update.removal == removal) {
+                    updates.remove(i);
+                    for (int j = i; j < updates.size(); j++) {
+                        // offset other ops since they swapped positions
+                        updates.get(j).currentPos += removal ? 1 : -1;
+                    }
+                    return update;
+                }
+            }
+            return null;
+        }
+
+        private void dispatchAdditions(List<PostponedUpdate> postponedUpdates,
+                ListUpdateCallback updateCallback, int start, int count, int globalIndex) {
+            if (!mDetectMoves) {
+                updateCallback.onInserted(start, count);
+                return;
+            }
+            for (int i = count - 1; i >= 0; i--) {
+                int status = mNewItemStatuses[globalIndex + i] & FLAG_MASK;
+                switch (status) {
+                    case 0: // real addition
+                        updateCallback.onInserted(start, 1);
+                        for (PostponedUpdate update : postponedUpdates) {
+                            update.currentPos += 1;
+                        }
+                        break;
+                    case FLAG_MOVED_CHANGED:
+                    case FLAG_MOVED_NOT_CHANGED:
+                        final int pos = mNewItemStatuses[globalIndex + i] >> FLAG_OFFSET;
+                        final PostponedUpdate update = removePostponedUpdate(postponedUpdates, pos,
+                                true);
+                        // the item was moved from that position
+                        //noinspection ConstantConditions
+                        updateCallback.onMoved(update.currentPos, start);
+                        if (status == FLAG_MOVED_CHANGED) {
+                            // also dispatch a change
+                            updateCallback.onChanged(start, 1,
+                                    mCallback.getChangePayload(pos, globalIndex + i));
+                        }
+                        break;
+                    case FLAG_IGNORE: // ignoring this
+                        postponedUpdates.add(new PostponedUpdate(globalIndex + i, start, false));
+                        break;
+                    default:
+                        throw new IllegalStateException(
+                                "unknown flag for pos " + (globalIndex + i) + " " + Long
+                                        .toBinaryString(status));
+                }
+            }
+        }
+
+        private void dispatchRemovals(List<PostponedUpdate> postponedUpdates,
+                ListUpdateCallback updateCallback, int start, int count, int globalIndex) {
+            if (!mDetectMoves) {
+                updateCallback.onRemoved(start, count);
+                return;
+            }
+            for (int i = count - 1; i >= 0; i--) {
+                final int status = mOldItemStatuses[globalIndex + i] & FLAG_MASK;
+                switch (status) {
+                    case 0: // real removal
+                        updateCallback.onRemoved(start + i, 1);
+                        for (PostponedUpdate update : postponedUpdates) {
+                            update.currentPos -= 1;
+                        }
+                        break;
+                    case FLAG_MOVED_CHANGED:
+                    case FLAG_MOVED_NOT_CHANGED:
+                        final int pos = mOldItemStatuses[globalIndex + i] >> FLAG_OFFSET;
+                        final PostponedUpdate update = removePostponedUpdate(postponedUpdates, pos,
+                                false);
+                        // the item was moved to that position. we do -1 because this is a move not
+                        // add and removing current item offsets the target move by 1
+                        //noinspection ConstantConditions
+                        updateCallback.onMoved(start + i, update.currentPos - 1);
+                        if (status == FLAG_MOVED_CHANGED) {
+                            // also dispatch a change
+                            updateCallback.onChanged(update.currentPos - 1, 1,
+                                    mCallback.getChangePayload(globalIndex + i, pos));
+                        }
+                        break;
+                    case FLAG_IGNORE: // ignoring this
+                        postponedUpdates.add(new PostponedUpdate(globalIndex + i, start + i, true));
+                        break;
+                    default:
+                        throw new IllegalStateException(
+                                "unknown flag for pos " + (globalIndex + i) + " " + Long
+                                        .toBinaryString(status));
+                }
+            }
+        }
+
+        @VisibleForTesting
+        List<Snake> getSnakes() {
+            return mSnakes;
+        }
+    }
+
+    /**
+     * Represents an update that we skipped because it was a move.
+     * <p>
+     * When an update is skipped, it is tracked as other updates are dispatched until the matching
+     * add/remove operation is found at which point the tracked position is used to dispatch the
+     * update.
+     */
+    private static class PostponedUpdate {
+
+        int posInOwnerList;
+
+        int currentPos;
+
+        boolean removal;
+
+        public PostponedUpdate(int posInOwnerList, int currentPos, boolean removal) {
+            this.posInOwnerList = posInOwnerList;
+            this.currentPos = currentPos;
+            this.removal = removal;
+        }
+    }
+}
diff --git a/v7/recyclerview/src/android/support/v7/util/ListUpdateCallback.java b/v7/recyclerview/src/android/support/v7/util/ListUpdateCallback.java
new file mode 100644
index 0000000..2136202
--- /dev/null
+++ b/v7/recyclerview/src/android/support/v7/util/ListUpdateCallback.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v7.util;
+
+/**
+ * An interface that can receive Update operations that are applied to a list.
+ * <p>
+ * This class can be used together with DiffUtil to detect changes between two lists.
+ */
+public interface ListUpdateCallback {
+    /**
+     * Called when {@code count} number of items are inserted at the given position.
+     *
+     * @param position The position of the new item.
+     * @param count    The number of items that have been added.
+     */
+    void onInserted(int position, int count);
+
+    /**
+     * Called when {@code count} number of items are removed from the given position.
+     *
+     * @param position The position of the item which has been removed.
+     * @param count    The number of items which have been removed.
+     */
+    void onRemoved(int position, int count);
+
+    /**
+     * Called when an item changes its position in the list.
+     *
+     * @param fromPosition The previous position of the item before the move.
+     * @param toPosition   The new position of the item.
+     */
+    void onMoved(int fromPosition, int toPosition);
+
+    /**
+     * Called when {@code count} number of items are updated at the given position.
+     *
+     * @param position The position of the item which has been updated.
+     * @param count    The number of items which has changed.
+     */
+    void onChanged(int position, int count, Object payload);
+}
diff --git a/v7/recyclerview/src/android/support/v7/util/SortedList.java b/v7/recyclerview/src/android/support/v7/util/SortedList.java
index 5ecb66f..1d48468 100644
--- a/v7/recyclerview/src/android/support/v7/util/SortedList.java
+++ b/v7/recyclerview/src/android/support/v7/util/SortedList.java
@@ -681,7 +681,7 @@
      * SortedList calls the callback methods on this class to notify changes about the underlying
      * data.
      */
-    public static abstract class Callback<T2> implements Comparator<T2> {
+    public static abstract class Callback<T2> implements Comparator<T2>, ListUpdateCallback {
 
         /**
          * Similar to {@link java.util.Comparator#compare(Object, Object)}, should compare two and
@@ -698,30 +698,6 @@
         abstract public int compare(T2 o1, T2 o2);
 
         /**
-         * Called by the SortedList when an item is inserted at the given position.
-         *
-         * @param position The position of the new item.
-         * @param count    The number of items that have been added.
-         */
-        abstract public void onInserted(int position, int count);
-
-        /**
-         * Called by the SortedList when an item is removed from the given position.
-         *
-         * @param position The position of the item which has been removed.
-         * @param count    The number of items which have been removed.
-         */
-        abstract public void onRemoved(int position, int count);
-
-        /**
-         * Called by the SortedList when an item changes its position in the list.
-         *
-         * @param fromPosition The previous position of the item before the move.
-         * @param toPosition   The new position of the item.
-         */
-        abstract public void onMoved(int fromPosition, int toPosition);
-
-        /**
          * Called by the SortedList when the item at the given position is updated.
          *
          * @param position The position of the item which has been updated.
@@ -729,6 +705,11 @@
          */
         abstract public void onChanged(int position, int count);
 
+        @Override
+        public void onChanged(int position, int count, Object payload) {
+            onChanged(position, count);
+        }
+
         /**
          * Called by the SortedList when it wants to check whether two items have the same data
          * or not. SortedList uses this information to decide whether it should call
@@ -781,16 +762,7 @@
     public static class BatchedCallback<T2> extends Callback<T2> {
 
         private final Callback<T2> mWrappedCallback;
-        static final int TYPE_NONE = 0;
-        static final int TYPE_ADD = 1;
-        static final int TYPE_REMOVE = 2;
-        static final int TYPE_CHANGE = 3;
-        static final int TYPE_MOVE = 4;
-
-        int mLastEventType = TYPE_NONE;
-        int mLastEventPosition = -1;
-        int mLastEventCount = -1;
-
+        private final BatchingListUpdateCallback mBatchingListUpdateCallback;
         /**
          * Creates a new BatchedCallback that wraps the provided Callback.
          *
@@ -800,6 +772,7 @@
          */
         public BatchedCallback(Callback<T2> wrappedCallback) {
             mWrappedCallback = wrappedCallback;
+            mBatchingListUpdateCallback = new BatchingListUpdateCallback(mWrappedCallback);
         }
 
         @Override
@@ -809,51 +782,22 @@
 
         @Override
         public void onInserted(int position, int count) {
-            if (mLastEventType == TYPE_ADD && position >= mLastEventPosition
-                    && position <= mLastEventPosition + mLastEventCount) {
-                mLastEventCount += count;
-                mLastEventPosition = Math.min(position, mLastEventPosition);
-                return;
-            }
-            dispatchLastEvent();
-            mLastEventPosition = position;
-            mLastEventCount = count;
-            mLastEventType = TYPE_ADD;
+            mBatchingListUpdateCallback.onInserted(position, count);
         }
 
         @Override
         public void onRemoved(int position, int count) {
-            if (mLastEventType == TYPE_REMOVE && mLastEventPosition == position) {
-                mLastEventCount += count;
-                return;
-            }
-            dispatchLastEvent();
-            mLastEventPosition = position;
-            mLastEventCount = count;
-            mLastEventType = TYPE_REMOVE;
+            mBatchingListUpdateCallback.onRemoved(position, count);
         }
 
         @Override
         public void onMoved(int fromPosition, int toPosition) {
-            dispatchLastEvent();//moves are not merged
-            mWrappedCallback.onMoved(fromPosition, toPosition);
+            mBatchingListUpdateCallback.onInserted(fromPosition, toPosition);
         }
 
         @Override
         public void onChanged(int position, int count) {
-            if (mLastEventType == TYPE_CHANGE &&
-                    !(position > mLastEventPosition + mLastEventCount
-                            || position + count < mLastEventPosition)) {
-                // take potential overlap into account
-                int previousEnd = mLastEventPosition + mLastEventCount;
-                mLastEventPosition = Math.min(position, mLastEventPosition);
-                mLastEventCount = Math.max(previousEnd, position + count) - mLastEventPosition;
-                return;
-            }
-            dispatchLastEvent();
-            mLastEventPosition = position;
-            mLastEventCount = count;
-            mLastEventType = TYPE_CHANGE;
+            mBatchingListUpdateCallback.onChanged(position, count, null);
         }
 
         @Override
@@ -866,27 +810,12 @@
             return mWrappedCallback.areItemsTheSame(item1, item2);
         }
 
-
         /**
          * This method dispatches any pending event notifications to the wrapped Callback.
          * You <b>must</b> always call this method after you are done with editing the SortedList.
          */
         public void dispatchLastEvent() {
-            if (mLastEventType == TYPE_NONE) {
-                return;
-            }
-            switch (mLastEventType) {
-                case TYPE_ADD:
-                    mWrappedCallback.onInserted(mLastEventPosition, mLastEventCount);
-                    break;
-                case TYPE_REMOVE:
-                    mWrappedCallback.onRemoved(mLastEventPosition, mLastEventCount);
-                    break;
-                case TYPE_CHANGE:
-                    mWrappedCallback.onChanged(mLastEventPosition, mLastEventCount);
-                    break;
-            }
-            mLastEventType = TYPE_NONE;
+            mBatchingListUpdateCallback.dispatchLastEvent();
         }
     }
 }