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 
CharSequenceTransformation(Builder builder)70     private CharSequenceTransformation(Builder builder) {
71         mFields = builder.mFields;
72     }
73 
74     /** @hide */
75     @Override
76     @TestApi
apply(@onNull ValueFinder finder, @NonNull RemoteViews parentTemplate, int childViewId)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 + " 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         // Cannot log converted, it might have PII
107         Log.d(TAG, "Converting text on child " + childViewId + " to " + converted.length()
108                 + "_chars");
109         parentTemplate.setCharSequence(childViewId, "setText", converted);
110     }
111 
112     /**
113      * Builder for {@link CharSequenceTransformation} objects.
114      */
115     public static class Builder {
116 
117         // Must use LinkedHashMap to preserve insertion order.
118         @NonNull private final LinkedHashMap<AutofillId, Pair<Pattern, String>> mFields =
119                 new LinkedHashMap<>();
120         private boolean mDestroyed;
121 
122         /**
123          * Creates a new builder and adds the first transformed contents of a field to the overall
124          * result of this transformation.
125          *
126          * @param id id of the screen field.
127          * @param regex regular expression with groups (delimited by {@code (} and {@code (}) that
128          * are used to substitute parts of the value.
129          * @param subst the string that substitutes the matched regex, using {@code $} for
130          * group substitution ({@code $1} for 1st group match, {@code $2} for 2nd, etc).
131          */
Builder(@onNull AutofillId id, @NonNull Pattern regex, @NonNull String subst)132         public Builder(@NonNull AutofillId id, @NonNull Pattern regex, @NonNull String subst) {
133             addField(id, regex, subst);
134         }
135 
136         /**
137          * Adds the transformed contents of a field to the overall result of this transformation.
138          *
139          * @param id id of the screen field.
140          * @param regex regular expression with groups (delimited by {@code (} and {@code (}) that
141          * are used to substitute parts of the value.
142          * @param subst the string that substitutes the matched regex, using {@code $} for
143          * group substitution ({@code $1} for 1st group match, {@code $2} for 2nd, etc).
144          *
145          * @return this builder.
146          */
addField(@onNull AutofillId id, @NonNull Pattern regex, @NonNull String subst)147         public Builder addField(@NonNull AutofillId id, @NonNull Pattern regex,
148                 @NonNull String subst) {
149             throwIfDestroyed();
150             Preconditions.checkNotNull(id);
151             Preconditions.checkNotNull(regex);
152             Preconditions.checkNotNull(subst);
153 
154             mFields.put(id, new Pair<>(regex, subst));
155             return this;
156         }
157 
158         /**
159          * Creates a new {@link CharSequenceTransformation} instance.
160          */
build()161         public CharSequenceTransformation build() {
162             throwIfDestroyed();
163             mDestroyed = true;
164             return new CharSequenceTransformation(this);
165         }
166 
throwIfDestroyed()167         private void throwIfDestroyed() {
168             Preconditions.checkState(!mDestroyed, "Already called build()");
169         }
170     }
171 
172     /////////////////////////////////////
173     // Object "contract" methods. //
174     /////////////////////////////////////
175     @Override
toString()176     public String toString() {
177         if (!sDebug) return super.toString();
178 
179         return "MultipleViewsCharSequenceTransformation: [fields=" + mFields + "]";
180     }
181 
182     /////////////////////////////////////
183     // Parcelable "contract" methods. //
184     /////////////////////////////////////
185     @Override
describeContents()186     public int describeContents() {
187         return 0;
188     }
189 
190     @Override
writeToParcel(Parcel parcel, int flags)191     public void writeToParcel(Parcel parcel, int flags) {
192         final int size = mFields.size();
193         final AutofillId[] ids = new AutofillId[size];
194         final Pattern[] regexs = new Pattern[size];
195         final String[] substs = new String[size];
196         Pair<Pattern, String> pair;
197         int i = 0;
198         for (Entry<AutofillId, Pair<Pattern, String>> entry : mFields.entrySet()) {
199             ids[i] = entry.getKey();
200             pair = entry.getValue();
201             regexs[i] = pair.first;
202             substs[i] = pair.second;
203             i++;
204         }
205 
206         parcel.writeParcelableArray(ids, flags);
207         parcel.writeSerializable(regexs);
208         parcel.writeStringArray(substs);
209     }
210 
211     public static final @android.annotation.NonNull Parcelable.Creator<CharSequenceTransformation> CREATOR =
212             new Parcelable.Creator<CharSequenceTransformation>() {
213         @Override
214         public CharSequenceTransformation createFromParcel(Parcel parcel) {
215             final AutofillId[] ids = parcel.readParcelableArray(null, AutofillId.class);
216             final Pattern[] regexs = (Pattern[]) parcel.readSerializable();
217             final String[] substs = parcel.createStringArray();
218 
219             // Always go through the builder to ensure the data ingested by
220             // the system obeys the contract of the builder to avoid attacks
221             // using specially crafted parcels.
222             final CharSequenceTransformation.Builder builder =
223                     new CharSequenceTransformation.Builder(ids[0], regexs[0], substs[0]);
224 
225             final int size = ids.length;
226             for (int i = 1; i < size; i++) {
227                 builder.addField(ids[i], regexs[i], substs[i]);
228             }
229             return builder.build();
230         }
231 
232         @Override
233         public CharSequenceTransformation[] newArray(int size) {
234             return new CharSequenceTransformation[size];
235         }
236     };
237 }
238