/* * 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.Objects; 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). * *

When it contains more than one field, the fields that match their regex are added to the * overall transformation result. * *

For example, a transformation to mask a credit card number contained in just one field would * be: * *

 * new CharSequenceTransformation
 *     .Builder(ccNumberId, Pattern.compile("^.*(\\d\\d\\d\\d)$"), "...$1")
 *     .build();
 * 
* *

But a transformation that generates a {@code Exp: MM / YYYY} credit expiration date from two * fields (month and year) would be: * *

 * new CharSequenceTransformation
 *   .Builder(ccExpMonthId, Pattern.compile("^(\\d\\d)$"), "Exp: $1")
 *   .addField(ccExpYearId, Pattern.compile("^(\\d\\d\\d\\d)$"), " / $1");
 * 
*/ 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> 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> entry : mFields.entrySet()) { final AutofillId id = entry.getKey(); final Pair 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> 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(); Objects.requireNonNull(id); Objects.requireNonNull(regex); Objects.requireNonNull(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 pair; int i = 0; for (Entry> 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 CREATOR = new Parcelable.Creator() { @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]; } }; }