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