New ContentProviderOperation to assert values during batch.

When performing a set of batch operations, some callers need
to enforce that a query has specific values.  For example,
when persisting edited Contact values, we need to assert
that the RawContacts.VERSION matches the version we read out
through queryEntities().

This change adds a new TYPE_ASSERT that uses withValues()
and withSelection(), and checks all values when applying the
batch operation, bailing if any values don't match.
diff --git a/api/current.xml b/api/current.xml
index cc7dc23..626946b 100644
--- a/api/current.xml
+++ b/api/current.xml
@@ -29268,7 +29268,7 @@
  visibility="public"
 >
 </method>
-<method name="newCountQuery"
+<method name="newAssertQuery"
  return="android.content.ContentProviderOperation.Builder"
  abstract="false"
  native="false"
diff --git a/core/java/android/content/ContentProviderOperation.java b/core/java/android/content/ContentProviderOperation.java
index 70ea5d0..f5a4b75 100644
--- a/core/java/android/content/ContentProviderOperation.java
+++ b/core/java/android/content/ContentProviderOperation.java
@@ -16,14 +16,15 @@
 
 package android.content;
 
-import android.net.Uri;
 import android.database.Cursor;
-import android.os.Parcelable;
+import android.net.Uri;
 import android.os.Parcel;
-import android.os.Debug;
+import android.os.Parcelable;
+import android.text.TextUtils;
 
-import java.util.Map;
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.Map;
 
 public class ContentProviderOperation implements Parcelable {
     /** @hide exposed for unit tests */
@@ -33,7 +34,7 @@
     /** @hide exposed for unit tests */
     public final static int TYPE_DELETE = 3;
     /** @hide exposed for unit tests */
-    public final static int TYPE_COUNT = 4;
+    public final static int TYPE_ASSERT = 4;
 
     private final int mType;
     private final Uri mUri;
@@ -44,8 +45,6 @@
     private final ContentValues mValuesBackReferences;
     private final Map<Integer, Integer> mSelectionArgsBackReferences;
 
-    private static final String[] COUNT_COLUMNS = new String[]{"count(*)"};
-
     /**
      * Creates a {@link ContentProviderOperation} by copying the contents of a
      * {@link Builder}.
@@ -156,15 +155,12 @@
     }
 
     /**
-     * Create a {@link Builder} suitable for building a count query. When used in conjunction
-     * with {@link Builder#withExpectedCount(int)} this is useful for checking that the
-     * uri/selection has the expected number of rows.
-     * {@link ContentProviderOperation}.
-     * @param uri The {@link Uri} to query.
-     * @return a {@link Builder}
+     * Create a {@link Builder} suitable for building a
+     * {@link ContentProviderOperation} to assert a set of values as provided
+     * through {@link Builder#withValues(ContentValues)}.
      */
-    public static Builder newCountQuery(Uri uri) {
-        return new Builder(TYPE_COUNT, uri);
+    public static Builder newAssertQuery(Uri uri) {
+        return new Builder(TYPE_ASSERT, uri);
     }
 
     public Uri getUri() {
@@ -181,7 +177,7 @@
     }
 
     public boolean isReadOperation() {
-        return mType == TYPE_COUNT;
+        return mType == TYPE_ASSERT;
     }
 
     /**
@@ -217,18 +213,30 @@
             numRows = provider.delete(mUri, mSelection, selectionArgs);
         } else if (mType == TYPE_UPDATE) {
             numRows = provider.update(mUri, values, mSelection, selectionArgs);
-        } else if (mType == TYPE_COUNT) {
-            Cursor cursor = provider.query(mUri, COUNT_COLUMNS, mSelection, selectionArgs, null);
+        } else if (mType == TYPE_ASSERT) {
+            // Build projection map from expected values
+            final ArrayList<String> projectionList = new ArrayList<String>();
+            for (Map.Entry<String, Object> entry : values.valueSet()) {
+                projectionList.add(entry.getKey());
+            }
+
+            // Assert that all rows match expected values
+            final String[] projection = projectionList.toArray(new String[projectionList.size()]);
+            final Cursor cursor = provider.query(mUri, projection, mSelection, selectionArgs, null);
+            numRows = cursor.getCount();
             try {
-                if (!cursor.moveToNext()) {
-                    throw new RuntimeException("since we are doing a count query we should always "
-                            + "be able to move to the first row");
+                while (cursor.moveToNext()) {
+                    for (int i = 0; i < projection.length; i++) {
+                        final String cursorValue = cursor.getString(i);
+                        final String expectedValue = values.getAsString(projection[i]);
+                        if (!TextUtils.equals(cursorValue, expectedValue)) {
+                            // Throw exception when expected values don't match
+                            throw new OperationApplicationException("Found value " + cursorValue
+                                    + " when expected " + expectedValue + " for column "
+                                    + projection[i]);
+                        }
+                    }
                 }
-                if (cursor.getCount() != 1) {
-                    throw new RuntimeException("since we are doing a count query there should "
-                            + "always be exacly row, found " + cursor.getCount());
-                }
-                numRows = cursor.getInt(0);
             } finally {
                 cursor.close();
             }
@@ -353,7 +361,7 @@
      * first created by calling {@link ContentProviderOperation#newInsert(android.net.Uri)},
      * {@link ContentProviderOperation#newUpdate(android.net.Uri)},
      * {@link ContentProviderOperation#newDelete(android.net.Uri)} or
-     * {@link ContentProviderOperation#newCountQuery(android.net.Uri)}. The withXXX methods
+     * {@link ContentProviderOperation#newAssertQuery(Uri)}. The withXXX methods
      * can then be used to add parameters to the builder. See the specific methods to find for
      * which {@link Builder} type each is allowed. Call {@link #build} to create the
      * {@link ContentProviderOperation} once all the parameters have been supplied.
@@ -379,7 +387,7 @@
 
         /** Create a ContentProviderOperation from this {@link Builder}. */
         public ContentProviderOperation build() {
-            if (mType == TYPE_UPDATE) {
+            if (mType == TYPE_UPDATE || mType == TYPE_ASSERT) {
                 if ((mValues == null || mValues.size() == 0)
                         && (mValuesBackReferences == null || mValuesBackReferences.size() == 0)) {
                     throw new IllegalArgumentException("Empty values");
@@ -394,13 +402,13 @@
          * value should be used for the column. The value is added as a {@link String}.
          * A column value from the back references takes precedence over a value specified in
          * {@link #withValues}.
-         * This can only be used with builders of type insert or update.
+         * This can only be used with builders of type insert, update, or assert.
          * @return this builder, to allow for chaining.
          */
         public Builder withValueBackReferences(ContentValues backReferences) {
-            if (mType != TYPE_INSERT && mType != TYPE_UPDATE) {
+            if (mType != TYPE_INSERT && mType != TYPE_UPDATE && mType != TYPE_ASSERT) {
                 throw new IllegalArgumentException(
-                        "only inserts and updates can have value back-references");
+                        "only inserts, updates, and asserts can have value back-references");
             }
             mValuesBackReferences = backReferences;
             return this;
@@ -410,13 +418,13 @@
          * Add a ContentValues back reference.
          * A column value from the back references takes precedence over a value specified in
          * {@link #withValues}.
-         * This can only be used with builders of type insert or update.
+         * This can only be used with builders of type insert, update, or assert.
          * @return this builder, to allow for chaining.
          */
         public Builder withValueBackReference(String key, int previousResult) {
-            if (mType != TYPE_INSERT && mType != TYPE_UPDATE) {
+            if (mType != TYPE_INSERT && mType != TYPE_UPDATE && mType != TYPE_ASSERT) {
                 throw new IllegalArgumentException(
-                        "only inserts and updates can have value back-references");
+                        "only inserts, updates, and asserts can have value back-references");
             }
             if (mValuesBackReferences == null) {
                 mValuesBackReferences = new ContentValues();
@@ -428,13 +436,13 @@
         /**
          * Add a back references as a selection arg. Any value at that index of the selection arg
          * that was specified by {@link #withSelection} will be overwritten.
-         * This can only be used with builders of type update, delete, or count query.
+         * This can only be used with builders of type update, delete, or assert.
          * @return this builder, to allow for chaining.
          */
         public Builder withSelectionBackReference(int selectionArgIndex, int previousResult) {
-            if (mType != TYPE_COUNT && mType != TYPE_UPDATE && mType != TYPE_DELETE) {
-                throw new IllegalArgumentException(
-                        "only deletes, updates and counts can have selection back-references");
+            if (mType != TYPE_UPDATE && mType != TYPE_DELETE && mType != TYPE_ASSERT) {
+                throw new IllegalArgumentException("only updates, deletes, and asserts "
+                        + "can have selection back-references");
             }
             if (mSelectionArgsBackReferences == null) {
                 mSelectionArgsBackReferences = new HashMap<Integer, Integer>();
@@ -447,12 +455,13 @@
          * The ContentValues to use. This may be null. These values may be overwritten by
          * the corresponding value specified by {@link #withValueBackReference} or by
          * future calls to {@link #withValues} or {@link #withValue}.
-         * This can only be used with builders of type insert or update.
+         * This can only be used with builders of type insert, update, or assert.
          * @return this builder, to allow for chaining.
          */
         public Builder withValues(ContentValues values) {
-            if (mType != TYPE_INSERT && mType != TYPE_UPDATE) {
-                throw new IllegalArgumentException("only inserts and updates can have values");
+            if (mType != TYPE_INSERT && mType != TYPE_UPDATE && mType != TYPE_ASSERT) {
+                throw new IllegalArgumentException(
+                        "only inserts, updates, and asserts can have values");
             }
             if (mValues == null) {
                 mValues = new ContentValues();
@@ -464,14 +473,14 @@
         /**
          * A value to insert or update. This value may be overwritten by
          * the corresponding value specified by {@link #withValueBackReference}.
-         * This can only be used with builders of type insert or update.
+         * This can only be used with builders of type insert, update, or assert.
          * @param key the name of this value
          * @param value the value itself. the type must be acceptable for insertion by
          * {@link ContentValues#put}
          * @return this builder, to allow for chaining.
          */
         public Builder withValue(String key, Object value) {
-            if (mType != TYPE_INSERT && mType != TYPE_UPDATE) {
+            if (mType != TYPE_INSERT && mType != TYPE_UPDATE && mType != TYPE_ASSERT) {
                 throw new IllegalArgumentException("only inserts and updates can have values");
             }
             if (mValues == null) {
@@ -508,13 +517,13 @@
          * replaced with the corresponding occurence of the selection argument. Any of the
          * selection arguments may be overwritten by a selection argument back reference as
          * specified by {@link #withSelectionBackReference}.
-         * This can only be used with builders of type update, delete, or count query.
+         * This can only be used with builders of type update, delete, or assert.
          * @return this builder, to allow for chaining.
          */
         public Builder withSelection(String selection, String[] selectionArgs) {
-            if (mType != TYPE_DELETE && mType != TYPE_UPDATE && mType != TYPE_COUNT) {
+            if (mType != TYPE_UPDATE && mType != TYPE_DELETE && mType != TYPE_ASSERT) {
                 throw new IllegalArgumentException(
-                        "only deletes, updates and counts can have selections");
+                        "only updates, deletes, and asserts can have selections");
             }
             mSelection = selection;
             mSelectionArgs = selectionArgs;
@@ -524,13 +533,13 @@
         /**
          * If set then if the number of rows affected by this operation do not match
          * this count {@link OperationApplicationException} will be throw.
-         * This can only be used with builders of type update, delete, or count query.
+         * This can only be used with builders of type update, delete, or assert.
          * @return this builder, to allow for chaining.
          */
         public Builder withExpectedCount(int count) {
-            if (mType != TYPE_DELETE && mType != TYPE_UPDATE && mType != TYPE_COUNT) {
+            if (mType != TYPE_UPDATE && mType != TYPE_DELETE && mType != TYPE_ASSERT) {
                 throw new IllegalArgumentException(
-                        "only deletes, updates and counts can have expected counts");
+                        "only updates, deletes, and asserts can have expected counts");
             }
             mExpectedCount = count;
             return this;
diff --git a/tests/FrameworkTest/tests/src/android/content/ContentProviderOperationTest.java b/tests/FrameworkTest/tests/src/android/content/ContentProviderOperationTest.java
index e9a43d7..1ba9d66a 100644
--- a/tests/FrameworkTest/tests/src/android/content/ContentProviderOperationTest.java
+++ b/tests/FrameworkTest/tests/src/android/content/ContentProviderOperationTest.java
@@ -16,7 +16,9 @@
 
 package android.content;
 
+import android.content.ContentValues;
 import android.database.Cursor;
+import android.database.MatrixCursor;
 import android.net.Uri;
 import android.os.Parcel;
 import android.test.suitebuilder.annotation.SmallTest;
@@ -28,7 +30,9 @@
 import java.lang.reflect.InvocationTargetException;
 import java.util.HashMap;
 import java.util.Hashtable;
+import java.util.Set;
 import java.util.Map;
+import java.util.Map.Entry;
 
 @SmallTest
 public class ContentProviderOperationTest extends TestCase {
@@ -130,6 +134,46 @@
         assertEquals(sTestUri1.buildUpon().appendPath("19").toString(), result.uri.toString());
     }
 
+    public void testAssert() {
+        // Build an operation to assert values match provider
+        ContentProviderOperation op1 = ContentProviderOperation.newAssertQuery(sTestUri1)
+                .withValues(sTestValues1).build();
+
+        try {
+            // Assert that values match from cursor
+            ContentProviderResult result = op1.apply(new TestContentProvider() {
+                public Cursor query(Uri uri, String[] projection, String selection,
+                        String[] selectionArgs, String sortOrder) {
+                    // Return cursor over specific set of values
+                    return getCursor(sTestValues1);
+                }
+            }, null, 0);
+        } catch (OperationApplicationException e) {
+            fail("newAssert() failed");
+        }
+    }
+
+    /**
+     * Build a {@link Cursor} with a single row that contains all values
+     * provided through the given {@link ContentValues}.
+     */
+    private Cursor getCursor(ContentValues contentValues) {
+        final Set<Entry<String, Object>> valueSet = contentValues.valueSet();
+        final String[] keys = new String[valueSet.size()];
+        final Object[] values = new Object[valueSet.size()];
+
+        int i = 0;
+        for (Entry<String, Object> entry : valueSet) {
+            keys[i] = entry.getKey();
+            values[i] = entry.getValue();
+            i++;
+        }
+
+        final MatrixCursor cursor = new MatrixCursor(keys);
+        cursor.addRow(values);
+        return cursor;
+    }
+
     public void testValueBackRefs() {
         ContentValues values = new ContentValues();
         values.put("a", "in1");
@@ -167,11 +211,15 @@
 
         String[] selectionArgs = new String[]{"a", null, null, "b", null};
 
+        final ContentValues values = new ContentValues();
+        values.put("unused", "unused");
+
         ContentProviderOperation op1 = ContentProviderOperation.newUpdate(sTestUri1)
                 .withSelectionBackReference(1, 3)
                 .withSelectionBackReference(2, 1)
                 .withSelectionBackReference(4, 2)
                 .withSelection("unused", selectionArgs)
+                .withValues(values)
                 .build();
         String[] s2 = op1.resolveSelectionArgsBackReferences(
                 previousResults, previousResults.length);
@@ -212,7 +260,7 @@
             parcel.setDataPosition(0);
             op2 = ContentProviderOperation.CREATOR.createFromParcel(parcel);
 
-            assertEquals(1 /* ContentProviderOperation.TYPE_INSERT */, operationGetType(op2));
+            assertEquals(ContentProviderOperation.TYPE_INSERT, operationGetType(op2));
             assertEquals("content://goo/bar", operationGetUri(op2).toString());
             assertEquals(Integer.valueOf(42), operationGetExpectedCount(op2));
             assertEquals("selection", operationGetSelection(op2));
@@ -238,9 +286,8 @@
             op1.writeToParcel(parcel, 0);
             parcel.setDataPosition(0);
             op2 = ContentProviderOperation.CREATOR.createFromParcel(parcel);
-            assertEquals(2 /* ContentProviderOperation.TYPE_UPDATE */, operationGetType(op2));
+            assertEquals(ContentProviderOperation.TYPE_UPDATE, operationGetType(op2));
             assertEquals("content://goo/bar", operationGetUri(op2).toString());
-            assertNull(operationGetEntity(op2));
             assertNull(operationGetExpectedCount(op2));
             assertNull(operationGetSelection(op2));
             assertNull(operationGetSelectionArgs(op2));
@@ -261,9 +308,8 @@
             op1.writeToParcel(parcel, 0);
             parcel.setDataPosition(0);
             op2 = ContentProviderOperation.CREATOR.createFromParcel(parcel);
-            assertEquals(3 /* ContentProviderOperation.TYPE_DELETE */, operationGetType(op2));
+            assertEquals(ContentProviderOperation.TYPE_DELETE, operationGetType(op2));
             assertEquals("content://goo/bar", operationGetUri(op2).toString());
-            assertNull(operationGetEntity(op2));
             assertNull(operationGetExpectedCount(op2));
             assertNull(operationGetSelection(op2));
             assertNull(operationGetSelectionArgs(op2));
@@ -329,15 +375,6 @@
         field.set(builder, values);
     }
 
-    private void builderSetEntity(
-            ContentProviderOperation.Builder builder, Entity entity)
-            throws NoSuchFieldException, IllegalAccessException {
-        Field field;
-        field = CLASS_BUILDER.getDeclaredField("mEntity");
-        field.setAccessible(true);
-        field.set(builder, entity);
-    }
-
     private void builderSetExpectedCount(
             ContentProviderOperation.Builder builder, Integer expectedCount)
             throws NoSuchFieldException, IllegalAccessException {
@@ -382,13 +419,6 @@
         return (ContentValues) field.get(operation);
     }
 
-    private Entity operationGetEntity(ContentProviderOperation operation)
-            throws NoSuchFieldException, IllegalAccessException {
-        final Field field = CLASS_OPERATION.getDeclaredField("mEntity");
-        field.setAccessible(true);
-        return (Entity) field.get(operation);
-    }
-
     private Integer operationGetExpectedCount(ContentProviderOperation operation)
             throws NoSuchFieldException, IllegalAccessException {
         final Field field = CLASS_OPERATION.getDeclaredField("mExpectedCount");