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