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.TestApi;
  import android.os.Parcel;
  import android.os.Parcelable;
  import android.util.Log;
  import android.util.Pair;
  import android.view.autofill.AutofillId;
  import android.widget.RemoteViews;
  import android.widget.TextView;
  
  import com.android.internal.util.Preconditions;
  
  import java.util.LinkedHashMap;
  import java.util.Map.Entry;
  import java.util.regex.Matcher;
  import java.util.regex.Pattern;
  
  /**
   * Replaces a {@link TextView} child of a {@link CustomDescription} with the contents of one or
   * more regular expressions (regexs).
   *
   * <p>When it contains more than one field, the fields that match their regex are added to the
   * overall transformation result.
   *
   * <p>For example, a transformation to mask a credit card number contained in just one field would
   * be:
   *
   * <pre class="prettyprint">
   * new CharSequenceTransformation
   *     .Builder(ccNumberId, Pattern.compile("^.*(\\d\\d\\d\\d)$"), "...$1")
   *     .build();
   * </pre>
   *
   * <p>But a transformation that generates a {@code Exp: MM / YYYY} credit expiration date from two
   * fields (month and year) would be:
   *
   * <pre class="prettyprint">
   * new CharSequenceTransformation
   *   .Builder(ccExpMonthId, Pattern.compile("^(\\d\\d)$"), "Exp: $1")
   *   .addField(ccExpYearId, Pattern.compile("^(\\d\\d\\d\\d)$"), " / $1");
   * </pre>
   */
  public final class CharSequenceTransformation extends InternalTransformation implements
          Transformation, Parcelable {
      private static final String TAG = "CharSequenceTransformation";
  
      // Must use LinkedHashMap to preserve insertion order.
      @NonNull private final LinkedHashMap<AutofillId, Pair<Pattern, String>> mFields;
  
      private CharSequenceTransformation(Builder builder) {
          mFields = builder.mFields;
      }
  
      /** @hide */
      @Override
      @TestApi
      public void apply(@NonNull ValueFinder finder, @NonNull RemoteViews parentTemplate,
              int childViewId) throws Exception {
          final StringBuilder converted = new StringBuilder();
          final int size = mFields.size();
          if (sDebug) Log.d(TAG, size + " fields on id " + childViewId);
          for (Entry<AutofillId, Pair<Pattern, String>> entry : mFields.entrySet()) {
              final AutofillId id = entry.getKey();
              final Pair<Pattern, String> field = entry.getValue();
              final String value = finder.findByAutofillId(id);
              if (value == null) {
                  Log.w(TAG, "No value for id " + id);
                  return;
              }
              try {
                  final Matcher matcher = field.first.matcher(value);
                  if (!matcher.find()) {
                      if (sDebug) Log.d(TAG, "Match for " + field.first + " failed on id " + id);
                      return;
                  }
                  // replaceAll throws an exception if the subst is invalid
                  final String convertedValue = matcher.replaceAll(field.second);
                  converted.append(convertedValue);
              } catch (Exception e) {
                  // Do not log full exception to avoid PII leaking
                  Log.w(TAG, "Cannot apply " + field.first.pattern() + "->" + field.second + " to "
                          + "field with autofill id" + id + ": " + e.getClass());
                  throw e;
              }
          }
          // Cannot log converted, it might have PII
          Log.d(TAG, "Converting text on child " + childViewId + " to " + converted.length()
                  + "_chars");
          parentTemplate.setCharSequence(childViewId, "setText", converted);
      }
  
      /**
       * Builder for {@link CharSequenceTransformation} objects.
       */
      public static class Builder {
  
          // Must use LinkedHashMap to preserve insertion order.
          @NonNull private final LinkedHashMap<AutofillId, Pair<Pattern, String>> mFields =
                  new LinkedHashMap<>();
          private boolean mDestroyed;
  
          /**
           * Creates a new builder and adds the first transformed contents of a field to the overall
           * result of this transformation.
           *
           * @param id id of the screen field.
           * @param regex regular expression with groups (delimited by {@code (} and {@code (}) that
           * are used to substitute parts of the value.
           * @param subst the string that substitutes the matched regex, using {@code $} for
           * group substitution ({@code $1} for 1st group match, {@code $2} for 2nd, etc).
           */
          public Builder(@NonNull AutofillId id, @NonNull Pattern regex, @NonNull String subst) {
              addField(id, regex, subst);
          }
  
          /**
           * Adds the transformed contents of a field to the overall result of this transformation.
           *
           * @param id id of the screen field.
           * @param regex regular expression with groups (delimited by {@code (} and {@code (}) that
           * are used to substitute parts of the value.
           * @param subst the string that substitutes the matched regex, using {@code $} for
           * group substitution ({@code $1} for 1st group match, {@code $2} for 2nd, etc).
           *
           * @return this builder.
           */
          public Builder addField(@NonNull AutofillId id, @NonNull Pattern regex,
                  @NonNull String subst) {
              throwIfDestroyed();
              Preconditions.checkNotNull(id);
              Preconditions.checkNotNull(regex);
              Preconditions.checkNotNull(subst);
  
              mFields.put(id, new Pair<>(regex, subst));
              return this;
          }
  
          /**
           * Creates a new {@link CharSequenceTransformation} instance.
           */
          public CharSequenceTransformation build() {
              throwIfDestroyed();
              mDestroyed = true;
              return new CharSequenceTransformation(this);
          }
  
          private void throwIfDestroyed() {
              Preconditions.checkState(!mDestroyed, "Already called build()");
          }
      }
  
      /////////////////////////////////////
      // Object "contract" methods. //
      /////////////////////////////////////
      @Override
      public String toString() {
          if (!sDebug) return super.toString();
  
          return "MultipleViewsCharSequenceTransformation: [fields=" + mFields + "]";
      }
  
      /////////////////////////////////////
      // Parcelable "contract" methods. //
      /////////////////////////////////////
      @Override
      public int describeContents() {
          return 0;
      }
  
      @Override
      public void writeToParcel(Parcel parcel, int flags) {
          final int size = mFields.size();
          final AutofillId[] ids = new AutofillId[size];
          final Pattern[] regexs = new Pattern[size];
          final String[] substs = new String[size];
          Pair<Pattern, String> pair;
          int i = 0;
          for (Entry<AutofillId, Pair<Pattern, String>> entry : mFields.entrySet()) {
              ids[i] = entry.getKey();
              pair = entry.getValue();
              regexs[i] = pair.first;
              substs[i] = pair.second;
              i++;
          }
  
          parcel.writeParcelableArray(ids, flags);
          parcel.writeSerializable(regexs);
          parcel.writeStringArray(substs);
      }
  
      public static final @android.annotation.NonNull Parcelable.Creator<CharSequenceTransformation> CREATOR =
              new Parcelable.Creator<CharSequenceTransformation>() {
          @Override
          public CharSequenceTransformation createFromParcel(Parcel parcel) {
              final AutofillId[] ids = parcel.readParcelableArray(null, AutofillId.class);
              final Pattern[] regexs = (Pattern[]) parcel.readSerializable();
              final String[] substs = parcel.createStringArray();
  
              // 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 CharSequenceTransformation.Builder builder =
                      new CharSequenceTransformation.Builder(ids[0], regexs[0], substs[0]);
  
              final int size = ids.length;
              for (int i = 1; i < size; i++) {
                  builder.addField(ids[i], regexs[i], substs[i]);
              }
              return builder.build();
          }
  
          @Override
          public CharSequenceTransformation[] newArray(int size) {
              return new CharSequenceTransformation[size];
          }
      };
  }
  