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