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