Home | History | Annotate | Download | only in autofill
      1 /*
      2  * Copyright (C) 2017 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.service.autofill;
     18 
     19 import static android.view.autofill.Helper.sDebug;
     20 
     21 import android.annotation.NonNull;
     22 import android.annotation.TestApi;
     23 import android.os.Parcel;
     24 import android.os.Parcelable;
     25 import android.util.Log;
     26 import android.util.Pair;
     27 import android.view.autofill.AutofillId;
     28 import android.widget.RemoteViews;
     29 import android.widget.TextView;
     30 
     31 import com.android.internal.util.Preconditions;
     32 
     33 import java.util.LinkedHashMap;
     34 import java.util.Map.Entry;
     35 import java.util.regex.Matcher;
     36 import java.util.regex.Pattern;
     37 
     38 /**
     39  * Replaces a {@link TextView} child of a {@link CustomDescription} with the contents of one or
     40  * more regular expressions (regexs).
     41  *
     42  * <p>When it contains more than one field, the fields that match their regex are added to the
     43  * overall transformation result.
     44  *
     45  * <p>For example, a transformation to mask a credit card number contained in just one field would
     46  * be:
     47  *
     48  * <pre class="prettyprint">
     49  * new CharSequenceTransformation
     50  *     .Builder(ccNumberId, Pattern.compile("^.*(\\d\\d\\d\\d)$"), "...$1")
     51  *     .build();
     52  * </pre>
     53  *
     54  * <p>But a transformation that generates a {@code Exp: MM / YYYY} credit expiration date from two
     55  * fields (month and year) would be:
     56  *
     57  * <pre class="prettyprint">
     58  * new CharSequenceTransformation
     59  *   .Builder(ccExpMonthId, Pattern.compile("^(\\d\\d)$"), "Exp: $1")
     60  *   .addField(ccExpYearId, Pattern.compile("^(\\d\\d\\d\\d)$"), " / $1");
     61  * </pre>
     62  */
     63 public final class CharSequenceTransformation extends InternalTransformation implements
     64         Transformation, Parcelable {
     65     private static final String TAG = "CharSequenceTransformation";
     66 
     67     // Must use LinkedHashMap to preserve insertion order.
     68     @NonNull private final LinkedHashMap<AutofillId, Pair<Pattern, String>> mFields;
     69 
     70     private CharSequenceTransformation(Builder builder) {
     71         mFields = builder.mFields;
     72     }
     73 
     74     /** @hide */
     75     @Override
     76     @TestApi
     77     public void apply(@NonNull ValueFinder finder, @NonNull RemoteViews parentTemplate,
     78             int childViewId) throws Exception {
     79         final StringBuilder converted = new StringBuilder();
     80         final int size = mFields.size();
     81         if (sDebug) Log.d(TAG, size + " multiple fields on id " + childViewId);
     82         for (Entry<AutofillId, Pair<Pattern, String>> entry : mFields.entrySet()) {
     83             final AutofillId id = entry.getKey();
     84             final Pair<Pattern, String> field = entry.getValue();
     85             final String value = finder.findByAutofillId(id);
     86             if (value == null) {
     87                 Log.w(TAG, "No value for id " + id);
     88                 return;
     89             }
     90             try {
     91                 final Matcher matcher = field.first.matcher(value);
     92                 if (!matcher.find()) {
     93                     if (sDebug) Log.d(TAG, "match for " + field.first + " failed on id " + id);
     94                     return;
     95                 }
     96                 // replaceAll throws an exception if the subst is invalid
     97                 final String convertedValue = matcher.replaceAll(field.second);
     98                 converted.append(convertedValue);
     99             } catch (Exception e) {
    100                 // Do not log full exception to avoid PII leaking
    101                 Log.w(TAG, "Cannot apply " + field.first.pattern() + "->" + field.second + " to "
    102                         + "field with autofill id" + id + ": " + e.getClass());
    103                 throw e;
    104             }
    105         }
    106         parentTemplate.setCharSequence(childViewId, "setText", converted);
    107     }
    108 
    109     /**
    110      * Builder for {@link CharSequenceTransformation} objects.
    111      */
    112     public static class Builder {
    113 
    114         // Must use LinkedHashMap to preserve insertion order.
    115         @NonNull private final LinkedHashMap<AutofillId, Pair<Pattern, String>> mFields =
    116                 new LinkedHashMap<>();
    117         private boolean mDestroyed;
    118 
    119         /**
    120          * Creates a new builder and adds the first transformed contents of a field to the overall
    121          * result of this transformation.
    122          *
    123          * @param id id of the screen field.
    124          * @param regex regular expression with groups (delimited by {@code (} and {@code (}) that
    125          * are used to substitute parts of the value.
    126          * @param subst the string that substitutes the matched regex, using {@code $} for
    127          * group substitution ({@code $1} for 1st group match, {@code $2} for 2nd, etc).
    128          */
    129         public Builder(@NonNull AutofillId id, @NonNull Pattern regex, @NonNull String subst) {
    130             addField(id, regex, subst);
    131         }
    132 
    133         /**
    134          * Adds the transformed contents of a field to the overall result of this transformation.
    135          *
    136          * @param id id of the screen field.
    137          * @param regex regular expression with groups (delimited by {@code (} and {@code (}) that
    138          * are used to substitute parts of the value.
    139          * @param subst the string that substitutes the matched regex, using {@code $} for
    140          * group substitution ({@code $1} for 1st group match, {@code $2} for 2nd, etc).
    141          *
    142          * @return this builder.
    143          */
    144         public Builder addField(@NonNull AutofillId id, @NonNull Pattern regex,
    145                 @NonNull String subst) {
    146             throwIfDestroyed();
    147             Preconditions.checkNotNull(id);
    148             Preconditions.checkNotNull(regex);
    149             Preconditions.checkNotNull(subst);
    150 
    151             mFields.put(id, new Pair<>(regex, subst));
    152             return this;
    153         }
    154 
    155         /**
    156          * Creates a new {@link CharSequenceTransformation} instance.
    157          */
    158         public CharSequenceTransformation build() {
    159             throwIfDestroyed();
    160             mDestroyed = true;
    161             return new CharSequenceTransformation(this);
    162         }
    163 
    164         private void throwIfDestroyed() {
    165             Preconditions.checkState(!mDestroyed, "Already called build()");
    166         }
    167     }
    168 
    169     /////////////////////////////////////
    170     // Object "contract" methods. //
    171     /////////////////////////////////////
    172     @Override
    173     public String toString() {
    174         if (!sDebug) return super.toString();
    175 
    176         return "MultipleViewsCharSequenceTransformation: [fields=" + mFields + "]";
    177     }
    178 
    179     /////////////////////////////////////
    180     // Parcelable "contract" methods. //
    181     /////////////////////////////////////
    182     @Override
    183     public int describeContents() {
    184         return 0;
    185     }
    186 
    187     @Override
    188     public void writeToParcel(Parcel parcel, int flags) {
    189         final int size = mFields.size();
    190         final AutofillId[] ids = new AutofillId[size];
    191         final Pattern[] regexs = new Pattern[size];
    192         final String[] substs = new String[size];
    193         Pair<Pattern, String> pair;
    194         int i = 0;
    195         for (Entry<AutofillId, Pair<Pattern, String>> entry : mFields.entrySet()) {
    196             ids[i] = entry.getKey();
    197             pair = entry.getValue();
    198             regexs[i] = pair.first;
    199             substs[i] = pair.second;
    200             i++;
    201         }
    202 
    203         parcel.writeParcelableArray(ids, flags);
    204         parcel.writeSerializable(regexs);
    205         parcel.writeStringArray(substs);
    206     }
    207 
    208     public static final Parcelable.Creator<CharSequenceTransformation> CREATOR =
    209             new Parcelable.Creator<CharSequenceTransformation>() {
    210         @Override
    211         public CharSequenceTransformation createFromParcel(Parcel parcel) {
    212             final AutofillId[] ids = parcel.readParcelableArray(null, AutofillId.class);
    213             final Pattern[] regexs = (Pattern[]) parcel.readSerializable();
    214             final String[] substs = parcel.createStringArray();
    215 
    216             // Always go through the builder to ensure the data ingested by
    217             // the system obeys the contract of the builder to avoid attacks
    218             // using specially crafted parcels.
    219             final CharSequenceTransformation.Builder builder =
    220                     new CharSequenceTransformation.Builder(ids[0], regexs[0], substs[0]);
    221 
    222             final int size = ids.length;
    223             for (int i = 1; i < size; i++) {
    224                 builder.addField(ids[i], regexs[i], substs[i]);
    225             }
    226             return builder.build();
    227         }
    228 
    229         @Override
    230         public CharSequenceTransformation[] newArray(int size) {
    231             return new CharSequenceTransformation[size];
    232         }
    233     };
    234 }
    235