diff options
7 files changed, 1088 insertions, 21 deletions
diff --git a/api/current.xml b/api/current.xml index d7d86de..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<java.lang.Integer, java.lang.Integer>"> +</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..4c7f359 100644 --- a/core/java/android/content/AbstractSyncableContentProvider.java +++ b/core/java/android/content/AbstractSyncableContentProvider.java @@ -17,6 +17,8 @@ import java.util.Collections; 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 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro 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 abstract class AbstractSyncableContentProvider extends SyncableContentPro 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 (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(); + } } } @@ -270,44 +286,74 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro 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 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro 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 @@ public abstract class ContentProvider implements ComponentCallbacks { 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 |