Merge "Fix bug 5399568 - ListView check states inconsistent after data set change"
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java
index c218f23..0d287cf 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.java
@@ -237,9 +237,11 @@
     SparseBooleanArray mCheckStates;
 
     /**
-     * Running state of which IDs are currently checked
+     * Running state of which IDs are currently checked.
+     * If there is a value for a given key, the checked state for that ID is true
+     * and the value holds the last known position in the adapter for that id.
      */
-    LongSparseArray<Boolean> mCheckedIdStates;
+    LongSparseArray<Integer> mCheckedIdStates;
 
     /**
      * Controls how the next layout will happen
@@ -472,6 +474,13 @@
     static final int OVERSCROLL_LIMIT_DIVISOR = 3;
 
     /**
+     * How many positions in either direction we will search to try to
+     * find a checked item with a stable ID that moved position across
+     * a data set change. If the item isn't found it will be unselected.
+     */
+    private static final int CHECK_POSITION_SEARCH_DISTANCE = 20;
+
+    /**
      * Used to request a layout when we changed touch mode
      */
     private static final int TOUCH_MODE_UNKNOWN = -1;
@@ -806,7 +815,7 @@
         if (adapter != null) {
             if (mChoiceMode != CHOICE_MODE_NONE && mAdapter.hasStableIds() &&
                     mCheckedIdStates == null) {
-                mCheckedIdStates = new LongSparseArray<Boolean>();
+                mCheckedIdStates = new LongSparseArray<Integer>();
             }
         }
 
@@ -901,7 +910,7 @@
             return new long[0];
         }
 
-        final LongSparseArray<Boolean> idStates = mCheckedIdStates;
+        final LongSparseArray<Integer> idStates = mCheckedIdStates;
         final int count = idStates.size();
         final long[] ids = new long[count];
 
@@ -948,7 +957,7 @@
             mCheckStates.put(position, value);
             if (mCheckedIdStates != null && mAdapter.hasStableIds()) {
                 if (value) {
-                    mCheckedIdStates.put(mAdapter.getItemId(position), Boolean.TRUE);
+                    mCheckedIdStates.put(mAdapter.getItemId(position), position);
                 } else {
                     mCheckedIdStates.delete(mAdapter.getItemId(position));
                 }
@@ -980,7 +989,7 @@
             if (value) {
                 mCheckStates.put(position, true);
                 if (updateIds) {
-                    mCheckedIdStates.put(mAdapter.getItemId(position), Boolean.TRUE);
+                    mCheckedIdStates.put(mAdapter.getItemId(position), position);
                 }
                 mCheckedItemCount = 1;
             } else if (mCheckStates.size() == 0 || !mCheckStates.valueAt(0)) {
@@ -1010,7 +1019,7 @@
                 mCheckStates.put(position, newValue);
                 if (mCheckedIdStates != null && mAdapter.hasStableIds()) {
                     if (newValue) {
-                        mCheckedIdStates.put(mAdapter.getItemId(position), Boolean.TRUE);
+                        mCheckedIdStates.put(mAdapter.getItemId(position), position);
                     } else {
                         mCheckedIdStates.delete(mAdapter.getItemId(position));
                     }
@@ -1032,7 +1041,7 @@
                     mCheckStates.put(position, true);
                     if (mCheckedIdStates != null && mAdapter.hasStableIds()) {
                         mCheckedIdStates.clear();
-                        mCheckedIdStates.put(mAdapter.getItemId(position), Boolean.TRUE);
+                        mCheckedIdStates.put(mAdapter.getItemId(position), position);
                     }
                     mCheckedItemCount = 1;
                 } else if (mCheckStates.size() == 0 || !mCheckStates.valueAt(0)) {
@@ -1081,7 +1090,7 @@
                 mCheckStates = new SparseBooleanArray();
             }
             if (mCheckedIdStates == null && mAdapter != null && mAdapter.hasStableIds()) {
-                mCheckedIdStates = new LongSparseArray<Boolean>();
+                mCheckedIdStates = new LongSparseArray<Integer>();
             }
             // Modal multi-choice mode only has choices when the mode is active. Clear them.
             if (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL) {
@@ -1411,7 +1420,7 @@
         boolean inActionMode;
         int checkedItemCount;
         SparseBooleanArray checkState;
-        LongSparseArray<Boolean> checkIdState;
+        LongSparseArray<Integer> checkIdState;
 
         /**
          * Constructor called from {@link AbsListView#onSaveInstanceState()}
@@ -1435,10 +1444,14 @@
             checkedItemCount = in.readInt();
             checkState = in.readSparseBooleanArray();
             long[] idState = in.createLongArray();
+            int[] idPositions = in.createIntArray();
 
-            if (idState.length > 0) {
-                checkIdState = new LongSparseArray<Boolean>();
-                checkIdState.setValues(idState, Boolean.TRUE);
+            final int idLength = idState.length;
+            if (idLength > 0) {
+                checkIdState = new LongSparseArray<Integer>();
+                for (int i = 0; i < idLength; i++) {
+                    checkIdState.put(idState[i], idPositions[i]);
+                }
             }
         }
 
@@ -1455,6 +1468,15 @@
             out.writeInt(checkedItemCount);
             out.writeSparseBooleanArray(checkState);
             out.writeLongArray(checkIdState != null ? checkIdState.getKeys() : new long[0]);
+
+            int size = checkIdState != null ? checkIdState.size() : 0;
+            int[] idPositions = new int[size];
+            if (size > 0) {
+                for (int i = 0; i < size; i++) {
+                    idPositions[i] = checkIdState.valueAt(i);
+                }
+                out.writeIntArray(idPositions);
+            }
         }
 
         @Override
@@ -1549,7 +1571,7 @@
             ss.checkState = mCheckStates.clone();
         }
         if (mCheckedIdStates != null) {
-            final LongSparseArray<Boolean> idState = new LongSparseArray<Boolean>();
+            final LongSparseArray<Integer> idState = new LongSparseArray<Integer>();
             final int count = mCheckedIdStates.size();
             for (int i = 0; i < count; i++) {
                 idState.put(mCheckedIdStates.keyAt(i), mCheckedIdStates.valueAt(i));
@@ -4770,15 +4792,63 @@
         return selectedPos >= 0;
     }
 
+    void confirmCheckedPositionsById() {
+        // Clear out the positional check states, we'll rebuild it below from IDs.
+        mCheckStates.clear();
+
+        boolean checkedCountChanged = false;
+        for (int checkedIndex = 0; checkedIndex < mCheckedIdStates.size(); checkedIndex++) {
+            final long id = mCheckedIdStates.keyAt(checkedIndex);
+            final int lastPos = mCheckedIdStates.valueAt(checkedIndex);
+
+            final long lastPosId = mAdapter.getItemId(lastPos);
+            if (id != lastPosId) {
+                // Look around to see if the ID is nearby. If not, uncheck it.
+                final int start = Math.max(0, lastPos - CHECK_POSITION_SEARCH_DISTANCE);
+                final int end = Math.min(lastPos + CHECK_POSITION_SEARCH_DISTANCE, mItemCount);
+                boolean found = false;
+                for (int searchPos = start; searchPos < end; searchPos++) {
+                    final long searchId = mAdapter.getItemId(searchPos);
+                    if (id == searchId) {
+                        found = true;
+                        mCheckStates.put(searchPos, true);
+                        mCheckedIdStates.setValueAt(checkedIndex, searchPos);
+                        break;
+                    }
+                }
+
+                if (!found) {
+                    mCheckedIdStates.delete(id);
+                    checkedIndex--;
+                    mCheckedItemCount--;
+                    checkedCountChanged = true;
+                    if (mChoiceActionMode != null && mMultiChoiceModeCallback != null) {
+                        mMultiChoiceModeCallback.onItemCheckedStateChanged(mChoiceActionMode,
+                                lastPos, id, false);
+                    }
+                }
+            } else {
+                mCheckStates.put(lastPos, true);
+            }
+        }
+
+        if (checkedCountChanged && mChoiceActionMode != null) {
+            mChoiceActionMode.invalidate();
+        }
+    }
+
     @Override
     protected void handleDataChanged() {
         int count = mItemCount;
         int lastHandledItemCount = mLastHandledItemCount;
         mLastHandledItemCount = mItemCount;
+
+        if (mChoiceMode != CHOICE_MODE_NONE && mAdapter != null && mAdapter.hasStableIds()) {
+            confirmCheckedPositionsById();
+        }
+
         if (count > 0) {
-
             int newPos;
-
             int selectablePos;
 
             // Find the row we are supposed to sync to