content: /*
   * 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.UnsupportedAppUsage;
  import android.database.Cursor;
  import android.net.Uri;
  import android.os.Parcel;
  import android.os.Parcelable;
  import android.text.TextUtils;
  import android.util.Log;
  
  import java.util.ArrayList;
  import java.util.HashMap;
  import java.util.Map;
  
  /**
   * 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;
  
      @UnsupportedAppUsage
      private final int mType;
      @UnsupportedAppUsage
      private final Uri mUri;
      @UnsupportedAppUsage
      private final String mSelection;
      private final String[] mSelectionArgs;
      private final ContentValues mValues;
      private final Integer mExpectedCount;
      private final ContentValues mValuesBackReferences;
      private final Map<Integer, Integer> mSelectionArgsBackReferences;
      private final boolean mYieldAllowed;
      private final boolean mFailureAllowed;
  
      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;
          mValues = builder.mValues;
          mSelection = builder.mSelection;
          mSelectionArgs = builder.mSelectionArgs;
          mExpectedCount = builder.mExpectedCount;
          mSelectionArgsBackReferences = builder.mSelectionArgsBackReferences;
          mValuesBackReferences = builder.mValuesBackReferences;
          mYieldAllowed = builder.mYieldAllowed;
          mFailureAllowed = builder.mFailureAllowed;
      }
  
      private ContentProviderOperation(Parcel source) {
          mType = source.readInt();
          mUri = Uri.CREATOR.createFromParcel(source);
          mValues = source.readInt() != 0 ? ContentValues.CREATOR.createFromParcel(source) : null;
          mSelection = source.readInt() != 0 ? source.readString() : null;
          mSelectionArgs = source.readInt() != 0 ? source.readStringArray() : null;
          mExpectedCount = source.readInt() != 0 ? source.readInt() : null;
          mValuesBackReferences = source.readInt() != 0
                  ? ContentValues.CREATOR.createFromParcel(source)
                  : null;
          mSelectionArgsBackReferences = source.readInt() != 0
                  ? new HashMap<Integer, Integer>()
                  : null;
          if (mSelectionArgsBackReferences != null) {
              final int count = source.readInt();
              for (int i = 0; i < count; i++) {
                  mSelectionArgsBackReferences.put(source.readInt(), source.readInt());
              }
          }
          mYieldAllowed = source.readInt() != 0;
          mFailureAllowed = source.readInt() != 0;
      }
  
      /** @hide */
      public ContentProviderOperation(ContentProviderOperation cpo, Uri withUri) {
          mType = cpo.mType;
          mUri = withUri;
          mValues = cpo.mValues;
          mSelection = cpo.mSelection;
          mSelectionArgs = cpo.mSelectionArgs;
          mExpectedCount = cpo.mExpectedCount;
          mSelectionArgsBackReferences = cpo.mSelectionArgsBackReferences;
          mValuesBackReferences = cpo.mValuesBackReferences;
          mYieldAllowed = cpo.mYieldAllowed;
          mFailureAllowed = cpo.mFailureAllowed;
      }
  
      public void writeToParcel(Parcel dest, int flags) {
          dest.writeInt(mType);
          Uri.writeToParcel(dest, mUri);
          if (mValues != null) {
              dest.writeInt(1);
              mValues.writeToParcel(dest, 0);
          } else {
              dest.writeInt(0);
          }
          if (mSelection != null) {
              dest.writeInt(1);
              dest.writeString(mSelection);
          } else {
              dest.writeInt(0);
          }
          if (mSelectionArgs != null) {
              dest.writeInt(1);
              dest.writeStringArray(mSelectionArgs);
          } else {
              dest.writeInt(0);
          }
          if (mExpectedCount != null) {
              dest.writeInt(1);
              dest.writeInt(mExpectedCount);
          } else {
              dest.writeInt(0);
          }
          if (mValuesBackReferences != null) {
              dest.writeInt(1);
              mValuesBackReferences.writeToParcel(dest, 0);
          } else {
              dest.writeInt(0);
          }
          if (mSelectionArgsBackReferences != null) {
              dest.writeInt(1);
              dest.writeInt(mSelectionArgsBackReferences.size());
              for (Map.Entry<Integer, Integer> entry : mSelectionArgsBackReferences.entrySet()) {
                  dest.writeInt(entry.getKey());
                  dest.writeInt(entry.getValue());
              }
          } else {
              dest.writeInt(0);
          }
          dest.writeInt(mYieldAllowed ? 1 : 0);
          dest.writeInt(mFailureAllowed ? 1 : 0);
      }
  
      /**
       * Create a {@link Builder} suitable for building an insert {@link ContentProviderOperation}.
       * @param uri The {@link Uri} that is the target of the insert.
       * @return a {@link Builder}
       */
      public static Builder newInsert(Uri uri) {
          return new Builder(TYPE_INSERT, uri);
      }
  
      /**
       * Create a {@link Builder} suitable for building an update {@link ContentProviderOperation}.
       * @param uri The {@link Uri} that is the target of the update.
       * @return a {@link Builder}
       */
      public static Builder newUpdate(Uri uri) {
          return new Builder(TYPE_UPDATE, uri);
      }
  
      /**
       * Create a {@link Builder} suitable for building a delete {@link ContentProviderOperation}.
       * @param uri The {@link Uri} that is the target of the delete.
       * @return a {@link Builder}
       */
      public static Builder newDelete(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 Builder newAssertQuery(Uri uri) {
          return new Builder(TYPE_ASSERT, uri);
      }
  
      /**
       * Gets the Uri for the target of the operation.
       */
      public 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;
      }
  
      /** {@hide} */
      public boolean isFailureAllowed() {
          return mFailureAllowed;
      }
  
      /** @hide exposed for unit tests */
      @UnsupportedAppUsage
      public int getType() {
          return mType;
      }
  
      /**
       * Returns true if the operation represents an insertion.
       *
       * @see #newInsert
       */
      public boolean isInsert() {
          return mType == TYPE_INSERT;
      }
  
      /**
       * Returns true if the operation represents a deletion.
       *
       * @see #newDelete
       */
      public boolean isDelete() {
          return mType == TYPE_DELETE;
      }
  
      /**
       * Returns true if the operation represents an update.
       *
       * @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 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 ContentProviderResult apply(ContentProvider provider, ContentProviderResult[] backRefs,
              int numBackRefs) throws OperationApplicationException {
          if (mFailureAllowed) {
              try {
                  return applyInternal(provider, backRefs, numBackRefs);
              } catch (Exception e) {
                  return new ContentProviderResult(e.getMessage());
              }
          } else {
              return applyInternal(provider, backRefs, numBackRefs);
          }
      }
  
      private ContentProviderResult applyInternal(ContentProvider provider,
              ContentProviderResult[] backRefs, int numBackRefs)
              throws OperationApplicationException {
          ContentValues values = resolveValueBackReferences(backRefs, numBackRefs);
          String[] selectionArgs =
                  resolveSelectionArgsBackReferences(backRefs, numBackRefs);
  
          if (mType == TYPE_INSERT) {
              final Uri newUri = provider.insert(mUri, values);
              if (newUri != null) {
                  return new ContentProviderResult(newUri);
              } else {
                  throw new OperationApplicationException(
                          "Insert into " + mUri + " returned no result");
              }
          }
  
          final int numRows;
          if (mType == TYPE_DELETE) {
              numRows = provider.delete(mUri, mSelection, selectionArgs);
          } else if (mType == TYPE_UPDATE) {
              numRows = provider.update(mUri, values, mSelection, selectionArgs);
          } 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, mSelection, selectionArgs, 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);
      }
  
      /**
       * The ContentValues back references are represented as a ContentValues object where the
       * key refers to a column and the value is an index of the back reference whose
       * valued should be associated with the column.
       * <p>
       * This is intended to be a private method but it is exposed for
       * unit testing purposes
       * @param backRefs an array of previous results
       * @param numBackRefs the number of valid previous results in backRefs
       * @return the ContentValues that should be used in this operation application after
       * expansion of back references. This can be called if either mValues or mValuesBackReferences
       * is null
       */
      public ContentValues resolveValueBackReferences(
              ContentProviderResult[] backRefs, int numBackRefs) {
          if (mValuesBackReferences == null) {
              return mValues;
          }
          final ContentValues values;
          if (mValues == null) {
              values = new ContentValues();
          } else {
              values = new ContentValues(mValues);
          }
          for (Map.Entry<String, Object> entry : mValuesBackReferences.valueSet()) {
              String key = entry.getKey();
              Integer backRefIndex = mValuesBackReferences.getAsInteger(key);
              if (backRefIndex == null) {
                  Log.e(TAG, this.toString());
                  throw new IllegalArgumentException("values backref " + key + " is not an integer");
              }
              values.put(key, backRefToValue(backRefs, numBackRefs, backRefIndex));
          }
          return values;
      }
  
      /**
       * The Selection Arguments back references are represented as a Map of Integer->Integer where
       * the key is an index into the selection argument array (see {@link Builder#withSelection})
       * and the value is the index of the previous result that should be used for that selection
       * argument array slot.
       * <p>
       * This is intended to be a private method but it is exposed for
       * unit testing purposes
       * @param backRefs an array of previous results
       * @param numBackRefs the number of valid previous results in backRefs
       * @return the ContentValues that should be used in this operation application after
       * expansion of back references. This can be called if either mValues or mValuesBackReferences
       * is null
       */
      public String[] resolveSelectionArgsBackReferences(
              ContentProviderResult[] backRefs, int numBackRefs) {
          if (mSelectionArgsBackReferences == null) {
              return mSelectionArgs;
          }
          String[] newArgs = new String[mSelectionArgs.length];
          System.arraycopy(mSelectionArgs, 0, newArgs, 0, mSelectionArgs.length);
          for (Map.Entry<Integer, Integer> selectionArgBackRef
                  : mSelectionArgsBackReferences.entrySet()) {
              final Integer selectionArgIndex = selectionArgBackRef.getKey();
              final int backRefIndex = selectionArgBackRef.getValue();
              newArgs[selectionArgIndex] =
                      String.valueOf(backRefToValue(backRefs, numBackRefs, backRefIndex));
          }
          return newArgs;
      }
  
      @Override
      public String toString() {
          return "mType: " + mType + ", mUri: " + mUri +
                  ", mSelection: " + mSelection +
                  ", mExpectedCount: " + mExpectedCount +
                  ", mYieldAllowed: " + mYieldAllowed +
                  ", mValues: " + mValues +
                  ", mValuesBackReferences: " + mValuesBackReferences +
                  ", mSelectionArgsBackReferences: " + mSelectionArgsBackReferences;
      }
  
      /**
       * Return the string representation of the requested back reference.
       * @param backRefs an array of results
       * @param numBackRefs the number of items in the backRefs array that are valid
       * @param backRefIndex which backRef to be used
       * @throws ArrayIndexOutOfBoundsException thrown if the backRefIndex is larger than
       * the numBackRefs
       * @return the string representation of the requested back reference.
       */
      private long backRefToValue(ContentProviderResult[] backRefs, int numBackRefs,
              Integer backRefIndex) {
          if (backRefIndex >= numBackRefs) {
              Log.e(TAG, this.toString());
              throw new ArrayIndexOutOfBoundsException("asked for back ref " + backRefIndex
                      + " but there are only " + numBackRefs + " back refs");
          }
          ContentProviderResult backRef = backRefs[backRefIndex];
          long backRefValue;
          if (backRef.uri != null) {
              backRefValue = ContentUris.parseId(backRef.uri);
          } else {
              backRefValue = backRef.count;
          }
          return backRefValue;
      }
  
      public int describeContents() {
          return 0;
      }
  
      public static final @android.annotation.NonNull Creator<ContentProviderOperation> CREATOR =
              new Creator<ContentProviderOperation>() {
          public ContentProviderOperation createFromParcel(Parcel source) {
              return new ContentProviderOperation(source);
          }
  
          public ContentProviderOperation[] newArray(int size) {
              return new ContentProviderOperation[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 String mSelection;
          private String[] mSelectionArgs;
          private ContentValues mValues;
          private Integer mExpectedCount;
          private ContentValues mValuesBackReferences;
          private Map<Integer, Integer> mSelectionArgsBackReferences;
          private boolean mYieldAllowed;
          private boolean mFailureAllowed;
  
          /** Create a {@link Builder} of a given type. The uri must not be null. */
          private Builder(int type, Uri uri) {
              if (uri == null) {
                  throw new IllegalArgumentException("uri must not be null");
              }
              mType = type;
              mUri = uri;
          }
  
          /** Create a ContentProviderOperation from this {@link Builder}. */
          public ContentProviderOperation build() {
              if (mType == TYPE_UPDATE) {
                  if ((mValues == null || mValues.isEmpty())
                        && (mValuesBackReferences == null || mValuesBackReferences.isEmpty())) {
                      throw new IllegalArgumentException("Empty values");
                  }
              }
              if (mType == TYPE_ASSERT) {
                  if ((mValues == null || mValues.isEmpty())
                        && (mValuesBackReferences == null || mValuesBackReferences.isEmpty())
                          && (mExpectedCount == null)) {
                      throw new IllegalArgumentException("Empty values");
                  }
              }
              return new ContentProviderOperation(this);
          }
  
          /**
           * Add a {@link ContentValues} of back references. The key is the name of the column
           * and the value is an integer that is the index of the previous result whose
           * value should be used for the column. The value is added as a {@link String}.
           * A column value from the back references takes precedence over a value specified in
           * {@link #withValues}.
           * This can only be used with builders of type insert, update, or assert.
           * @return this builder, to allow for chaining.
           */
          public Builder withValueBackReferences(ContentValues backReferences) {
              if (mType != TYPE_INSERT && mType != TYPE_UPDATE && mType != TYPE_ASSERT) {
                  throw new IllegalArgumentException(
                          "only inserts, updates, and asserts can have value back-references");
              }
              mValuesBackReferences = backReferences;
              return this;
          }
  
          /**
           * Add a ContentValues back reference.
           * A column value from the back references takes precedence over a value specified in
           * {@link #withValues}.
           * This can only be used with builders of type insert, update, or assert.
           * @return this builder, to allow for chaining.
           */
          public Builder withValueBackReference(String key, int previousResult) {
              if (mType != TYPE_INSERT && mType != TYPE_UPDATE && mType != TYPE_ASSERT) {
                  throw new IllegalArgumentException(
                          "only inserts, updates, and asserts can have value back-references");
              }
              if (mValuesBackReferences == null) {
                  mValuesBackReferences = new ContentValues();
              }
              mValuesBackReferences.put(key, previousResult);
              return this;
          }
  
          /**
           * Add a back references as a selection arg. Any value at that index of the selection arg
           * that was specified by {@link #withSelection} will be overwritten.
           * This can only be used with builders of type update, delete, or assert.
           * @return this builder, to allow for chaining.
           */
          public Builder withSelectionBackReference(int selectionArgIndex, int previousResult) {
              if (mType != TYPE_UPDATE && mType != TYPE_DELETE && mType != TYPE_ASSERT) {
                  throw new IllegalArgumentException("only updates, deletes, and asserts "
                          + "can have selection back-references");
              }
              if (mSelectionArgsBackReferences == null) {
                  mSelectionArgsBackReferences = new HashMap<Integer, Integer>();
              }
              mSelectionArgsBackReferences.put(selectionArgIndex, previousResult);
              return this;
          }
  
          /**
           * The ContentValues to use. This may be null. These values may be overwritten by
           * the corresponding value specified by {@link #withValueBackReference} or by
           * future calls to {@link #withValues} or {@link #withValue}.
           * This can only be used with builders of type insert, update, or assert.
           * @return this builder, to allow for chaining.
           */
          public Builder withValues(ContentValues values) {
              if (mType != TYPE_INSERT && mType != TYPE_UPDATE && mType != TYPE_ASSERT) {
                  throw new IllegalArgumentException(
                          "only inserts, updates, and asserts can have values");
              }
              if (mValues == null) {
                  mValues = new ContentValues();
              }
              mValues.putAll(values);
              return this;
          }
  
          /**
           * A value to insert or update. This value may be overwritten by
           * the corresponding value specified by {@link #withValueBackReference}.
           * This can only be used with builders of type insert, update, or assert.
           * @param key the name of this value
           * @param value the value itself. the type must be acceptable for insertion by
           * {@link ContentValues#put}
           * @return this builder, to allow for chaining.
           */
          public Builder withValue(String key, Object value) {
              if (mType != TYPE_INSERT && mType != TYPE_UPDATE && mType != TYPE_ASSERT) {
                  throw new IllegalArgumentException("only inserts and updates can have values");
              }
              if (mValues == null) {
                  mValues = new ContentValues();
              }
              if (value == null) {
                  mValues.putNull(key);
              } else if (value instanceof String) {
                  mValues.put(key, (String) value);
              } else if (value instanceof Byte) {
                  mValues.put(key, (Byte) value);
              } else if (value instanceof Short) {
                  mValues.put(key, (Short) value);
              } else if (value instanceof Integer) {
                  mValues.put(key, (Integer) value);
              } else if (value instanceof Long) {
                  mValues.put(key, (Long) value);
              } else if (value instanceof Float) {
                  mValues.put(key, (Float) value);
              } else if (value instanceof Double) {
                  mValues.put(key, (Double) value);
              } else if (value instanceof Boolean) {
                  mValues.put(key, (Boolean) value);
              } else if (value instanceof byte[]) {
                  mValues.put(key, (byte[]) value);
              } else {
                  throw new IllegalArgumentException("bad value type: " + value.getClass().getName());
              }
              return this;
          }
  
          /**
           * The selection and arguments to use. An occurrence of '?' in the selection will be
           * replaced with the corresponding occurrence of the selection argument. Any of the
           * selection arguments may be overwritten by a selection argument back reference as
           * specified by {@link #withSelectionBackReference}.
           * This can only be used with builders of type update, delete, or assert.
           * @return this builder, to allow for chaining.
           */
          public Builder withSelection(String selection, String[] selectionArgs) {
              if (mType != TYPE_UPDATE && mType != TYPE_DELETE && mType != TYPE_ASSERT) {
                  throw new IllegalArgumentException(
                          "only updates, deletes, and asserts can have selections");
              }
              mSelection = selection;
              if (selectionArgs == null) {
                  mSelectionArgs = null;
              } else {
                  mSelectionArgs = new String[selectionArgs.length];
                  System.arraycopy(selectionArgs, 0, mSelectionArgs, 0, selectionArgs.length);
              }
              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 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 Builder withYieldAllowed(boolean yieldAllowed) {
              mYieldAllowed = yieldAllowed;
              return this;
          }
  
          /** {@hide} */
          public Builder withFailureAllowed(boolean failureAllowed) {
              mFailureAllowed = failureAllowed;
              return this;
          }
      }
  }
  