blob: e3e884406188a67b344f5cb4ceddd2ebca46cb2b [file] [log] [blame]
Felipe Leme979013d2017-06-22 10:59:23 -07001/*
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
17package android.service.autofill;
18
19import static android.view.autofill.Helper.sDebug;
20
21import android.annotation.NonNull;
Philip P. Moltmannebbe2d42017-07-10 12:07:54 -070022import android.annotation.TestApi;
Felipe Leme979013d2017-06-22 10:59:23 -070023import android.os.Parcel;
24import android.os.Parcelable;
Felipe Leme979013d2017-06-22 10:59:23 -070025import android.util.Log;
26import android.util.Pair;
27import android.view.autofill.AutofillId;
28import android.widget.RemoteViews;
29import android.widget.TextView;
30
31import com.android.internal.util.Preconditions;
32
Felipe Leme6fb52a52018-01-17 16:08:01 -080033import java.util.LinkedHashMap;
34import java.util.Map.Entry;
Felipe Lemea5083c42017-08-08 12:39:04 -070035import java.util.regex.Matcher;
Philip P. Moltmannebbe2d42017-07-10 12:07:54 -070036import java.util.regex.Pattern;
37
Felipe Leme979013d2017-06-22 10:59:23 -070038/**
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">
Felipe Leme906b8532017-07-17 14:56:17 -070049 * new CharSequenceTransformation
50 * .Builder(ccNumberId, Pattern.compile("^.*(\\d\\d\\d\\d)$"), "...$1")
51 * .build();
Felipe Leme979013d2017-06-22 10:59:23 -070052 * </pre>
53 *
Philip P. Moltmannebbe2d42017-07-10 12:07:54 -070054 * <p>But a transformation that generates a {@code Exp: MM / YYYY} credit expiration date from two
Felipe Leme979013d2017-06-22 10:59:23 -070055 * fields (month and year) would be:
56 *
57 * <pre class="prettyprint">
Felipe Lemea5083c42017-08-08 12:39:04 -070058 * new CharSequenceTransformation
59 * .Builder(ccExpMonthId, Pattern.compile("^(\\d\\d)$"), "Exp: $1")
Felipe Leme906b8532017-07-17 14:56:17 -070060 * .addField(ccExpYearId, Pattern.compile("^(\\d\\d\\d\\d)$"), " / $1");
Felipe Leme979013d2017-06-22 10:59:23 -070061 * </pre>
62 */
Philip P. Moltmann3858aa62017-07-11 15:22:14 -070063public final class CharSequenceTransformation extends InternalTransformation implements
64 Transformation, Parcelable {
Felipe Leme979013d2017-06-22 10:59:23 -070065 private static final String TAG = "CharSequenceTransformation";
Felipe Leme6fb52a52018-01-17 16:08:01 -080066
67 // Must use LinkedHashMap to preserve insertion order.
68 @NonNull private final LinkedHashMap<AutofillId, Pair<Pattern, String>> mFields;
Felipe Leme979013d2017-06-22 10:59:23 -070069
70 private CharSequenceTransformation(Builder builder) {
71 mFields = builder.mFields;
72 }
73
74 /** @hide */
75 @Override
Philip P. Moltmannebbe2d42017-07-10 12:07:54 -070076 @TestApi
Felipe Leme979013d2017-06-22 10:59:23 -070077 public void apply(@NonNull ValueFinder finder, @NonNull RemoteViews parentTemplate,
Felipe Leme22101ca2017-07-18 08:58:50 -070078 int childViewId) throws Exception {
Felipe Leme979013d2017-06-22 10:59:23 -070079 final StringBuilder converted = new StringBuilder();
80 final int size = mFields.size();
Felipe Leme5429ade2018-08-31 13:13:01 -070081 if (sDebug) Log.d(TAG, size + " fields on id " + childViewId);
Felipe Leme6fb52a52018-01-17 16:08:01 -080082 for (Entry<AutofillId, Pair<Pattern, String>> entry : mFields.entrySet()) {
83 final AutofillId id = entry.getKey();
84 final Pair<Pattern, String> field = entry.getValue();
Felipe Leme979013d2017-06-22 10:59:23 -070085 final String value = finder.findByAutofillId(id);
86 if (value == null) {
87 Log.w(TAG, "No value for id " + id);
88 return;
89 }
Philip P. Moltmannebbe2d42017-07-10 12:07:54 -070090 try {
Felipe Lemea5083c42017-08-08 12:39:04 -070091 final Matcher matcher = field.first.matcher(value);
Felipe Leme1540bfd2017-09-07 15:21:01 -070092 if (!matcher.find()) {
Felipe Lemee18f51f2018-09-07 11:18:27 -070093 if (sDebug) Log.d(TAG, "Match for " + field.first + " failed on id " + id);
Felipe Lemea5083c42017-08-08 12:39:04 -070094 return;
95 }
Philip P. Moltmannebbe2d42017-07-10 12:07:54 -070096 // replaceAll throws an exception if the subst is invalid
Felipe Lemea5083c42017-08-08 12:39:04 -070097 final String convertedValue = matcher.replaceAll(field.second);
Philip P. Moltmannebbe2d42017-07-10 12:07:54 -070098 converted.append(convertedValue);
99 } catch (Exception e) {
Felipe Leme22101ca2017-07-18 08:58:50 -0700100 // Do not log full exception to avoid PII leaking
101 Log.w(TAG, "Cannot apply " + field.first.pattern() + "->" + field.second + " to "
Philip P. Moltmannebbe2d42017-07-10 12:07:54 -0700102 + "field with autofill id" + id + ": " + e.getClass());
Felipe Leme22101ca2017-07-18 08:58:50 -0700103 throw e;
Philip P. Moltmannebbe2d42017-07-10 12:07:54 -0700104 }
Felipe Leme979013d2017-06-22 10:59:23 -0700105 }
Felipe Lemee18f51f2018-09-07 11:18:27 -0700106 // Cannot log converted, it might have PII
107 Log.d(TAG, "Converting text on child " + childViewId + " to " + converted.length()
108 + "_chars");
Felipe Leme979013d2017-06-22 10:59:23 -0700109 parentTemplate.setCharSequence(childViewId, "setText", converted);
110 }
111
112 /**
113 * Builder for {@link CharSequenceTransformation} objects.
114 */
115 public static class Builder {
Felipe Leme6fb52a52018-01-17 16:08:01 -0800116
117 // Must use LinkedHashMap to preserve insertion order.
118 @NonNull private final LinkedHashMap<AutofillId, Pair<Pattern, String>> mFields =
119 new LinkedHashMap<>();
Felipe Leme979013d2017-06-22 10:59:23 -0700120 private boolean mDestroyed;
121
Philip P. Moltmannebbe2d42017-07-10 12:07:54 -0700122 /**
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
Felipe Leme906b8532017-07-17 14:56:17 -0700128 * are used to substitute parts of the value.
Philip P. Moltmannebbe2d42017-07-10 12:07:54 -0700129 * @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 */
Felipe Leme906b8532017-07-17 14:56:17 -0700132 public Builder(@NonNull AutofillId id, @NonNull Pattern regex, @NonNull String subst) {
Philip P. Moltmannebbe2d42017-07-10 12:07:54 -0700133 addField(id, regex, subst);
134 }
Felipe Leme979013d2017-06-22 10:59:23 -0700135
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
Felipe Leme906b8532017-07-17 14:56:17 -0700141 * are used to substitute parts of the value.
Felipe Leme979013d2017-06-22 10:59:23 -0700142 * @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 */
Felipe Leme906b8532017-07-17 14:56:17 -0700147 public Builder addField(@NonNull AutofillId id, @NonNull Pattern regex,
Felipe Leme979013d2017-06-22 10:59:23 -0700148 @NonNull String subst) {
Felipe Leme979013d2017-06-22 10:59:23 -0700149 throwIfDestroyed();
150 Preconditions.checkNotNull(id);
151 Preconditions.checkNotNull(regex);
152 Preconditions.checkNotNull(subst);
Philip P. Moltmannebbe2d42017-07-10 12:07:54 -0700153
Felipe Leme906b8532017-07-17 14:56:17 -0700154 mFields.put(id, new Pair<>(regex, subst));
Felipe Leme979013d2017-06-22 10:59:23 -0700155 return this;
156 }
157
158 /**
159 * Creates a new {@link CharSequenceTransformation} instance.
Felipe Leme979013d2017-06-22 10:59:23 -0700160 */
161 public CharSequenceTransformation build() {
162 throwIfDestroyed();
Felipe Leme979013d2017-06-22 10:59:23 -0700163 mDestroyed = true;
164 return new CharSequenceTransformation(this);
165 }
166
167 private void throwIfDestroyed() {
168 Preconditions.checkState(!mDestroyed, "Already called build()");
169 }
170 }
171
172 /////////////////////////////////////
173 // Object "contract" methods. //
174 /////////////////////////////////////
175 @Override
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
186 public int describeContents() {
187 return 0;
188 }
189
190 @Override
191 public void writeToParcel(Parcel parcel, int flags) {
192 final int size = mFields.size();
193 final AutofillId[] ids = new AutofillId[size];
Felipe Leme906b8532017-07-17 14:56:17 -0700194 final Pattern[] regexs = new Pattern[size];
Felipe Leme979013d2017-06-22 10:59:23 -0700195 final String[] substs = new String[size];
Philip P. Moltmannebbe2d42017-07-10 12:07:54 -0700196 Pair<Pattern, String> pair;
Felipe Leme6fb52a52018-01-17 16:08:01 -0800197 int i = 0;
198 for (Entry<AutofillId, Pair<Pattern, String>> entry : mFields.entrySet()) {
199 ids[i] = entry.getKey();
200 pair = entry.getValue();
Felipe Leme906b8532017-07-17 14:56:17 -0700201 regexs[i] = pair.first;
Felipe Leme979013d2017-06-22 10:59:23 -0700202 substs[i] = pair.second;
Felipe Leme6fb52a52018-01-17 16:08:01 -0800203 i++;
Felipe Leme979013d2017-06-22 10:59:23 -0700204 }
Felipe Leme6fb52a52018-01-17 16:08:01 -0800205
Felipe Leme979013d2017-06-22 10:59:23 -0700206 parcel.writeParcelableArray(ids, flags);
Felipe Leme906b8532017-07-17 14:56:17 -0700207 parcel.writeSerializable(regexs);
Felipe Leme979013d2017-06-22 10:59:23 -0700208 parcel.writeStringArray(substs);
209 }
210
Jeff Sharkey9e8f83d2019-02-28 12:06:45 -0700211 public static final @android.annotation.NonNull Parcelable.Creator<CharSequenceTransformation> CREATOR =
Felipe Leme979013d2017-06-22 10:59:23 -0700212 new Parcelable.Creator<CharSequenceTransformation>() {
213 @Override
214 public CharSequenceTransformation createFromParcel(Parcel parcel) {
Philip P. Moltmannebbe2d42017-07-10 12:07:54 -0700215 final AutofillId[] ids = parcel.readParcelableArray(null, AutofillId.class);
Felipe Leme906b8532017-07-17 14:56:17 -0700216 final Pattern[] regexs = (Pattern[]) parcel.readSerializable();
Philip P. Moltmannebbe2d42017-07-10 12:07:54 -0700217 final String[] substs = parcel.createStringArray();
218
Felipe Leme979013d2017-06-22 10:59:23 -0700219 // 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 =
Philip P. Moltmannebbe2d42017-07-10 12:07:54 -0700223 new CharSequenceTransformation.Builder(ids[0], regexs[0], substs[0]);
224
Felipe Leme979013d2017-06-22 10:59:23 -0700225 final int size = ids.length;
Philip P. Moltmannebbe2d42017-07-10 12:07:54 -0700226 for (int i = 1; i < size; i++) {
Felipe Leme979013d2017-06-22 10:59:23 -0700227 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}