enhance ContentProvider with the ability to perform batch operations
diff --git a/api/current.xml b/api/current.xml
index d7d86dee..279c2b5 100644
--- a/api/current.xml
+++ b/api/current.xml
@@ -25060,6 +25060,21 @@
  visibility="public"
 >
 </constructor>
+<method name="applyBatch"
+ return="android.content.ContentProviderResult[]"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="operations" type="android.content.ContentProviderOperation[]">
+</parameter>
+<exception name="OperationApplicationException" type="android.content.OperationApplicationException">
+</exception>
+</method>
 <method name="attachInfo"
  return="void"
  abstract="false"
@@ -25681,6 +25696,269 @@
 </exception>
 </method>
 </class>
+<class name="ContentProviderOperation"
+ extends="java.lang.Object"
+ abstract="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<method name="apply"
+ return="android.content.ContentProviderResult"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="provider" type="android.content.ContentProvider">
+</parameter>
+<parameter name="backRefs" type="android.content.ContentProviderResult[]">
+</parameter>
+<parameter name="numBackRefs" type="int">
+</parameter>
+<exception name="OperationApplicationException" type="android.content.OperationApplicationException">
+</exception>
+</method>
+<method name="backRefToValue"
+ return="java.lang.String"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="true"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="backRefs" type="android.content.ContentProviderResult[]">
+</parameter>
+<parameter name="numBackRefs" type="int">
+</parameter>
+<parameter name="backRefIndex" type="java.lang.Integer">
+</parameter>
+</method>
+<method name="newCountQuery"
+ return="android.content.ContentProviderOperation.Builder"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="true"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="uri" type="android.net.Uri">
+</parameter>
+</method>
+<method name="newDelete"
+ return="android.content.ContentProviderOperation.Builder"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="true"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="uri" type="android.net.Uri">
+</parameter>
+</method>
+<method name="newInsert"
+ return="android.content.ContentProviderOperation.Builder"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="true"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="uri" type="android.net.Uri">
+</parameter>
+</method>
+<method name="newUpdate"
+ return="android.content.ContentProviderOperation.Builder"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="true"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="uri" type="android.net.Uri">
+</parameter>
+</method>
+<method name="resolveSelectionArgsBackReferences"
+ return="java.lang.String[]"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="backRef" type="android.content.ContentProviderResult[]">
+</parameter>
+<parameter name="numBackRefs" type="int">
+</parameter>
+</method>
+<method name="resolveValueBackReferences"
+ return="android.content.ContentValues"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="backRefs" type="android.content.ContentProviderResult[]">
+</parameter>
+<parameter name="numBackRefs" type="int">
+</parameter>
+</method>
+</class>
+<class name="ContentProviderOperation.Builder"
+ extends="java.lang.Object"
+ abstract="false"
+ static="true"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<method name="build"
+ return="android.content.ContentProviderOperation"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</method>
+<method name="withExpectedCount"
+ return="android.content.ContentProviderOperation.Builder"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="count" type="int">
+</parameter>
+</method>
+<method name="withSelection"
+ return="android.content.ContentProviderOperation.Builder"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="selection" type="java.lang.String">
+</parameter>
+<parameter name="selectionArgs" type="java.lang.String[]">
+</parameter>
+</method>
+<method name="withSelectionBackReferences"
+ return="android.content.ContentProviderOperation.Builder"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="backReferences" type="java.util.Map&lt;java.lang.Integer, java.lang.Integer&gt;">
+</parameter>
+</method>
+<method name="withValueBackReferences"
+ return="android.content.ContentProviderOperation.Builder"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="backReferences" type="android.content.ContentValues">
+</parameter>
+</method>
+<method name="withValues"
+ return="android.content.ContentProviderOperation.Builder"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="values" type="android.content.ContentValues">
+</parameter>
+</method>
+</class>
+<class name="ContentProviderResult"
+ extends="java.lang.Object"
+ abstract="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<constructor name="ContentProviderResult"
+ type="android.content.ContentProviderResult"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="uri" type="android.net.Uri">
+</parameter>
+</constructor>
+<constructor name="ContentProviderResult"
+ type="android.content.ContentProviderResult"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="count" type="int">
+</parameter>
+</constructor>
+<field name="count"
+ type="java.lang.Integer"
+ transient="false"
+ volatile="false"
+ static="false"
+ final="true"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</field>
+<field name="uri"
+ type="android.net.Uri"
+ transient="false"
+ volatile="false"
+ static="false"
+ final="true"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</field>
+</class>
 <class name="ContentQueryMap"
  extends="java.util.Observable"
  abstract="false"
@@ -33581,6 +33859,55 @@
 </parameter>
 </method>
 </class>
+<class name="OperationApplicationException"
+ extends="java.lang.Exception"
+ abstract="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<constructor name="OperationApplicationException"
+ type="android.content.OperationApplicationException"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</constructor>
+<constructor name="OperationApplicationException"
+ type="android.content.OperationApplicationException"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="message" type="java.lang.String">
+</parameter>
+</constructor>
+<constructor name="OperationApplicationException"
+ type="android.content.OperationApplicationException"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="message" type="java.lang.String">
+</parameter>
+<parameter name="cause" type="java.lang.Throwable">
+</parameter>
+</constructor>
+<constructor name="OperationApplicationException"
+ type="android.content.OperationApplicationException"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="cause" type="java.lang.Throwable">
+</parameter>
+</constructor>
+</class>
 <class name="ReceiverCallNotAllowedException"
  extends="android.util.AndroidRuntimeException"
  abstract="false"
diff --git a/core/java/android/content/AbstractSyncableContentProvider.java b/core/java/android/content/AbstractSyncableContentProvider.java
index 1452985..4c7f3593 100644
--- a/core/java/android/content/AbstractSyncableContentProvider.java
+++ b/core/java/android/content/AbstractSyncableContentProvider.java
@@ -17,6 +17,8 @@
 import java.util.Map;
 import java.util.Vector;
 import java.util.ArrayList;
+import java.util.Set;
+import java.util.HashSet;
 
 import com.google.android.collect.Maps;
 
@@ -55,6 +57,9 @@
         return mIsTemporary;
     }
 
+    private final ThreadLocal<Boolean> mApplyingBatch = new ThreadLocal<Boolean>();
+    private final ThreadLocal<Set<Uri>> mPendingBatchNotifications = new ThreadLocal<Set<Uri>>();
+
     /**
      * Indicates whether or not this ContentProvider contains a full
      * set of data or just diffs. This knowledge comes in handy when
@@ -243,26 +248,37 @@
     public final int update(final Uri url, final ContentValues values,
             final String selection, final String[] selectionArgs) {
         mDb = mOpenHelper.getWritableDatabase();
-        mDb.beginTransaction();
+        final boolean notApplyingBatch = !applyingBatch();
+        if (notApplyingBatch) {
+            mDb.beginTransaction();
+        }
         try {
             if (isTemporary() && mSyncState.matches(url)) {
                 int numRows = mSyncState.asContentProvider().update(
                         url, values, selection, selectionArgs);
-                mDb.setTransactionSuccessful();
+                if (notApplyingBatch) {
+                    mDb.setTransactionSuccessful();
+                }
                 return numRows;
             }
 
             int result = updateInternal(url, values, selection, selectionArgs);
-            mDb.setTransactionSuccessful();
-
-            if (!isTemporary() && result > 0) {
-                getContext().getContentResolver().notifyChange(url, null /* observer */,
-                        changeRequiresLocalSync(url));
+            if (notApplyingBatch) {
+                mDb.setTransactionSuccessful();
             }
-
+            if (!isTemporary() && result > 0) {
+                if (notApplyingBatch) {
+                    getContext().getContentResolver().notifyChange(url, null /* observer */,
+                            changeRequiresLocalSync(url));
+                } else {
+                    mPendingBatchNotifications.get().add(url);
+                }
+            }
             return result;
         } finally {
-            mDb.endTransaction();
+            if (notApplyingBatch) {
+                mDb.endTransaction();
+            }
         }
     }
 
@@ -270,44 +286,74 @@
     public final int delete(final Uri url, final String selection,
             final String[] selectionArgs) {
         mDb = mOpenHelper.getWritableDatabase();
-        mDb.beginTransaction();
+        final boolean notApplyingBatch = !applyingBatch();
+        if (notApplyingBatch) {
+            mDb.beginTransaction();
+        }
         try {
             if (isTemporary() && mSyncState.matches(url)) {
                 int numRows = mSyncState.asContentProvider().delete(url, selection, selectionArgs);
-                mDb.setTransactionSuccessful();
+                if (notApplyingBatch) {
+                    mDb.setTransactionSuccessful();
+                }
                 return numRows;
             }
             int result = deleteInternal(url, selection, selectionArgs);
-            mDb.setTransactionSuccessful();
+            if (notApplyingBatch) {
+                mDb.setTransactionSuccessful();
+            }
             if (!isTemporary() && result > 0) {
-                getContext().getContentResolver().notifyChange(url, null /* observer */,
-                        changeRequiresLocalSync(url));
+                if (notApplyingBatch) {
+                    getContext().getContentResolver().notifyChange(url, null /* observer */,
+                            changeRequiresLocalSync(url));
+                } else {
+                    mPendingBatchNotifications.get().add(url);
+                }
             }
             return result;
         } finally {
-            mDb.endTransaction();
+            if (notApplyingBatch) {
+                mDb.endTransaction();
+            }
         }
     }
 
+    private boolean applyingBatch() {
+        return mApplyingBatch.get() != null && mApplyingBatch.get();
+    }
+
     @Override
     public final Uri insert(final Uri url, final ContentValues values) {
         mDb = mOpenHelper.getWritableDatabase();
-        mDb.beginTransaction();
+        final boolean notApplyingBatch = !applyingBatch();
+        if (notApplyingBatch) {
+            mDb.beginTransaction();
+        }
         try {
             if (isTemporary() && mSyncState.matches(url)) {
                 Uri result = mSyncState.asContentProvider().insert(url, values);
-                mDb.setTransactionSuccessful();
+                if (notApplyingBatch) {
+                    mDb.setTransactionSuccessful();
+                }
                 return result;
             }
             Uri result = insertInternal(url, values);
-            mDb.setTransactionSuccessful();
+            if (notApplyingBatch) {
+                mDb.setTransactionSuccessful();
+            }
             if (!isTemporary() && result != null) {
-                getContext().getContentResolver().notifyChange(url, null /* observer */,
-                        changeRequiresLocalSync(url));
+                if (notApplyingBatch) {
+                    getContext().getContentResolver().notifyChange(url, null /* observer */,
+                            changeRequiresLocalSync(url));
+                } else {
+                    mPendingBatchNotifications.get().add(url);
+                }
             }
             return result;
         } finally {
-            mDb.endTransaction();
+            if (notApplyingBatch) {
+                mDb.endTransaction();
+            }
         }
     }
 
@@ -342,6 +388,34 @@
         return completed;
     }
 
+    public ContentProviderResult[] applyBatch(ContentProviderOperation[] operations)
+            throws OperationApplicationException {
+        // initialize if this is the first time this thread has applied a batch
+        if (mApplyingBatch.get() == null) {
+            mApplyingBatch.set(false);
+            mPendingBatchNotifications.set(new HashSet<Uri>());
+        }
+
+        if (applyingBatch()) {
+            throw new IllegalStateException(
+                    "applyBatch is not reentrant but mApplyingBatch is already set");
+        }
+        getDatabase().beginTransaction();
+        try {
+            mApplyingBatch.set(true);
+            ContentProviderResult[] results = super.applyBatch(operations);
+            getDatabase().setTransactionSuccessful();
+            return results;
+        } finally {
+            mApplyingBatch.set(false);
+            getDatabase().endTransaction();
+            for (Uri url : mPendingBatchNotifications.get()) {
+                getContext().getContentResolver().notifyChange(url, null /* observer */,
+                        changeRequiresLocalSync(url));
+            }
+        }
+    }
+    
     /**
      * Check if changes to this URI can be syncable changes.
      * @param uri the URI of the resource that was changed
diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java
index 3a8de6e..c204dda 100644
--- a/core/java/android/content/ContentProvider.java
+++ b/core/java/android/content/ContentProvider.java
@@ -629,4 +629,26 @@
             ContentProvider.this.onCreate();
         }
     }
-}
+
+    /**
+     * Applies each of the {@link ContentProviderOperation} objects and returns an array
+     * of their results. Passes through OperationApplicationException, which may be thrown
+     * by the call to {@link ContentProviderOperation#apply}.
+     * If all the applications succeed then a {@link ContentProviderResult} array with the
+     * same number of elements as the operations will be returned. It is implementation-specific
+     * how many, if any, operations will have been successfully applied if a call to
+     * apply results in a {@link OperationApplicationException}.
+     * @param operations the operations to apply
+     * @return the results of the applications
+     * @throws OperationApplicationException thrown if an application fails.
+     * See {@link ContentProviderOperation#apply} for more information.
+     */
+    public ContentProviderResult[] applyBatch(ContentProviderOperation[] operations)
+            throws OperationApplicationException {
+        ContentProviderResult[] results = new ContentProviderResult[operations.length];
+        for (int i = 0; i < operations.length; i++) {
+            results[i] = operations[i].apply(this, results, i);
+        }
+        return results;
+    }
+}
\ No newline at end of file
diff --git a/core/java/android/content/ContentProviderOperation.java b/core/java/android/content/ContentProviderOperation.java
new file mode 100644
index 0000000..148cc35
--- /dev/null
+++ b/core/java/android/content/ContentProviderOperation.java
@@ -0,0 +1,358 @@
+/*
+ * Copyright (C) 2009 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.content;
+
+import android.net.Uri;
+import android.database.Cursor;
+
+import java.util.Map;
+
+public class ContentProviderOperation {
+    private final static int TYPE_INSERT = 1;
+    private final static int TYPE_UPDATE = 2;
+    private final static int TYPE_DELETE = 3;
+    private final static int TYPE_COUNT = 4;
+
+    private final int mType;
+    private final Uri mUri;
+    private final String mSelection;
+    private final String[] mSelectionArgs;
+    private final ContentValues mValues;
+    private final Integer mExpectedCount;
+    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}.
+     */
+    private ContentProviderOperation(Builder builder) {
+        mType = builder.mType;
+        mUri = builder.mUri;
+        mValues = builder.mValues;
+        mSelection = builder.mSelection;
+        mSelectionArgs = builder.mSelectionArgs;
+        mExpectedCount = builder.mExpectedCount;
+        mSelectionArgsBackReferences = builder.mSelectionArgsBackReferences;
+        mValuesBackReferences = builder.mValuesBackReferences;
+    }
+
+    /**
+     * Create a {@link Builder} suitable for building an insert {@link ContentProviderOperation}.
+     * @param uri The {@link Uri} that is the target of the insert.
+     * @return a {@link Builder}
+     */
+    public static Builder newInsert(Uri uri) {
+        return new Builder(TYPE_INSERT, uri);
+    }
+
+    /**
+     * Create a {@link Builder} suitable for building an update {@link ContentProviderOperation}.
+     * @param uri The {@link Uri} that is the target of the update.
+     * @return a {@link Builder}
+     */
+    public static Builder newUpdate(Uri uri) {
+        return new Builder(TYPE_UPDATE, uri);
+    }
+
+    /**
+     * Create a {@link Builder} suitable for building a delete {@link ContentProviderOperation}.
+     * @param uri The {@link Uri} that is the target of the delete.
+     * @return a {@link Builder}
+     */
+    public static Builder newDelete(Uri uri) {
+        return new Builder(TYPE_DELETE, uri);
+    }
+
+    /**
+     * 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}
+     */
+    public static Builder newCountQuery(Uri uri) {
+        return new Builder(TYPE_COUNT, uri);
+    }
+
+    /**
+     * Applies this operation using the given provider. The backRefs array is used to resolve any
+     * back references that were requested using
+     * {@link Builder#withValueBackReferences(ContentValues)} and
+     * {@link Builder#withSelectionBackReferences(java.util.Map)}.
+     * @param provider the {@link ContentProvider} on which this batch is applied
+     * @param backRefs a {@link ContentProviderResult} array that will be consulted
+     * to resolve any requested back references.
+     * @param numBackRefs the number of valid results on the backRefs array.
+     * @return a {@link ContentProviderResult} that contains either the {@link Uri} of the inserted
+     * row if this was an insert otherwise the number of rows affected.
+     * @throws OperationApplicationException thrown if either the insert fails or
+     * if the number of rows affected didn't match the expected count
+     */
+    public ContentProviderResult apply(ContentProvider provider, ContentProviderResult[] backRefs,
+            int numBackRefs) throws OperationApplicationException {
+        ContentValues values = resolveValueBackReferences(backRefs, numBackRefs);
+        String[] selectionArgs =
+                resolveSelectionArgsBackReferences(backRefs, numBackRefs);
+
+        if (mType == TYPE_INSERT) {
+            Uri newUri = provider.insert(mUri, values);
+            if (newUri == null) {
+                throw new OperationApplicationException("insert failed");
+            }
+            return new ContentProviderResult(newUri);
+        }
+
+        int numRows;
+        if (mType == TYPE_DELETE) {
+            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);
+            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");
+                }
+                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();
+            }
+        } else {
+            throw new IllegalStateException("bad type, " + mType);
+        }
+
+        if (mExpectedCount != null && mExpectedCount != numRows) {
+            throw new OperationApplicationException("wrong number of rows: " + numRows);
+        }
+
+        return new ContentProviderResult(numRows);
+    }
+
+    /**
+     * The ContentValues back references are represented as a ContentValues object where the
+     * key refers to a column and the value is an index of the back reference whose
+     * valued should be associated with the column.
+     * @param backRefs an array of previous results
+     * @param numBackRefs the number of valid previous results in backRefs
+     * @return the ContentValues that should be used in this operation application after
+     * expansion of back references. This can be called if either mValues or mValuesBackReferences
+     * is null
+     * @VisibleForTesting this is intended to be a private method but it is exposed for
+     * unit testing purposes
+     */
+    public ContentValues resolveValueBackReferences(
+            ContentProviderResult[] backRefs, int numBackRefs) {
+        if (mValuesBackReferences == null) {
+            return mValues;
+        }
+        final ContentValues values;
+        if (mValues == null) {
+            values = new ContentValues();
+        } else {
+            values = new ContentValues(mValues);
+        }
+        for (Map.Entry<String, Object> entry : mValuesBackReferences.valueSet()) {
+            String key = entry.getKey();
+            Integer backRefIndex = mValuesBackReferences.getAsInteger(key);
+            if (backRefIndex == null) {
+                throw new IllegalArgumentException("values backref " + key + " is not an integer");
+            }
+            values.put(key, backRefToValue(backRefs, numBackRefs, backRefIndex));
+        }
+        return values;
+    }
+
+    /**
+     * The Selection Arguments back references are represented as a Map of Integer->Integer where
+     * the key is an index into the selection argument array (see {@link Builder#withSelection})
+     * and the value is the index of the previous result that should be used for that selection
+     * argument array slot.
+     * @param backRefs an array of previous results
+     * @param numBackRefs the number of valid previous results in backRefs
+     * @return the ContentValues that should be used in this operation application after
+     * expansion of back references. This can be called if either mValues or mValuesBackReferences
+     * is null
+     * @VisibleForTesting this is intended to be a private method but it is exposed for
+     * unit testing purposes
+     */
+    public String[] resolveSelectionArgsBackReferences(
+            ContentProviderResult[] backRefs, int numBackRefs) {
+        if (mSelectionArgsBackReferences == null) {
+            return mSelectionArgs;
+        }
+        String[] newArgs = new String[mSelectionArgs.length];
+        System.arraycopy(mSelectionArgs, 0, newArgs, 0, mSelectionArgs.length);
+        for (Map.Entry<Integer, Integer> selectionArgBackRef
+                : mSelectionArgsBackReferences.entrySet()) {
+            final Integer selectionArgIndex = selectionArgBackRef.getKey();
+            final int backRefIndex = selectionArgBackRef.getValue();
+            newArgs[selectionArgIndex] = backRefToValue(backRefs, numBackRefs, backRefIndex);
+        }
+        return newArgs;
+    }
+
+    /**
+     * Return the string representation of the requested back reference.
+     * @param backRefs an array of results
+     * @param numBackRefs the number of items in the backRefs array that are valid
+     * @param backRefIndex which backRef to be used
+     * @throws ArrayIndexOutOfBoundsException thrown if the backRefIndex is larger than
+     * the numBackRefs
+     * @return the string representation of the requested back reference.
+     */
+    private static String backRefToValue(ContentProviderResult[] backRefs, int numBackRefs,
+            Integer backRefIndex) {
+        if (backRefIndex > numBackRefs) {
+            throw new ArrayIndexOutOfBoundsException("asked for back ref " + backRefIndex
+                    + " but there are only " + numBackRefs + " back refs");
+        }
+        ContentProviderResult backRef = backRefs[backRefIndex];
+        String backRefValue;
+        if (backRef.uri != null) {
+            backRefValue = backRef.uri.getLastPathSegment();
+        } else {
+            backRefValue = String.valueOf(backRef.count);
+        }
+        return backRefValue;
+    }
+
+    /**
+     * Used to add parameters to a {@link ContentProviderOperation}. The {@link Builder} is
+     * 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
+     * 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.
+     */
+    public static class Builder {
+        private final int mType;
+        private final Uri mUri;
+        private String mSelection;
+        private String[] mSelectionArgs;
+        private ContentValues mValues;
+        private Integer mExpectedCount;
+        private ContentValues mValuesBackReferences;
+        private Map<Integer, Integer> mSelectionArgsBackReferences;
+
+        /** Create a {@link Builder} of a given type. The uri must not be null. */
+        private Builder(int type, Uri uri) {
+            if (uri == null) {
+                throw new IllegalArgumentException("uri must not be null");
+            }
+            mType = type;
+            mUri = uri;
+        }
+
+        /** Create a ContentroviderOperation from this {@link Builder}. */
+        public ContentProviderOperation build() {
+            return new ContentProviderOperation(this);
+        }
+
+        /**
+         * Add a {@link ContentValues} of back references. The key is the name of the column
+         * and the value is an integer that is the index of the previous result whose
+         * 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.
+         * @return this builder, to allow for chaining.
+         */
+        public Builder withValueBackReferences(ContentValues backReferences) {
+            if (mType != TYPE_INSERT && mType != TYPE_UPDATE) {
+                throw new IllegalArgumentException(
+                        "only inserts and updates can have value back-references");
+            }
+            mValuesBackReferences = backReferences;
+            return this;
+        }
+
+        /**
+         * Add a {@link Map} of back references. The integer key is the index of the selection arg
+         * to set and the integer value is the index of the previous result whose
+         * value should be used for the arg. If any value at that index of the selection arg
+         * that was specified by {@likn withSelection} will be overwritten.
+         * This can only be used with builders of type update, delete, or count query.
+         * @return this builder, to allow for chaining.
+         */
+        public Builder withSelectionBackReferences(Map<Integer, Integer> backReferences) {
+            if (mType != TYPE_COUNT && mType != TYPE_UPDATE && mType != TYPE_DELETE) {
+                throw new IllegalArgumentException(
+                        "only deletes, updates and counts can have selection back-references");
+            }
+            mSelectionArgsBackReferences = backReferences;
+            return this;
+        }
+
+        /**
+         * The ContentValues to use. This may be null. These values may be overwritten by
+         * the corresponding value specified by {@link #withValueBackReferences(ContentValues)}.
+         * This can only be used with builders of type insert or update.
+         * @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");
+            }
+            mValues = values;
+            return this;
+        }
+
+        /**
+         * The selection and arguments to use. An occurrence of '?' in the selection will be
+         * 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 #withSelectionBackReferences}.
+         * This can only be used with builders of type update, delete, or count query.
+         * @return this builder, to allow for chaining.
+         */
+        public Builder withSelection(String selection, String[] selectionArgs) {
+            if (mType != TYPE_DELETE && mType != TYPE_UPDATE && mType != TYPE_COUNT) {
+                throw new IllegalArgumentException(
+                        "only deletes, updates and counts can have selections");
+            }
+            mSelection = selection;
+            mSelectionArgs = selectionArgs;
+            return this;
+        }
+
+        /**
+         * 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.
+         * @return this builder, to allow for chaining.
+         */
+        public Builder withExpectedCount(int count) {
+            if (mType != TYPE_DELETE && mType != TYPE_UPDATE && mType != TYPE_COUNT) {
+                throw new IllegalArgumentException(
+                        "only deletes, updates and counts can have expected counts");
+            }
+            mExpectedCount = count;
+            return this;
+        }
+    }
+}
diff --git a/core/java/android/content/ContentProviderResult.java b/core/java/android/content/ContentProviderResult.java
new file mode 100644
index 0000000..2e34e40
--- /dev/null
+++ b/core/java/android/content/ContentProviderResult.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2009 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.content;
+
+import android.net.Uri;
+
+/**
+ * Contains the result of the application of a {@link ContentProviderOperation}. It is guaranteed
+ * to have exactly one of {@link #uri} or {@link #count} set.
+ */
+public class ContentProviderResult {
+    public final Uri uri;
+    public final Integer count;
+
+    public ContentProviderResult(Uri uri) {
+        if (uri == null) throw new IllegalArgumentException("uri must not be null");
+        this.uri = uri;
+        this.count = null;
+    }
+
+    public ContentProviderResult(int count) {
+        this.count = count;
+        this.uri = null;
+    }
+}
\ No newline at end of file
diff --git a/core/java/android/content/OperationApplicationException.java b/core/java/android/content/OperationApplicationException.java
new file mode 100644
index 0000000..d4101bf
--- /dev/null
+++ b/core/java/android/content/OperationApplicationException.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2009 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.content;
+
+/**
+ * Thrown when an application of a {@link ContentProviderOperation} fails due the specified
+ * constraints.
+ */
+public class OperationApplicationException extends Exception {
+    public OperationApplicationException() {
+        super();
+    }
+    public OperationApplicationException(String message) {
+        super(message);
+    }
+    public OperationApplicationException(String message, Throwable cause) {
+        super(message, cause);
+    }
+    public OperationApplicationException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/tests/AndroidTests/src/com/android/unit_tests/content/ContentProviderOperationTest.java b/tests/AndroidTests/src/com/android/unit_tests/content/ContentProviderOperationTest.java
new file mode 100644
index 0000000..46a12fd
--- /dev/null
+++ b/tests/AndroidTests/src/com/android/unit_tests/content/ContentProviderOperationTest.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2009 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.android.unit_tests.content;
+
+import android.test.suitebuilder.annotation.SmallTest;
+import android.net.Uri;
+import android.content.ContentValues;
+import android.content.ContentProviderOperation;
+import android.content.OperationApplicationException;
+import android.content.ContentProviderResult;
+import android.content.ContentProvider;
+import android.text.TextUtils;
+import android.database.Cursor;
+import junit.framework.TestCase;
+
+import java.util.Map;
+import java.util.Hashtable;
+
+@SmallTest
+public class ContentProviderOperationTest extends TestCase {
+    private final static Uri sTestUri1 = Uri.parse("content://authority/blah");
+    private final static ContentValues sTestValues1;
+
+    static {
+        sTestValues1 = new ContentValues();
+        sTestValues1.put("a", 1);
+        sTestValues1.put("b", "two");
+    }
+
+    public void testInsert() throws OperationApplicationException {
+        ContentProviderOperation op1 = ContentProviderOperation.newInsert(sTestUri1)
+                .withValues(sTestValues1)
+                .build();
+        ContentProviderResult result = op1.apply(new TestContentProvider() {
+            public Uri insert(Uri uri, ContentValues values) {
+                assertEquals(sTestUri1.toString(), uri.toString());
+                assertEquals(sTestValues1.toString(), values.toString());
+                return uri.buildUpon().appendPath("19").build();
+            }
+        }, null, 0);
+        assertEquals(sTestUri1.buildUpon().appendPath("19").toString(), result.uri.toString());
+    }
+
+    public void testInsertNoValues() throws OperationApplicationException {
+        ContentProviderOperation op1 = ContentProviderOperation.newInsert(sTestUri1)
+                .build();
+        ContentProviderResult result = op1.apply(new TestContentProvider() {
+            public Uri insert(Uri uri, ContentValues values) {
+                assertEquals(sTestUri1.toString(), uri.toString());
+                assertNull(values);
+                return uri.buildUpon().appendPath("19").build();
+            }
+        }, null, 0);
+        assertEquals(sTestUri1.buildUpon().appendPath("19").toString(), result.uri.toString());
+    }
+
+    public void testInsertFailed() {
+        ContentProviderOperation op1 = ContentProviderOperation.newInsert(sTestUri1)
+                .withValues(sTestValues1)
+                .build();
+        try {
+            op1.apply(new TestContentProvider() {
+                public Uri insert(Uri uri, ContentValues values) {
+                    assertEquals(sTestUri1.toString(), uri.toString());
+                    assertEquals(sTestValues1.toString(), values.toString());
+                    return null;
+                }
+            }, null, 0);
+            fail("the apply should have thrown an OperationApplicationException");
+        } catch (OperationApplicationException e) {
+            // this is the expected case
+        }
+    }
+
+    public void testInsertWithBackRefs() throws OperationApplicationException {
+        ContentValues valuesBackRefs = new ContentValues();
+        valuesBackRefs.put("a1", 3);
+        valuesBackRefs.put("a2", 1);
+
+        ContentProviderResult[] previousResults = new ContentProviderResult[4];
+        previousResults[0] = new ContentProviderResult(100);
+        previousResults[1] = new ContentProviderResult(101);
+        previousResults[2] = new ContentProviderResult(102);
+        previousResults[3] = new ContentProviderResult(103);
+        ContentProviderOperation op1 = ContentProviderOperation.newInsert(sTestUri1)
+                .withValues(sTestValues1)
+                .withValueBackReferences(valuesBackRefs)
+                .build();
+        ContentProviderResult result = op1.apply(new TestContentProvider() {
+            public Uri insert(Uri uri, ContentValues values) {
+                assertEquals(sTestUri1.toString(), uri.toString());
+                ContentValues expected = new ContentValues(sTestValues1);
+                expected.put("a1", 103);
+                expected.put("a2", 101);
+                assertEquals(expected.toString(), values.toString());
+                return uri.buildUpon().appendPath("19").build();
+            }
+        }, previousResults, previousResults.length);
+        assertEquals(sTestUri1.buildUpon().appendPath("19").toString(), result.uri.toString());
+    }
+
+    public void testUpdate() throws OperationApplicationException {
+        ContentProviderOperation op1 = ContentProviderOperation.newInsert(sTestUri1)
+                .withValues(sTestValues1)
+                .build();
+        ContentProviderResult[] backRefs = new ContentProviderResult[2];
+        ContentProviderResult result = op1.apply(new TestContentProvider() {
+            public Uri insert(Uri uri, ContentValues values) {
+                assertEquals(sTestUri1.toString(), uri.toString());
+                assertEquals(sTestValues1.toString(), values.toString());
+                return uri.buildUpon().appendPath("19").build();
+            }
+        }, backRefs, 1);
+        assertEquals(sTestUri1.buildUpon().appendPath("19").toString(), result.uri.toString());
+    }
+
+    public void testValueBackRefs() {
+        ContentValues values = new ContentValues();
+        values.put("a", "in1");
+        values.put("a2", "in2");
+        values.put("b", "in3");
+        values.put("c", "in4");
+
+        ContentProviderResult[] previousResults = new ContentProviderResult[4];
+        previousResults[0] = new ContentProviderResult(100);
+        previousResults[1] = new ContentProviderResult(101);
+        previousResults[2] = new ContentProviderResult(102);
+        previousResults[3] = new ContentProviderResult(103);
+
+        ContentValues valuesBackRefs = new ContentValues();
+        valuesBackRefs.put("a1", 3); // a1 -> 103
+        valuesBackRefs.put("a2", 1); // a2 -> 101
+        valuesBackRefs.put("a3", 2); // a3 -> 102
+
+        ContentValues expectedValues = new ContentValues(values);
+        expectedValues.put("a1", "103");
+        expectedValues.put("a2", "101");
+        expectedValues.put("a3", "102");
+
+        ContentProviderOperation op1 = ContentProviderOperation.newInsert(sTestUri1)
+                .withValues(values)
+                .withValueBackReferences(valuesBackRefs)
+                .build();
+        ContentValues v2 = op1.resolveValueBackReferences(previousResults, previousResults.length);
+        assertEquals(expectedValues, v2);
+    }
+
+    public void testSelectionBackRefs() {
+        Map<Integer, Integer> selectionBackRefs = new Hashtable<Integer, Integer>();
+        selectionBackRefs.put(1, 3);
+        selectionBackRefs.put(2, 1);
+        selectionBackRefs.put(4, 2);
+
+        ContentProviderResult[] previousResults = new ContentProviderResult[4];
+        previousResults[0] = new ContentProviderResult(100);
+        previousResults[1] = new ContentProviderResult(101);
+        previousResults[2] = new ContentProviderResult(102);
+        previousResults[3] = new ContentProviderResult(103);
+
+        String[] selectionArgs = new String[]{"a", null, null, "b", null};
+
+        ContentProviderOperation op1 = ContentProviderOperation.newUpdate(sTestUri1)
+                .withSelectionBackReferences(selectionBackRefs)
+                .withSelection("unused", selectionArgs)
+                .build();
+        String[] s2 = op1.resolveSelectionArgsBackReferences(
+                previousResults, previousResults.length);
+        assertEquals("a,103,101,b,102", TextUtils.join(",", s2));
+    }
+
+    static class TestContentProvider extends ContentProvider {
+        public boolean onCreate() {
+            throw new UnsupportedOperationException();
+        }
+
+        public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+                String sortOrder) {
+            throw new UnsupportedOperationException();
+        }
+
+        public String getType(Uri uri) {
+            throw new UnsupportedOperationException();
+        }
+
+        public Uri insert(Uri uri, ContentValues values) {
+            throw new UnsupportedOperationException();
+        }
+
+        public int delete(Uri uri, String selection, String[] selectionArgs) {
+            throw new UnsupportedOperationException();
+        }
+
+        public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+            throw new UnsupportedOperationException();
+        }
+    }
+}
\ No newline at end of file