1/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.content;
18
19import android.content.ContentProvider;
20import android.database.Cursor;
21import android.net.Uri;
22import android.os.Parcel;
23import android.os.Parcelable;
24import android.text.TextUtils;
25import android.util.Log;
26
27import java.util.ArrayList;
28import java.util.HashMap;
29import java.util.Map;
30
31/**
32 * Represents a single operation to be performed as part of a batch of operations.
33 *
34 * @see ContentProvider#applyBatch(ArrayList)
35 */
36public class ContentProviderOperation implements Parcelable {
37    /** @hide exposed for unit tests */
38    public final static int TYPE_INSERT = 1;
39    /** @hide exposed for unit tests */
40    public final static int TYPE_UPDATE = 2;
41    /** @hide exposed for unit tests */
42    public final static int TYPE_DELETE = 3;
43    /** @hide exposed for unit tests */
44    public final static int TYPE_ASSERT = 4;
45
46    private final int mType;
47    private final Uri mUri;
48    private final String mSelection;
49    private final String[] mSelectionArgs;
50    private final ContentValues mValues;
51    private final Integer mExpectedCount;
52    private final ContentValues mValuesBackReferences;
53    private final Map<Integer, Integer> mSelectionArgsBackReferences;
54    private final boolean mYieldAllowed;
55
56    private final static String TAG = "ContentProviderOperation";
57
58    /**
59     * Creates a {@link ContentProviderOperation} by copying the contents of a
60     * {@link Builder}.
61     */
62    private ContentProviderOperation(Builder builder) {
63        mType = builder.mType;
64        mUri = builder.mUri;
65        mValues = builder.mValues;
66        mSelection = builder.mSelection;
67        mSelectionArgs = builder.mSelectionArgs;
68        mExpectedCount = builder.mExpectedCount;
69        mSelectionArgsBackReferences = builder.mSelectionArgsBackReferences;
70        mValuesBackReferences = builder.mValuesBackReferences;
71        mYieldAllowed = builder.mYieldAllowed;
72    }
73
74    private ContentProviderOperation(Parcel source) {
75        mType = source.readInt();
76        mUri = Uri.CREATOR.createFromParcel(source);
77        mValues = source.readInt() != 0 ? ContentValues.CREATOR.createFromParcel(source) : null;
78        mSelection = source.readInt() != 0 ? source.readString() : null;
79        mSelectionArgs = source.readInt() != 0 ? source.readStringArray() : null;
80        mExpectedCount = source.readInt() != 0 ? source.readInt() : null;
81        mValuesBackReferences = source.readInt() != 0
82                ? ContentValues.CREATOR.createFromParcel(source)
83                : null;
84        mSelectionArgsBackReferences = source.readInt() != 0
85                ? new HashMap<Integer, Integer>()
86                : null;
87        if (mSelectionArgsBackReferences != null) {
88            final int count = source.readInt();
89            for (int i = 0; i < count; i++) {
90                mSelectionArgsBackReferences.put(source.readInt(), source.readInt());
91            }
92        }
93        mYieldAllowed = source.readInt() != 0;
94    }
95
96    /** @hide */
97    public ContentProviderOperation(ContentProviderOperation cpo, boolean removeUserIdFromUri) {
98        mType = cpo.mType;
99        if (removeUserIdFromUri) {
100            mUri = ContentProvider.getUriWithoutUserId(cpo.mUri);
101        } else {
102            mUri = cpo.mUri;
103        }
104        mValues = cpo.mValues;
105        mSelection = cpo.mSelection;
106        mSelectionArgs = cpo.mSelectionArgs;
107        mExpectedCount = cpo.mExpectedCount;
108        mSelectionArgsBackReferences = cpo.mSelectionArgsBackReferences;
109        mValuesBackReferences = cpo.mValuesBackReferences;
110        mYieldAllowed = cpo.mYieldAllowed;
111    }
112
113    /** @hide */
114    public ContentProviderOperation getWithoutUserIdInUri() {
115        if (ContentProvider.uriHasUserId(mUri)) {
116            return new ContentProviderOperation(this, true);
117        }
118        return this;
119    }
120
121    public void writeToParcel(Parcel dest, int flags) {
122        dest.writeInt(mType);
123        Uri.writeToParcel(dest, mUri);
124        if (mValues != null) {
125            dest.writeInt(1);
126            mValues.writeToParcel(dest, 0);
127        } else {
128            dest.writeInt(0);
129        }
130        if (mSelection != null) {
131            dest.writeInt(1);
132            dest.writeString(mSelection);
133        } else {
134            dest.writeInt(0);
135        }
136        if (mSelectionArgs != null) {
137            dest.writeInt(1);
138            dest.writeStringArray(mSelectionArgs);
139        } else {
140            dest.writeInt(0);
141        }
142        if (mExpectedCount != null) {
143            dest.writeInt(1);
144            dest.writeInt(mExpectedCount);
145        } else {
146            dest.writeInt(0);
147        }
148        if (mValuesBackReferences != null) {
149            dest.writeInt(1);
150            mValuesBackReferences.writeToParcel(dest, 0);
151        } else {
152            dest.writeInt(0);
153        }
154        if (mSelectionArgsBackReferences != null) {
155            dest.writeInt(1);
156            dest.writeInt(mSelectionArgsBackReferences.size());
157            for (Map.Entry<Integer, Integer> entry : mSelectionArgsBackReferences.entrySet()) {
158                dest.writeInt(entry.getKey());
159                dest.writeInt(entry.getValue());
160            }
161        } else {
162            dest.writeInt(0);
163        }
164        dest.writeInt(mYieldAllowed ? 1 : 0);
165    }
166
167    /**
168     * Create a {@link Builder} suitable for building an insert {@link ContentProviderOperation}.
169     * @param uri The {@link Uri} that is the target of the insert.
170     * @return a {@link Builder}
171     */
172    public static Builder newInsert(Uri uri) {
173        return new Builder(TYPE_INSERT, uri);
174    }
175
176    /**
177     * Create a {@link Builder} suitable for building an update {@link ContentProviderOperation}.
178     * @param uri The {@link Uri} that is the target of the update.
179     * @return a {@link Builder}
180     */
181    public static Builder newUpdate(Uri uri) {
182        return new Builder(TYPE_UPDATE, uri);
183    }
184
185    /**
186     * Create a {@link Builder} suitable for building a delete {@link ContentProviderOperation}.
187     * @param uri The {@link Uri} that is the target of the delete.
188     * @return a {@link Builder}
189     */
190    public static Builder newDelete(Uri uri) {
191        return new Builder(TYPE_DELETE, uri);
192    }
193
194    /**
195     * Create a {@link Builder} suitable for building a
196     * {@link ContentProviderOperation} to assert a set of values as provided
197     * through {@link Builder#withValues(ContentValues)}.
198     */
199    public static Builder newAssertQuery(Uri uri) {
200        return new Builder(TYPE_ASSERT, uri);
201    }
202
203    /**
204     * Gets the Uri for the target of the operation.
205     */
206    public Uri getUri() {
207        return mUri;
208    }
209
210    /**
211     * Returns true if the operation allows yielding the database to other transactions
212     * if the database is contended.
213     *
214     * @see android.database.sqlite.SQLiteDatabase#yieldIfContendedSafely()
215     */
216    public boolean isYieldAllowed() {
217        return mYieldAllowed;
218    }
219
220    /** @hide exposed for unit tests */
221    public int getType() {
222        return mType;
223    }
224
225    /**
226     * Returns true if the operation represents an insertion.
227     *
228     * @see #newInsert
229     */
230    public boolean isInsert() {
231        return mType == TYPE_INSERT;
232    }
233
234    /**
235     * Returns true if the operation represents a deletion.
236     *
237     * @see #newDelete
238     */
239    public boolean isDelete() {
240        return mType == TYPE_DELETE;
241    }
242
243    /**
244     * Returns true if the operation represents an update.
245     *
246     * @see #newUpdate
247     */
248    public boolean isUpdate() {
249        return mType == TYPE_UPDATE;
250    }
251
252    /**
253     * Returns true if the operation represents an assert query.
254     *
255     * @see #newAssertQuery
256     */
257    public boolean isAssertQuery() {
258        return mType == TYPE_ASSERT;
259    }
260
261    /**
262     * Returns true if the operation represents an insertion, deletion, or update.
263     *
264     * @see #isInsert
265     * @see #isDelete
266     * @see #isUpdate
267     */
268    public boolean isWriteOperation() {
269        return mType == TYPE_DELETE || mType == TYPE_INSERT || mType == TYPE_UPDATE;
270    }
271
272    /**
273     * Returns true if the operation represents an assert query.
274     *
275     * @see #isAssertQuery
276     */
277    public boolean isReadOperation() {
278        return mType == TYPE_ASSERT;
279    }
280
281    /**
282     * Applies this operation using the given provider. The backRefs array is used to resolve any
283     * back references that were requested using
284     * {@link Builder#withValueBackReferences(ContentValues)} and
285     * {@link Builder#withSelectionBackReference}.
286     * @param provider the {@link ContentProvider} on which this batch is applied
287     * @param backRefs a {@link ContentProviderResult} array that will be consulted
288     * to resolve any requested back references.
289     * @param numBackRefs the number of valid results on the backRefs array.
290     * @return a {@link ContentProviderResult} that contains either the {@link Uri} of the inserted
291     * row if this was an insert otherwise the number of rows affected.
292     * @throws OperationApplicationException thrown if either the insert fails or
293     * if the number of rows affected didn't match the expected count
294     */
295    public ContentProviderResult apply(ContentProvider provider, ContentProviderResult[] backRefs,
296            int numBackRefs) throws OperationApplicationException {
297        ContentValues values = resolveValueBackReferences(backRefs, numBackRefs);
298        String[] selectionArgs =
299                resolveSelectionArgsBackReferences(backRefs, numBackRefs);
300
301        if (mType == TYPE_INSERT) {
302            Uri newUri = provider.insert(mUri, values);
303            if (newUri == null) {
304                throw new OperationApplicationException("insert failed");
305            }
306            return new ContentProviderResult(newUri);
307        }
308
309        int numRows;
310        if (mType == TYPE_DELETE) {
311            numRows = provider.delete(mUri, mSelection, selectionArgs);
312        } else if (mType == TYPE_UPDATE) {
313            numRows = provider.update(mUri, values, mSelection, selectionArgs);
314        } else if (mType == TYPE_ASSERT) {
315            // Assert that all rows match expected values
316            String[] projection =  null;
317            if (values != null) {
318                // Build projection map from expected values
319                final ArrayList<String> projectionList = new ArrayList<String>();
320                for (Map.Entry<String, Object> entry : values.valueSet()) {
321                    projectionList.add(entry.getKey());
322                }
323                projection = projectionList.toArray(new String[projectionList.size()]);
324            }
325            final Cursor cursor = provider.query(mUri, projection, mSelection, selectionArgs, null);
326            try {
327                numRows = cursor.getCount();
328                if (projection != null) {
329                    while (cursor.moveToNext()) {
330                        for (int i = 0; i < projection.length; i++) {
331                            final String cursorValue = cursor.getString(i);
332                            final String expectedValue = values.getAsString(projection[i]);
333                            if (!TextUtils.equals(cursorValue, expectedValue)) {
334                                // Throw exception when expected values don't match
335                                Log.e(TAG, this.toString());
336                                throw new OperationApplicationException("Found value " + cursorValue
337                                        + " when expected " + expectedValue + " for column "
338                                        + projection[i]);
339                            }
340                        }
341                    }
342                }
343            } finally {
344                cursor.close();
345            }
346        } else {
347            Log.e(TAG, this.toString());
348            throw new IllegalStateException("bad type, " + mType);
349        }
350
351        if (mExpectedCount != null && mExpectedCount != numRows) {
352            Log.e(TAG, this.toString());
353            throw new OperationApplicationException("wrong number of rows: " + numRows);
354        }
355
356        return new ContentProviderResult(numRows);
357    }
358
359    /**
360     * The ContentValues back references are represented as a ContentValues object where the
361     * key refers to a column and the value is an index of the back reference whose
362     * valued should be associated with the column.
363     * <p>
364     * This is intended to be a private method but it is exposed for
365     * unit testing purposes
366     * @param backRefs an array of previous results
367     * @param numBackRefs the number of valid previous results in backRefs
368     * @return the ContentValues that should be used in this operation application after
369     * expansion of back references. This can be called if either mValues or mValuesBackReferences
370     * is null
371     */
372    public ContentValues resolveValueBackReferences(
373            ContentProviderResult[] backRefs, int numBackRefs) {
374        if (mValuesBackReferences == null) {
375            return mValues;
376        }
377        final ContentValues values;
378        if (mValues == null) {
379            values = new ContentValues();
380        } else {
381            values = new ContentValues(mValues);
382        }
383        for (Map.Entry<String, Object> entry : mValuesBackReferences.valueSet()) {
384            String key = entry.getKey();
385            Integer backRefIndex = mValuesBackReferences.getAsInteger(key);
386            if (backRefIndex == null) {
387                Log.e(TAG, this.toString());
388                throw new IllegalArgumentException("values backref " + key + " is not an integer");
389            }
390            values.put(key, backRefToValue(backRefs, numBackRefs, backRefIndex));
391        }
392        return values;
393    }
394
395    /**
396     * The Selection Arguments back references are represented as a Map of Integer->Integer where
397     * the key is an index into the selection argument array (see {@link Builder#withSelection})
398     * and the value is the index of the previous result that should be used for that selection
399     * argument array slot.
400     * <p>
401     * This is intended to be a private method but it is exposed for
402     * unit testing purposes
403     * @param backRefs an array of previous results
404     * @param numBackRefs the number of valid previous results in backRefs
405     * @return the ContentValues that should be used in this operation application after
406     * expansion of back references. This can be called if either mValues or mValuesBackReferences
407     * is null
408     */
409    public String[] resolveSelectionArgsBackReferences(
410            ContentProviderResult[] backRefs, int numBackRefs) {
411        if (mSelectionArgsBackReferences == null) {
412            return mSelectionArgs;
413        }
414        String[] newArgs = new String[mSelectionArgs.length];
415        System.arraycopy(mSelectionArgs, 0, newArgs, 0, mSelectionArgs.length);
416        for (Map.Entry<Integer, Integer> selectionArgBackRef
417                : mSelectionArgsBackReferences.entrySet()) {
418            final Integer selectionArgIndex = selectionArgBackRef.getKey();
419            final int backRefIndex = selectionArgBackRef.getValue();
420            newArgs[selectionArgIndex] =
421                    String.valueOf(backRefToValue(backRefs, numBackRefs, backRefIndex));
422        }
423        return newArgs;
424    }
425
426    @Override
427    public String toString() {
428        return "mType: " + mType + ", mUri: " + mUri +
429                ", mSelection: " + mSelection +
430                ", mExpectedCount: " + mExpectedCount +
431                ", mYieldAllowed: " + mYieldAllowed +
432                ", mValues: " + mValues +
433                ", mValuesBackReferences: " + mValuesBackReferences +
434                ", mSelectionArgsBackReferences: " + mSelectionArgsBackReferences;
435    }
436
437    /**
438     * Return the string representation of the requested back reference.
439     * @param backRefs an array of results
440     * @param numBackRefs the number of items in the backRefs array that are valid
441     * @param backRefIndex which backRef to be used
442     * @throws ArrayIndexOutOfBoundsException thrown if the backRefIndex is larger than
443     * the numBackRefs
444     * @return the string representation of the requested back reference.
445     */
446    private long backRefToValue(ContentProviderResult[] backRefs, int numBackRefs,
447            Integer backRefIndex) {
448        if (backRefIndex >= numBackRefs) {
449            Log.e(TAG, this.toString());
450            throw new ArrayIndexOutOfBoundsException("asked for back ref " + backRefIndex
451                    + " but there are only " + numBackRefs + " back refs");
452        }
453        ContentProviderResult backRef = backRefs[backRefIndex];
454        long backRefValue;
455        if (backRef.uri != null) {
456            backRefValue = ContentUris.parseId(backRef.uri);
457        } else {
458            backRefValue = backRef.count;
459        }
460        return backRefValue;
461    }
462
463    public int describeContents() {
464        return 0;
465    }
466
467    public static final Creator<ContentProviderOperation> CREATOR =
468            new Creator<ContentProviderOperation>() {
469        public ContentProviderOperation createFromParcel(Parcel source) {
470            return new ContentProviderOperation(source);
471        }
472
473        public ContentProviderOperation[] newArray(int size) {
474            return new ContentProviderOperation[size];
475        }
476    };
477
478    /**
479     * Used to add parameters to a {@link ContentProviderOperation}. The {@link Builder} is
480     * first created by calling {@link ContentProviderOperation#newInsert(android.net.Uri)},
481     * {@link ContentProviderOperation#newUpdate(android.net.Uri)},
482     * {@link ContentProviderOperation#newDelete(android.net.Uri)} or
483     * {@link ContentProviderOperation#newAssertQuery(Uri)}. The withXXX methods
484     * can then be used to add parameters to the builder. See the specific methods to find for
485     * which {@link Builder} type each is allowed. Call {@link #build} to create the
486     * {@link ContentProviderOperation} once all the parameters have been supplied.
487     */
488    public static class Builder {
489        private final int mType;
490        private final Uri mUri;
491        private String mSelection;
492        private String[] mSelectionArgs;
493        private ContentValues mValues;
494        private Integer mExpectedCount;
495        private ContentValues mValuesBackReferences;
496        private Map<Integer, Integer> mSelectionArgsBackReferences;
497        private boolean mYieldAllowed;
498
499        /** Create a {@link Builder} of a given type. The uri must not be null. */
500        private Builder(int type, Uri uri) {
501            if (uri == null) {
502                throw new IllegalArgumentException("uri must not be null");
503            }
504            mType = type;
505            mUri = uri;
506        }
507
508        /** Create a ContentProviderOperation from this {@link Builder}. */
509        public ContentProviderOperation build() {
510            if (mType == TYPE_UPDATE) {
511                if ((mValues == null || mValues.isEmpty())
512                      && (mValuesBackReferences == null || mValuesBackReferences.isEmpty())) {
513                    throw new IllegalArgumentException("Empty values");
514                }
515            }
516            if (mType == TYPE_ASSERT) {
517                if ((mValues == null || mValues.isEmpty())
518                      && (mValuesBackReferences == null || mValuesBackReferences.isEmpty())
519                        && (mExpectedCount == null)) {
520                    throw new IllegalArgumentException("Empty values");
521                }
522            }
523            return new ContentProviderOperation(this);
524        }
525
526        /**
527         * Add a {@link ContentValues} of back references. The key is the name of the column
528         * and the value is an integer that is the index of the previous result whose
529         * value should be used for the column. The value is added as a {@link String}.
530         * A column value from the back references takes precedence over a value specified in
531         * {@link #withValues}.
532         * This can only be used with builders of type insert, update, or assert.
533         * @return this builder, to allow for chaining.
534         */
535        public Builder withValueBackReferences(ContentValues backReferences) {
536            if (mType != TYPE_INSERT && mType != TYPE_UPDATE && mType != TYPE_ASSERT) {
537                throw new IllegalArgumentException(
538                        "only inserts, updates, and asserts can have value back-references");
539            }
540            mValuesBackReferences = backReferences;
541            return this;
542        }
543
544        /**
545         * Add a ContentValues back reference.
546         * A column value from the back references takes precedence over a value specified in
547         * {@link #withValues}.
548         * This can only be used with builders of type insert, update, or assert.
549         * @return this builder, to allow for chaining.
550         */
551        public Builder withValueBackReference(String key, int previousResult) {
552            if (mType != TYPE_INSERT && mType != TYPE_UPDATE && mType != TYPE_ASSERT) {
553                throw new IllegalArgumentException(
554                        "only inserts, updates, and asserts can have value back-references");
555            }
556            if (mValuesBackReferences == null) {
557                mValuesBackReferences = new ContentValues();
558            }
559            mValuesBackReferences.put(key, previousResult);
560            return this;
561        }
562
563        /**
564         * Add a back references as a selection arg. Any value at that index of the selection arg
565         * that was specified by {@link #withSelection} will be overwritten.
566         * This can only be used with builders of type update, delete, or assert.
567         * @return this builder, to allow for chaining.
568         */
569        public Builder withSelectionBackReference(int selectionArgIndex, int previousResult) {
570            if (mType != TYPE_UPDATE && mType != TYPE_DELETE && mType != TYPE_ASSERT) {
571                throw new IllegalArgumentException("only updates, deletes, and asserts "
572                        + "can have selection back-references");
573            }
574            if (mSelectionArgsBackReferences == null) {
575                mSelectionArgsBackReferences = new HashMap<Integer, Integer>();
576            }
577            mSelectionArgsBackReferences.put(selectionArgIndex, previousResult);
578            return this;
579        }
580
581        /**
582         * The ContentValues to use. This may be null. These values may be overwritten by
583         * the corresponding value specified by {@link #withValueBackReference} or by
584         * future calls to {@link #withValues} or {@link #withValue}.
585         * This can only be used with builders of type insert, update, or assert.
586         * @return this builder, to allow for chaining.
587         */
588        public Builder withValues(ContentValues values) {
589            if (mType != TYPE_INSERT && mType != TYPE_UPDATE && mType != TYPE_ASSERT) {
590                throw new IllegalArgumentException(
591                        "only inserts, updates, and asserts can have values");
592            }
593            if (mValues == null) {
594                mValues = new ContentValues();
595            }
596            mValues.putAll(values);
597            return this;
598        }
599
600        /**
601         * A value to insert or update. This value may be overwritten by
602         * the corresponding value specified by {@link #withValueBackReference}.
603         * This can only be used with builders of type insert, update, or assert.
604         * @param key the name of this value
605         * @param value the value itself. the type must be acceptable for insertion by
606         * {@link ContentValues#put}
607         * @return this builder, to allow for chaining.
608         */
609        public Builder withValue(String key, Object value) {
610            if (mType != TYPE_INSERT && mType != TYPE_UPDATE && mType != TYPE_ASSERT) {
611                throw new IllegalArgumentException("only inserts and updates can have values");
612            }
613            if (mValues == null) {
614                mValues = new ContentValues();
615            }
616            if (value == null) {
617                mValues.putNull(key);
618            } else if (value instanceof String) {
619                mValues.put(key, (String) value);
620            } else if (value instanceof Byte) {
621                mValues.put(key, (Byte) value);
622            } else if (value instanceof Short) {
623                mValues.put(key, (Short) value);
624            } else if (value instanceof Integer) {
625                mValues.put(key, (Integer) value);
626            } else if (value instanceof Long) {
627                mValues.put(key, (Long) value);
628            } else if (value instanceof Float) {
629                mValues.put(key, (Float) value);
630            } else if (value instanceof Double) {
631                mValues.put(key, (Double) value);
632            } else if (value instanceof Boolean) {
633                mValues.put(key, (Boolean) value);
634            } else if (value instanceof byte[]) {
635                mValues.put(key, (byte[]) value);
636            } else {
637                throw new IllegalArgumentException("bad value type: " + value.getClass().getName());
638            }
639            return this;
640        }
641
642        /**
643         * The selection and arguments to use. An occurrence of '?' in the selection will be
644         * replaced with the corresponding occurence of the selection argument. Any of the
645         * selection arguments may be overwritten by a selection argument back reference as
646         * specified by {@link #withSelectionBackReference}.
647         * This can only be used with builders of type update, delete, or assert.
648         * @return this builder, to allow for chaining.
649         */
650        public Builder withSelection(String selection, String[] selectionArgs) {
651            if (mType != TYPE_UPDATE && mType != TYPE_DELETE && mType != TYPE_ASSERT) {
652                throw new IllegalArgumentException(
653                        "only updates, deletes, and asserts can have selections");
654            }
655            mSelection = selection;
656            if (selectionArgs == null) {
657                mSelectionArgs = null;
658            } else {
659                mSelectionArgs = new String[selectionArgs.length];
660                System.arraycopy(selectionArgs, 0, mSelectionArgs, 0, selectionArgs.length);
661            }
662            return this;
663        }
664
665        /**
666         * If set then if the number of rows affected by this operation does not match
667         * this count {@link OperationApplicationException} will be throw.
668         * This can only be used with builders of type update, delete, or assert.
669         * @return this builder, to allow for chaining.
670         */
671        public Builder withExpectedCount(int count) {
672            if (mType != TYPE_UPDATE && mType != TYPE_DELETE && mType != TYPE_ASSERT) {
673                throw new IllegalArgumentException(
674                        "only updates, deletes, and asserts can have expected counts");
675            }
676            mExpectedCount = count;
677            return this;
678        }
679
680        /**
681         * If set to true then the operation allows yielding the database to other transactions
682         * if the database is contended.
683         * @return this builder, to allow for chaining.
684         * @see android.database.sqlite.SQLiteDatabase#yieldIfContendedSafely()
685         */
686        public Builder withYieldAllowed(boolean yieldAllowed) {
687            mYieldAllowed = yieldAllowed;
688            return this;
689        }
690    }
691}
692