/*
 * 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<String, Object> mValues;
    private final ArrayMap<String, Object> mExtras;
    @UnsupportedAppUsage
    private final String mSelection;
    private final SparseArray<Object> 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, java.lang.Object.class);
        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<String> projectionList = new ArrayList<String>();
                for (Map.Entry<String, Object> 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=").append(typeToString(mType)).append(' ');
        if (mUri != null) {
            sb.append("uri=").append(mUri).append(' ');
        }
        if (mValues != null) {
            sb.append("values=").append(mValues).append(' ');
        }
        if (mSelection != null) {
            sb.append("selection=").append(mSelection).append(' ');
        }
        if (mSelectionArgs != null) {
            sb.append("selectionArgs=").append(mSelectionArgs).append(' ');
        }
        if (mExpectedCount != null) {
            sb.append("expectedCount=").append(mExpectedCount).append(' ');
        }
        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<ContentProviderOperation> CREATOR =
            new Creator<ContentProviderOperation>() {
        @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<BackReference> CREATOR =
                new Creator<BackReference>() {
            @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<String, Object> mValues;
        private ArrayMap<String, Object> mExtras;
        private String mSelection;
        private SparseArray<Object> 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.
         * <p>
         * 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<String, Object> 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<String, Object> 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.
         * <p>
         * 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.
         * <p>
         * An occurrence of {@code ?} in the selection will be replaced with the
         * corresponding selection argument when the operation is executed.
         * <p>
         * 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));
            }
        }
    }
}
