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