content: /*
   * Copyright (C) 2017 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.service.autofill;
  
  import static android.view.autofill.Helper.sDebug;
  
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.annotation.TestApi;
  import android.app.Activity;
  import android.app.PendingIntent;
  import android.os.Parcel;
  import android.os.Parcelable;
  import android.util.Pair;
  import android.util.SparseArray;
  import android.widget.RemoteViews;
  
  import com.android.internal.util.Preconditions;
  
  import java.util.ArrayList;
  
  /**
   * Defines a custom description for the autofill save UI.
   *
   * <p>This is useful when the autofill service needs to show a detailed view of what would be saved;
   * for example, when the screen contains a credit card, it could display a logo of the credit card
   * bank, the last four digits of the credit card number, and its expiration number.
   *
   * <p>A custom description is made of 2 parts:
   * <ul>
   *   <li>A {@link RemoteViews presentation template} containing children views.
   *   <li>{@link Transformation Transformations} to populate the children views.
   * </ul>
   *
   * <p>For the credit card example mentioned above, the (simplified) template would be:
   *
   * <pre class="prettyprint">
   * &lt;LinearLayout&gt;
   *   &lt;ImageView android:id="@+id/templateccLogo"/&gt;
   *   &lt;TextView android:id="@+id/templateCcNumber"/&gt;
   *   &lt;TextView android:id="@+id/templateExpDate"/&gt;
   * &lt;/LinearLayout&gt;
   * </pre>
   *
   * <p>Which in code translates to:
   *
   * <pre class="prettyprint">
   *   CustomDescription.Builder buider = new Builder(new RemoteViews(pgkName, R.layout.cc_template);
   * </pre>
   *
   * <p>Then the value of each of the 3 children would be changed at runtime based on the the value of
   * the screen fields and the {@link Transformation Transformations}:
   *
   * <pre class="prettyprint">
   * // Image child - different logo for each bank, based on credit card prefix
   * builder.addChild(R.id.templateccLogo,
   *   new ImageTransformation.Builder(ccNumberId)
   *     .addOption(Pattern.compile("^4815.*$"), R.drawable.ic_credit_card_logo1)
   *     .addOption(Pattern.compile("^1623.*$"), R.drawable.ic_credit_card_logo2)
   *     .addOption(Pattern.compile("^42.*$"), R.drawable.ic_credit_card_logo3)
   *     .build();
   * // Masked credit card number (as .....LAST_4_DIGITS)
   * builder.addChild(R.id.templateCcNumber, new CharSequenceTransformation
   *     .Builder(ccNumberId, Pattern.compile("^.*(\\d\\d\\d\\d)$"), "...$1")
   *     .build();
   * // Expiration date as MM / YYYY:
   * builder.addChild(R.id.templateExpDate, new CharSequenceTransformation
   *     .Builder(ccExpMonthId, Pattern.compile("^(\\d\\d)$"), "Exp: $1")
   *     .addField(ccExpYearId, Pattern.compile("^(\\d\\d)$"), "/$1")
   *     .build();
   * </pre>
   *
   * <p>See {@link ImageTransformation}, {@link CharSequenceTransformation} for more info about these
   * transformations.
   */
  public final class CustomDescription implements Parcelable {
  
      private final RemoteViews mPresentation;
      private final ArrayList<Pair<Integer, InternalTransformation>> mTransformations;
      private final ArrayList<Pair<InternalValidator, BatchUpdates>> mUpdates;
      private final SparseArray<InternalOnClickAction> mActions;
  
      private CustomDescription(Builder builder) {
          mPresentation = builder.mPresentation;
          mTransformations = builder.mTransformations;
          mUpdates = builder.mUpdates;
          mActions = builder.mActions;
      }
  
      /** @hide */
      @Nullable
      public RemoteViews getPresentation() {
          return mPresentation;
      }
  
      /** @hide */
      @Nullable
      public ArrayList<Pair<Integer, InternalTransformation>> getTransformations() {
          return mTransformations;
      }
  
      /** @hide */
      @Nullable
      public ArrayList<Pair<InternalValidator, BatchUpdates>> getUpdates() {
          return mUpdates;
      }
  
      /** @hide */
      @Nullable
      @TestApi
      public SparseArray<InternalOnClickAction> getActions() {
          return mActions;
      }
  
      /**
       * Builder for {@link CustomDescription} objects.
       */
      public static class Builder {
          private final RemoteViews mPresentation;
  
          private boolean mDestroyed;
          private ArrayList<Pair<Integer, InternalTransformation>> mTransformations;
          private ArrayList<Pair<InternalValidator, BatchUpdates>> mUpdates;
          private SparseArray<InternalOnClickAction> mActions;
  
          /**
           * Default constructor.
           *
           * <p><b>Note:</b> If any child view of presentation triggers a
           * {@link RemoteViews#setOnClickPendingIntent(int, android.app.PendingIntent) pending intent
           * on click}, such {@link PendingIntent} must follow the restrictions below, otherwise
           * it might not be triggered or the autofill save UI might not be shown when its activity
           * is finished:
           * <ul>
           *   <li>It cannot be created with the {@link PendingIntent#FLAG_IMMUTABLE} flag.
           *   <li>It must be a PendingIntent for an {@link Activity}.
           *   <li>The activity must call {@link Activity#finish()} when done.
           *   <li>The activity should not launch other activities.
           * </ul>
           *
           * @param parentPresentation template presentation with (optional) children views.
           * @throws NullPointerException if {@code parentPresentation} is null (on Android
           * {@link android.os.Build.VERSION_CODES#P} or higher).
           */
          public Builder(@NonNull RemoteViews parentPresentation) {
              mPresentation = Preconditions.checkNotNull(parentPresentation);
          }
  
          /**
           * Adds a transformation to replace the value of a child view with the fields in the
           * screen.
           *
           * <p>When multiple transformations are added for the same child view, they will be applied
           * in the same order as added.
           *
           * @param id view id of the children view.
           * @param transformation an implementation provided by the Android System.
           *
           * @return this builder.
           *
           * @throws IllegalArgumentException if {@code transformation} is not a class provided
           * by the Android System.
           * @throws IllegalStateException if {@link #build()} was already called.
           */
          @NonNull
          public Builder addChild(int id, @NonNull Transformation transformation) {
              throwIfDestroyed();
              Preconditions.checkArgument((transformation instanceof InternalTransformation),
                      "not provided by Android System: " + transformation);
              if (mTransformations == null) {
                  mTransformations = new ArrayList<>();
              }
              mTransformations.add(new Pair<>(id, (InternalTransformation) transformation));
              return this;
          }
  
          /**
           * Updates the {@link RemoteViews presentation template} when a condition is satisfied by
           * applying a series of remote view operations. This allows dynamic customization of the
           * portion of the save UI that is controlled by the autofill service. Such dynamic
           * customization is based on the content of target views.
           *
           * <p>The updates are applied in the sequence they are added, after the
           * {@link #addChild(int, Transformation) transformations} are applied to the children
           * views.
           *
           * <p>For example, to make children views visible when fields are not empty:
           *
           * <pre class="prettyprint">
           * RemoteViews template = new RemoteViews(pgkName, R.layout.my_full_template);
           *
           * Pattern notEmptyPattern = Pattern.compile(".+");
           * Validator hasAddress = new RegexValidator(addressAutofillId, notEmptyPattern);
           * Validator hasCcNumber = new RegexValidator(ccNumberAutofillId, notEmptyPattern);
           *
           * RemoteViews addressUpdates = new RemoteViews(pgkName, R.layout.my_full_template)
           * addressUpdates.setViewVisibility(R.id.address, View.VISIBLE);
           *
           * // Make address visible
           * BatchUpdates addressBatchUpdates = new BatchUpdates.Builder()
           *     .updateTemplate(addressUpdates)
           *     .build();
           *
           * RemoteViews ccUpdates = new RemoteViews(pgkName, R.layout.my_full_template)
           * ccUpdates.setViewVisibility(R.id.cc_number, View.VISIBLE);
           *
           * // Mask credit card number (as .....LAST_4_DIGITS) and make it visible
           * BatchUpdates ccBatchUpdates = new BatchUpdates.Builder()
           *     .updateTemplate(ccUpdates)
           *     .transformChild(R.id.templateCcNumber, new CharSequenceTransformation
           *                     .Builder(ccNumberId, Pattern.compile("^.*(\\d\\d\\d\\d)$"), "...$1")
           *                     .build())
           *     .build();
           *
           * CustomDescription customDescription = new CustomDescription.Builder(template)
           *     .batchUpdate(hasAddress, addressBatchUpdates)
           *     .batchUpdate(hasCcNumber, ccBatchUpdates)
           *     .build();
           * </pre>
           *
           * <p>Another approach is to add a child first, then apply the transformations. Example:
           *
           * <pre class="prettyprint">
           * RemoteViews template = new RemoteViews(pgkName, R.layout.my_base_template);
           *
           * RemoteViews addressPresentation = new RemoteViews(pgkName, R.layout.address)
           * RemoteViews addressUpdates = new RemoteViews(pgkName, R.layout.my_template)
           * addressUpdates.addView(R.id.parentId, addressPresentation);
           * BatchUpdates addressBatchUpdates = new BatchUpdates.Builder()
           *     .updateTemplate(addressUpdates)
           *     .build();
           *
           * RemoteViews ccPresentation = new RemoteViews(pgkName, R.layout.cc)
           * RemoteViews ccUpdates = new RemoteViews(pgkName, R.layout.my_template)
           * ccUpdates.addView(R.id.parentId, ccPresentation);
           * BatchUpdates ccBatchUpdates = new BatchUpdates.Builder()
           *     .updateTemplate(ccUpdates)
           *     .transformChild(R.id.templateCcNumber, new CharSequenceTransformation
           *                     .Builder(ccNumberId, Pattern.compile("^.*(\\d\\d\\d\\d)$"), "...$1")
           *                     .build())
           *     .build();
           *
           * CustomDescription customDescription = new CustomDescription.Builder(template)
           *     .batchUpdate(hasAddress, addressBatchUpdates)
           *     .batchUpdate(hasCcNumber, ccBatchUpdates)
           *     .build();
           * </pre>
           *
           * @param condition condition used to trigger the updates.
           * @param updates actions to be applied to the
           * {@link #CustomDescription.Builder(RemoteViews) template presentation} when the condition
           * is satisfied.
           *
           * @return this builder
           *
           * @throws IllegalArgumentException if {@code condition} is not a class provided
           * by the Android System.
           * @throws IllegalStateException if {@link #build()} was already called.
           */
          @NonNull
          public Builder batchUpdate(@NonNull Validator condition, @NonNull BatchUpdates updates) {
              throwIfDestroyed();
              Preconditions.checkArgument((condition instanceof InternalValidator),
                      "not provided by Android System: " + condition);
              Preconditions.checkNotNull(updates);
              if (mUpdates == null) {
                  mUpdates = new ArrayList<>();
              }
              mUpdates.add(new Pair<>((InternalValidator) condition, updates));
              return this;
          }
  
          /**
           * Sets an action to be applied to the {@link RemoteViews presentation template} when the
           * child view with the given {@code id} is clicked.
           *
           * <p>Typically used when the presentation uses a masked field (like {@code ****}) for
           * sensitive fields like passwords or credit cards numbers, but offers a an icon that the
           * user can tap to show the value for that field.
           *
           * <p>Example:
           *
           * <pre class="prettyprint">
           * customDescriptionBuilder
           *   .addChild(R.id.password_plain, new CharSequenceTransformation
           *      .Builder(passwordId, Pattern.compile("^(.*)$"), "$1").build())
           *   .addOnClickAction(R.id.showIcon, new VisibilitySetterAction
           *     .Builder(R.id.hideIcon, View.VISIBLE)
           *     .setVisibility(R.id.showIcon, View.GONE)
           *     .setVisibility(R.id.password_plain, View.VISIBLE)
           *     .setVisibility(R.id.password_masked, View.GONE)
           *     .build())
           *   .addOnClickAction(R.id.hideIcon, new VisibilitySetterAction
           *     .Builder(R.id.showIcon, View.VISIBLE)
           *     .setVisibility(R.id.hideIcon, View.GONE)
           *     .setVisibility(R.id.password_masked, View.VISIBLE)
           *     .setVisibility(R.id.password_plain, View.GONE)
           *     .build());
           * </pre>
           *
           * <p><b>Note:</b> Currently only one action can be applied to a child; if this method
           * is called multiple times passing the same {@code id}, only the last call will be used.
           *
           * @param id resource id of the child view.
           * @param action action to be performed. Must be an an implementation provided by the
           * Android System.
           *
           * @return this builder
           *
           * @throws IllegalArgumentException if {@code action} is not a class provided
           * by the Android System.
           * @throws IllegalStateException if {@link #build()} was already called.
           */
          @NonNull
          public Builder addOnClickAction(int id, @NonNull OnClickAction action) {
              throwIfDestroyed();
              Preconditions.checkArgument((action instanceof InternalOnClickAction),
                      "not provided by Android System: " + action);
              if (mActions == null) {
                  mActions = new SparseArray<InternalOnClickAction>();
              }
              mActions.put(id, (InternalOnClickAction) action);
  
              return this;
          }
  
          /**
           * Creates a new {@link CustomDescription} instance.
           */
          @NonNull
          public CustomDescription build() {
              throwIfDestroyed();
              mDestroyed = true;
              return new CustomDescription(this);
          }
  
          private void throwIfDestroyed() {
              if (mDestroyed) {
                  throw new IllegalStateException("Already called #build()");
              }
          }
      }
  
      /////////////////////////////////////
      // Object "contract" methods. //
      /////////////////////////////////////
      @Override
      public String toString() {
          if (!sDebug) return super.toString();
  
          return new StringBuilder("CustomDescription: [presentation=")
                  .append(mPresentation)
                  .append(", transformations=")
                      .append(mTransformations == null ? "N/A" : mTransformations.size())
                  .append(", updates=")
                      .append(mUpdates == null ? "N/A" : mUpdates.size())
                  .append(", actions=")
                      .append(mActions == null ? "N/A" : mActions.size())
                  .append("]").toString();
      }
  
      /////////////////////////////////////
      // Parcelable "contract" methods. //
      /////////////////////////////////////
      @Override
      public int describeContents() {
          return 0;
      }
  
      @Override
      public void writeToParcel(Parcel dest, int flags) {
          dest.writeParcelable(mPresentation, flags);
          if (mPresentation == null) return;
  
          if (mTransformations == null) {
              dest.writeIntArray(null);
          } else {
              final int size = mTransformations.size();
              final int[] ids = new int[size];
              final InternalTransformation[] values = new InternalTransformation[size];
              for (int i = 0; i < size; i++) {
                  final Pair<Integer, InternalTransformation> pair = mTransformations.get(i);
                  ids[i] = pair.first;
                  values[i] = pair.second;
              }
              dest.writeIntArray(ids);
              dest.writeParcelableArray(values, flags);
          }
          if (mUpdates == null) {
              dest.writeParcelableArray(null, flags);
          } else {
              final int size = mUpdates.size();
              final InternalValidator[] conditions = new InternalValidator[size];
              final BatchUpdates[] updates = new BatchUpdates[size];
  
              for (int i = 0; i < size; i++) {
                  final Pair<InternalValidator, BatchUpdates> pair = mUpdates.get(i);
                  conditions[i] = pair.first;
                  updates[i] = pair.second;
              }
              dest.writeParcelableArray(conditions, flags);
              dest.writeParcelableArray(updates, flags);
          }
          if (mActions == null) {
              dest.writeIntArray(null);
          } else {
              final int size = mActions.size();
              final int[] ids = new int[size];
              final InternalOnClickAction[] values = new InternalOnClickAction[size];
              for (int i = 0; i < size; i++) {
                  ids[i] = mActions.keyAt(i);
                  values[i] = mActions.valueAt(i);
              }
              dest.writeIntArray(ids);
              dest.writeParcelableArray(values, flags);
          }
      }
      public static final @android.annotation.NonNull Parcelable.Creator<CustomDescription> CREATOR =
              new Parcelable.Creator<CustomDescription>() {
          @Override
          public CustomDescription createFromParcel(Parcel parcel) {
              // Always go through the builder to ensure the data ingested by
              // the system obeys the contract of the builder to avoid attacks
              // using specially crafted parcels.
              final RemoteViews parentPresentation = parcel.readParcelable(null);
              if (parentPresentation == null) return null;
  
              final Builder builder = new Builder(parentPresentation);
              final int[] transformationIds = parcel.createIntArray();
              if (transformationIds != null) {
                  final InternalTransformation[] values =
                      parcel.readParcelableArray(null, InternalTransformation.class);
                  final int size = transformationIds.length;
                  for (int i = 0; i < size; i++) {
                      builder.addChild(transformationIds[i], values[i]);
                  }
              }
              final InternalValidator[] conditions =
                      parcel.readParcelableArray(null, InternalValidator.class);
              if (conditions != null) {
                  final BatchUpdates[] updates = parcel.readParcelableArray(null, BatchUpdates.class);
                  final int size = conditions.length;
                  for (int i = 0; i < size; i++) {
                      builder.batchUpdate(conditions[i], updates[i]);
                  }
              }
              final int[] actionIds = parcel.createIntArray();
              if (actionIds != null) {
                  final InternalOnClickAction[] values =
                      parcel.readParcelableArray(null, InternalOnClickAction.class);
                  final int size = actionIds.length;
                  for (int i = 0; i < size; i++) {
                      builder.addOnClickAction(actionIds[i], values[i]);
                  }
              }
              return builder.build();
          }
  
          @Override
          public CustomDescription[] newArray(int size) {
              return new CustomDescription[size];
          }
      };
  }
  