/* * 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.annotation.NonNull; import android.annotation.Nullable; import android.compat.annotation.UnsupportedAppUsage; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; import android.util.ArrayMap; import android.util.Log; import android.util.SparseArray; import java.util.ArrayList; import java.util.Map; import java.util.Objects; /** * Represents a single operation to be performed as part of a batch of operations. * * @see ContentProvider#applyBatch(ArrayList) */ public class ContentProviderOperation implements Parcelable { /** @hide exposed for unit tests */ @UnsupportedAppUsage public final static int TYPE_INSERT = 1; /** @hide exposed for unit tests */ @UnsupportedAppUsage public final static int TYPE_UPDATE = 2; /** @hide exposed for unit tests */ @UnsupportedAppUsage public final static int TYPE_DELETE = 3; /** @hide exposed for unit tests */ public final static int TYPE_ASSERT = 4; /** @hide exposed for unit tests */ public final static int TYPE_CALL = 5; @UnsupportedAppUsage private final int mType; @UnsupportedAppUsage private final Uri mUri; private final String mMethod; private final String mArg; private final ArrayMap mValues; private final ArrayMap mExtras; @UnsupportedAppUsage private final String mSelection; private final SparseArray mSelectionArgs; private final Integer mExpectedCount; private final boolean mYieldAllowed; private final boolean mExceptionAllowed; private final static String TAG = "ContentProviderOperation"; /** * Creates a {@link ContentProviderOperation} by copying the contents of a * {@link Builder}. */ private ContentProviderOperation(Builder builder) { mType = builder.mType; mUri = builder.mUri; mMethod = builder.mMethod; mArg = builder.mArg; mValues = builder.mValues; mExtras = builder.mExtras; mSelection = builder.mSelection; mSelectionArgs = builder.mSelectionArgs; mExpectedCount = builder.mExpectedCount; mYieldAllowed = builder.mYieldAllowed; mExceptionAllowed = builder.mExceptionAllowed; } private ContentProviderOperation(Parcel source) { mType = source.readInt(); mUri = Uri.CREATOR.createFromParcel(source); mMethod = source.readInt() != 0 ? source.readString8() : null; mArg = source.readInt() != 0 ? source.readString8() : null; final int valuesSize = source.readInt(); if (valuesSize != -1) { mValues = new ArrayMap<>(valuesSize); source.readArrayMap(mValues, null); } else { mValues = null; } final int extrasSize = source.readInt(); if (extrasSize != -1) { mExtras = new ArrayMap<>(extrasSize); source.readArrayMap(mExtras, null); } else { mExtras = null; } mSelection = source.readInt() != 0 ? source.readString8() : null; mSelectionArgs = source.readSparseArray(null); mExpectedCount = source.readInt() != 0 ? source.readInt() : null; mYieldAllowed = source.readInt() != 0; mExceptionAllowed = source.readInt() != 0; } /** @hide */ public ContentProviderOperation(ContentProviderOperation cpo, Uri withUri) { mType = cpo.mType; mUri = withUri; mMethod = cpo.mMethod; mArg = cpo.mArg; mValues = cpo.mValues; mExtras = cpo.mExtras; mSelection = cpo.mSelection; mSelectionArgs = cpo.mSelectionArgs; mExpectedCount = cpo.mExpectedCount; mYieldAllowed = cpo.mYieldAllowed; mExceptionAllowed = cpo.mExceptionAllowed; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mType); Uri.writeToParcel(dest, mUri); if (mMethod != null) { dest.writeInt(1); dest.writeString8(mMethod); } else { dest.writeInt(0); } if (mArg != null) { dest.writeInt(1); dest.writeString8(mArg); } else { dest.writeInt(0); } if (mValues != null) { dest.writeInt(mValues.size()); dest.writeArrayMap(mValues); } else { dest.writeInt(-1); } if (mExtras != null) { dest.writeInt(mExtras.size()); dest.writeArrayMap(mExtras); } else { dest.writeInt(-1); } if (mSelection != null) { dest.writeInt(1); dest.writeString8(mSelection); } else { dest.writeInt(0); } dest.writeSparseArray(mSelectionArgs); if (mExpectedCount != null) { dest.writeInt(1); dest.writeInt(mExpectedCount); } else { dest.writeInt(0); } dest.writeInt(mYieldAllowed ? 1 : 0); dest.writeInt(mExceptionAllowed ? 1 : 0); } /** * Create a {@link Builder} suitable for building an operation that will * invoke {@link ContentProvider#insert}. * * @param uri The {@link Uri} that is the target of the operation. */ public static @NonNull Builder newInsert(@NonNull Uri uri) { return new Builder(TYPE_INSERT, uri); } /** * Create a {@link Builder} suitable for building an operation that will * invoke {@link ContentProvider#update}. * * @param uri The {@link Uri} that is the target of the operation. */ public static @NonNull Builder newUpdate(@NonNull Uri uri) { return new Builder(TYPE_UPDATE, uri); } /** * Create a {@link Builder} suitable for building an operation that will * invoke {@link ContentProvider#delete}. * * @param uri The {@link Uri} that is the target of the operation. */ public static @NonNull Builder newDelete(@NonNull Uri uri) { return new Builder(TYPE_DELETE, uri); } /** * 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 @NonNull Builder newAssertQuery(@NonNull Uri uri) { return new Builder(TYPE_ASSERT, uri); } /** * Create a {@link Builder} suitable for building an operation that will * invoke {@link ContentProvider#call}. * * @param uri The {@link Uri} that is the target of the operation. */ public static @NonNull Builder newCall(@NonNull Uri uri, @Nullable String method, @Nullable String arg) { return new Builder(TYPE_CALL, uri, method, arg); } /** * Gets the Uri for the target of the operation. */ public @NonNull Uri getUri() { return mUri; } /** * Returns true if the operation allows yielding the database to other transactions * if the database is contended. * * @see android.database.sqlite.SQLiteDatabase#yieldIfContendedSafely() */ public boolean isYieldAllowed() { return mYieldAllowed; } /** * Returns true if this operation allows subsequent operations to continue * even if this operation throws an exception. When true, any encountered * exception is returned via {@link ContentProviderResult#exception}. */ public boolean isExceptionAllowed() { return mExceptionAllowed; } /** @hide exposed for unit tests */ @UnsupportedAppUsage public int getType() { return mType; } /** * Returns true if the operation represents a {@link ContentProvider#insert} * operation. * * @see #newInsert */ public boolean isInsert() { return mType == TYPE_INSERT; } /** * Returns true if the operation represents a {@link ContentProvider#delete} * operation. * * @see #newDelete */ public boolean isDelete() { return mType == TYPE_DELETE; } /** * Returns true if the operation represents a {@link ContentProvider#update} * operation. * * @see #newUpdate */ public boolean isUpdate() { return mType == TYPE_UPDATE; } /** * Returns true if the operation represents an assert query. * * @see #newAssertQuery */ public boolean isAssertQuery() { return mType == TYPE_ASSERT; } /** * Returns true if the operation represents a {@link ContentProvider#call} * operation. * * @see #newCall */ public boolean isCall() { return mType == TYPE_CALL; } /** * Returns true if the operation represents an insertion, deletion, or update. * * @see #isInsert * @see #isDelete * @see #isUpdate */ public boolean isWriteOperation() { return mType == TYPE_DELETE || mType == TYPE_INSERT || mType == TYPE_UPDATE; } /** * Returns true if the operation represents an assert query. * * @see #isAssertQuery */ public boolean isReadOperation() { return mType == TYPE_ASSERT; } /** * 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#withSelectionBackReference}. * @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 @NonNull ContentProviderResult apply(@NonNull ContentProvider provider, @NonNull ContentProviderResult[] backRefs, int numBackRefs) throws OperationApplicationException { if (mExceptionAllowed) { try { return applyInternal(provider, backRefs, numBackRefs); } catch (Exception e) { return new ContentProviderResult(e); } } else { return applyInternal(provider, backRefs, numBackRefs); } } private ContentProviderResult applyInternal(ContentProvider provider, ContentProviderResult[] backRefs, int numBackRefs) throws OperationApplicationException { final ContentValues values = resolveValueBackReferences(backRefs, numBackRefs); // If the creator requested explicit selection or selectionArgs, it // should take precedence over similar values they defined in extras Bundle extras = resolveExtrasBackReferences(backRefs, numBackRefs); if (mSelection != null) { extras = (extras != null) ? extras : new Bundle(); extras.putString(ContentResolver.QUERY_ARG_SQL_SELECTION, mSelection); } if (mSelectionArgs != null) { extras = (extras != null) ? extras : new Bundle(); extras.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, resolveSelectionArgsBackReferences(backRefs, numBackRefs)); } if (mType == TYPE_INSERT) { final Uri newUri = provider.insert(mUri, values, extras); if (newUri != null) { return new ContentProviderResult(newUri); } else { throw new OperationApplicationException( "Insert into " + mUri + " returned no result"); } } else if (mType == TYPE_CALL) { final Bundle res = provider.call(mUri.getAuthority(), mMethod, mArg, extras); return new ContentProviderResult(res); } final int numRows; if (mType == TYPE_DELETE) { numRows = provider.delete(mUri, extras); } else if (mType == TYPE_UPDATE) { numRows = provider.update(mUri, values, extras); } else if (mType == TYPE_ASSERT) { // Assert that all rows match expected values String[] projection = null; if (values != null) { // Build projection map from expected values final ArrayList projectionList = new ArrayList(); for (Map.Entry entry : values.valueSet()) { projectionList.add(entry.getKey()); } projection = projectionList.toArray(new String[projectionList.size()]); } final Cursor cursor = provider.query(mUri, projection, extras, null); try { numRows = cursor.getCount(); if (projection != null) { 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]); } } } } } finally { cursor.close(); } } else { throw new IllegalStateException("bad type, " + mType); } if (mExpectedCount != null && mExpectedCount != numRows) { throw new OperationApplicationException( "Expected " + mExpectedCount + " rows but actual " + numRows); } return new ContentProviderResult(numRows); } /** * Return the values for this operation after resolving any requested * back-references using the given results. * * @param backRefs the results to use when resolving any back-references * @param numBackRefs the number of results which are valid */ public @Nullable ContentValues resolveValueBackReferences( @NonNull ContentProviderResult[] backRefs, int numBackRefs) { if (mValues != null) { final ContentValues values = new ContentValues(); for (int i = 0; i < mValues.size(); i++) { final Object value = mValues.valueAt(i); final Object resolved; if (value instanceof BackReference) { resolved = ((BackReference) value).resolve(backRefs, numBackRefs); } else { resolved = value; } values.putObject(mValues.keyAt(i), resolved); } return values; } else { return null; } } /** * Return the extras for this operation after resolving any requested * back-references using the given results. * * @param backRefs the results to use when resolving any back-references * @param numBackRefs the number of results which are valid */ public @Nullable Bundle resolveExtrasBackReferences( @NonNull ContentProviderResult[] backRefs, int numBackRefs) { if (mExtras != null) { final Bundle extras = new Bundle(); for (int i = 0; i < mExtras.size(); i++) { final Object value = mExtras.valueAt(i); final Object resolved; if (value instanceof BackReference) { resolved = ((BackReference) value).resolve(backRefs, numBackRefs); } else { resolved = value; } extras.putObject(mExtras.keyAt(i), resolved); } return extras; } else { return null; } } /** * Return the selection arguments for this operation after resolving any * requested back-references using the given results. * * @param backRefs the results to use when resolving any back-references * @param numBackRefs the number of results which are valid */ public @Nullable String[] resolveSelectionArgsBackReferences( @NonNull ContentProviderResult[] backRefs, int numBackRefs) { if (mSelectionArgs != null) { int max = -1; for (int i = 0; i < mSelectionArgs.size(); i++) { max = Math.max(max, mSelectionArgs.keyAt(i)); } final String[] selectionArgs = new String[max + 1]; for (int i = 0; i < mSelectionArgs.size(); i++) { final Object value = mSelectionArgs.valueAt(i); final Object resolved; if (value instanceof BackReference) { resolved = ((BackReference) value).resolve(backRefs, numBackRefs); } else { resolved = value; } selectionArgs[mSelectionArgs.keyAt(i)] = String.valueOf(resolved); } return selectionArgs; } else { return null; } } /** {@hide} */ public static String typeToString(int type) { switch (type) { case TYPE_INSERT: return "insert"; case TYPE_UPDATE: return "update"; case TYPE_DELETE: return "delete"; case TYPE_ASSERT: return "assert"; case TYPE_CALL: return "call"; default: return Integer.toString(type); } } @Override public String toString() { final StringBuilder sb = new StringBuilder("ContentProviderOperation("); sb.append("type=" + typeToString(mType) + " "); if (mUri != null) { sb.append("uri=" + mUri + " "); } if (mValues != null) { sb.append("values=" + mValues + " "); } if (mSelection != null) { sb.append("selection=" + mSelection + " "); } if (mSelectionArgs != null) { sb.append("selectionArgs=" + mSelectionArgs + " "); } if (mExpectedCount != null) { sb.append("expectedCount=" + mExpectedCount + " "); } if (mYieldAllowed) { sb.append("yieldAllowed "); } if (mExceptionAllowed) { sb.append("exceptionAllowed "); } sb.deleteCharAt(sb.length() - 1); sb.append(")"); return sb.toString(); } @Override public int describeContents() { return 0; } public static final @android.annotation.NonNull Creator CREATOR = new Creator() { @Override public ContentProviderOperation createFromParcel(Parcel source) { return new ContentProviderOperation(source); } @Override public ContentProviderOperation[] newArray(int size) { return new ContentProviderOperation[size]; } }; /** {@hide} */ public static class BackReference implements Parcelable { private final int fromIndex; private final String fromKey; private BackReference(int fromIndex, String fromKey) { this.fromIndex = fromIndex; this.fromKey = fromKey; } public BackReference(Parcel src) { this.fromIndex = src.readInt(); if (src.readInt() != 0) { this.fromKey = src.readString8(); } else { this.fromKey = null; } } public Object resolve(ContentProviderResult[] backRefs, int numBackRefs) { if (fromIndex >= numBackRefs) { Log.e(TAG, this.toString()); throw new ArrayIndexOutOfBoundsException("asked for back ref " + fromIndex + " but there are only " + numBackRefs + " back refs"); } ContentProviderResult backRef = backRefs[fromIndex]; Object backRefValue; if (backRef.extras != null) { backRefValue = backRef.extras.get(fromKey); } else if (backRef.uri != null) { backRefValue = ContentUris.parseId(backRef.uri); } else { backRefValue = (long) backRef.count; } return backRefValue; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(fromIndex); if (fromKey != null) { dest.writeInt(1); dest.writeString8(fromKey); } else { dest.writeInt(0); } } @Override public int describeContents() { return 0; } public static final @android.annotation.NonNull Creator CREATOR = new Creator() { @Override public BackReference createFromParcel(Parcel source) { return new BackReference(source); } @Override public BackReference[] newArray(int size) { return new BackReference[size]; } }; } /** * 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#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. */ public static class Builder { private final int mType; private final Uri mUri; private final String mMethod; private final String mArg; private ArrayMap mValues; private ArrayMap mExtras; private String mSelection; private SparseArray mSelectionArgs; private Integer mExpectedCount; private boolean mYieldAllowed; private boolean mExceptionAllowed; private Builder(int type, Uri uri) { this(type, uri, null, null); } private Builder(int type, Uri uri, String method, String arg) { mType = type; mUri = Objects.requireNonNull(uri); mMethod = method; mArg = arg; } /** Create a ContentProviderOperation from this {@link Builder}. */ public @NonNull ContentProviderOperation build() { if (mType == TYPE_UPDATE) { if ((mValues == null || mValues.isEmpty())) { throw new IllegalArgumentException("Empty values"); } } if (mType == TYPE_ASSERT) { if ((mValues == null || mValues.isEmpty()) && (mExpectedCount == null)) { throw new IllegalArgumentException("Empty values"); } } return new ContentProviderOperation(this); } private void ensureValues() { if (mValues == null) { mValues = new ArrayMap<>(); } } private void ensureExtras() { if (mExtras == null) { mExtras = new ArrayMap<>(); } } private void ensureSelectionArgs() { if (mSelectionArgs == null) { mSelectionArgs = new SparseArray<>(); } } private void setValue(@NonNull String key, @NonNull Object value) { ensureValues(); final boolean oldReference = mValues.get(key) instanceof BackReference; final boolean newReference = value instanceof BackReference; if (!oldReference || newReference) { mValues.put(key, value); } } private void setExtra(@NonNull String key, @NonNull Object value) { ensureExtras(); final boolean oldReference = mExtras.get(key) instanceof BackReference; final boolean newReference = value instanceof BackReference; if (!oldReference || newReference) { mExtras.put(key, value); } } private void setSelectionArg(int index, @NonNull Object value) { ensureSelectionArgs(); final boolean oldReference = mSelectionArgs.get(index) instanceof BackReference; final boolean newReference = value instanceof BackReference; if (!oldReference || newReference) { mSelectionArgs.put(index, value); } } /** * Configure the values to use for this operation. This method will * replace any previously defined values for the contained keys, but it * will not replace any back-reference requests. *

* Any value may be dynamically overwritten using the result of a * previous operation by using methods such as * {@link #withValueBackReference(String, int)}. */ public @NonNull Builder withValues(@NonNull ContentValues values) { assertValuesAllowed(); ensureValues(); final ArrayMap rawValues = values.getValues(); for (int i = 0; i < rawValues.size(); i++) { setValue(rawValues.keyAt(i), rawValues.valueAt(i)); } return this; } /** * Configure the given value to use for this operation. This method will * replace any previously defined value for this key. * * @param key the key indicating which value to configure */ public @NonNull Builder withValue(@NonNull String key, @Nullable Object value) { assertValuesAllowed(); if (!ContentValues.isSupportedValue(value)) { throw new IllegalArgumentException("bad value type: " + value.getClass().getName()); } setValue(key, value); return this; } /** * Configure the given values to be dynamically overwritten using the * result of a previous operation. This method will replace any * previously defined values for these keys. * * @param backReferences set of values where the key indicates which * value to configure and the value the index indicating * which historical {@link ContentProviderResult} should * overwrite the value */ public @NonNull Builder withValueBackReferences(@NonNull ContentValues backReferences) { assertValuesAllowed(); final ArrayMap rawValues = backReferences.getValues(); for (int i = 0; i < rawValues.size(); i++) { setValue(rawValues.keyAt(i), new BackReference((int) rawValues.valueAt(i), null)); } return this; } /** * Configure the given value to be dynamically overwritten using the * result of a previous operation. This method will replace any * previously defined value for this key. * * @param key the key indicating which value to configure * @param fromIndex the index indicating which historical * {@link ContentProviderResult} should overwrite the value */ public @NonNull Builder withValueBackReference(@NonNull String key, int fromIndex) { assertValuesAllowed(); setValue(key, new BackReference(fromIndex, null)); return this; } /** * Configure the given value to be dynamically overwritten using the * result of a previous operation. This method will replace any * previously defined value for this key. * * @param key the key indicating which value to configure * @param fromIndex the index indicating which historical * {@link ContentProviderResult} should overwrite the value * @param fromKey the key of indicating which * {@link ContentProviderResult#extras} value should * overwrite the value */ public @NonNull Builder withValueBackReference(@NonNull String key, int fromIndex, @NonNull String fromKey) { assertValuesAllowed(); setValue(key, new BackReference(fromIndex, fromKey)); return this; } /** * Configure the extras to use for this operation. This method will * replace any previously defined values for the contained keys, but it * will not replace any back-reference requests. *

* Any value may be dynamically overwritten using the result of a * previous operation by using methods such as * {@link #withExtraBackReference(String, int)}. */ public @NonNull Builder withExtras(@NonNull Bundle extras) { assertExtrasAllowed(); ensureExtras(); for (String key : extras.keySet()) { setExtra(key, extras.get(key)); } return this; } /** * Configure the given extra to use for this operation. This method will * replace any previously defined extras for this key. * * @param key the key indicating which extra to configure */ public @NonNull Builder withExtra(@NonNull String key, @Nullable Object value) { assertExtrasAllowed(); setExtra(key, value); return this; } /** * Configure the given extra to be dynamically overwritten using the * result of a previous operation. This method will replace any * previously defined extras for this key. * * @param key the key indicating which extra to configure * @param fromIndex the index indicating which historical * {@link ContentProviderResult} should overwrite the extra */ public @NonNull Builder withExtraBackReference(@NonNull String key, int fromIndex) { assertExtrasAllowed(); setExtra(key, new BackReference(fromIndex, null)); return this; } /** * Configure the given extra to be dynamically overwritten using the * result of a previous operation. This method will replace any * previously defined extras for this key. * * @param key the key indicating which extra to configure * @param fromIndex the index indicating which historical * {@link ContentProviderResult} should overwrite the extra * @param fromKey the key of indicating which * {@link ContentProviderResult#extras} value should * overwrite the extra */ public @NonNull Builder withExtraBackReference(@NonNull String key, int fromIndex, @NonNull String fromKey) { assertExtrasAllowed(); setExtra(key, new BackReference(fromIndex, fromKey)); return this; } /** * Configure the selection and selection arguments to use for this * operation. This method will replace any previously defined selection * and selection arguments, but it will not replace any back-reference * requests. *

* An occurrence of {@code ?} in the selection will be replaced with the * corresponding selection argument when the operation is executed. *

* Any selection argument may be dynamically overwritten using the * result of a previous operation by using methods such as * {@link #withSelectionBackReference(int, int)}. */ public @NonNull Builder withSelection(@Nullable String selection, @Nullable String[] selectionArgs) { assertSelectionAllowed(); mSelection = selection; if (selectionArgs != null) { ensureSelectionArgs(); for (int i = 0; i < selectionArgs.length; i++) { setSelectionArg(i, selectionArgs[i]); } } return this; } /** * Configure the given selection argument to be dynamically overwritten * using the result of a previous operation. This method will replace * any previously defined selection argument at this index. * * @param index the index indicating which selection argument to * configure * @param fromIndex the index indicating which historical * {@link ContentProviderResult} should overwrite the * selection argument */ public @NonNull Builder withSelectionBackReference(int index, int fromIndex) { assertSelectionAllowed(); setSelectionArg(index, new BackReference(fromIndex, null)); return this; } /** * Configure the given selection argument to be dynamically overwritten * using the result of a previous operation. This method will replace * any previously defined selection argument at this index. * * @param index the index indicating which selection argument to * configure * @param fromIndex the index indicating which historical * {@link ContentProviderResult} should overwrite the * selection argument * @param fromKey the key of indicating which * {@link ContentProviderResult#extras} value should * overwrite the selection argument */ public @NonNull Builder withSelectionBackReference(int index, int fromIndex, @NonNull String fromKey) { assertSelectionAllowed(); setSelectionArg(index, new BackReference(fromIndex, fromKey)); return this; } /** * If set then if the number of rows affected by this operation does not match * this count {@link OperationApplicationException} will be throw. * This can only be used with builders of type update, delete, or assert. * @return this builder, to allow for chaining. */ public @NonNull Builder withExpectedCount(int count) { if (mType != TYPE_UPDATE && mType != TYPE_DELETE && mType != TYPE_ASSERT) { throw new IllegalArgumentException( "only updates, deletes, and asserts can have expected counts"); } mExpectedCount = count; return this; } /** * If set to true then the operation allows yielding the database to other transactions * if the database is contended. * @return this builder, to allow for chaining. * @see android.database.sqlite.SQLiteDatabase#yieldIfContendedSafely() */ public @NonNull Builder withYieldAllowed(boolean yieldAllowed) { mYieldAllowed = yieldAllowed; return this; } /** * If set to true, this operation allows subsequent operations to * continue even if this operation throws an exception. When true, any * encountered exception is returned via * {@link ContentProviderResult#exception}. */ public @NonNull Builder withExceptionAllowed(boolean exceptionAllowed) { mExceptionAllowed = exceptionAllowed; return this; } /** {@hide} */ public @NonNull Builder withFailureAllowed(boolean failureAllowed) { return withExceptionAllowed(failureAllowed); } private void assertValuesAllowed() { switch (mType) { case TYPE_INSERT: case TYPE_UPDATE: case TYPE_ASSERT: break; default: throw new IllegalArgumentException( "Values not supported for " + typeToString(mType)); } } private void assertSelectionAllowed() { switch (mType) { case TYPE_UPDATE: case TYPE_DELETE: case TYPE_ASSERT: break; default: throw new IllegalArgumentException( "Selection not supported for " + typeToString(mType)); } } private void assertExtrasAllowed() { switch (mType) { case TYPE_INSERT: case TYPE_UPDATE: case TYPE_DELETE: case TYPE_ASSERT: case TYPE_CALL: break; default: throw new IllegalArgumentException( "Extras not supported for " + typeToString(mType)); } } } }