Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 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; |
Philip P. Moltmann | ebbe2d4 | 2017-07-10 12:07:54 -0700 | [diff] [blame] | 22 | import android.annotation.TestApi; |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 23 | import android.os.Parcel; |
| 24 | import android.os.Parcelable; |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 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 | |
Felipe Leme | 6fb52a5 | 2018-01-17 16:08:01 -0800 | [diff] [blame] | 33 | import java.util.LinkedHashMap; |
| 34 | import java.util.Map.Entry; |
Felipe Leme | a5083c4 | 2017-08-08 12:39:04 -0700 | [diff] [blame] | 35 | import java.util.regex.Matcher; |
Philip P. Moltmann | ebbe2d4 | 2017-07-10 12:07:54 -0700 | [diff] [blame] | 36 | import java.util.regex.Pattern; |
| 37 | |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 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"> |
Felipe Leme | 906b853 | 2017-07-17 14:56:17 -0700 | [diff] [blame] | 49 | * new CharSequenceTransformation |
| 50 | * .Builder(ccNumberId, Pattern.compile("^.*(\\d\\d\\d\\d)$"), "...$1") |
| 51 | * .build(); |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 52 | * </pre> |
| 53 | * |
Philip P. Moltmann | ebbe2d4 | 2017-07-10 12:07:54 -0700 | [diff] [blame] | 54 | * <p>But a transformation that generates a {@code Exp: MM / YYYY} credit expiration date from two |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 55 | * fields (month and year) would be: |
| 56 | * |
| 57 | * <pre class="prettyprint"> |
Felipe Leme | a5083c4 | 2017-08-08 12:39:04 -0700 | [diff] [blame] | 58 | * new CharSequenceTransformation |
| 59 | * .Builder(ccExpMonthId, Pattern.compile("^(\\d\\d)$"), "Exp: $1") |
Felipe Leme | 906b853 | 2017-07-17 14:56:17 -0700 | [diff] [blame] | 60 | * .addField(ccExpYearId, Pattern.compile("^(\\d\\d\\d\\d)$"), " / $1"); |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 61 | * </pre> |
| 62 | */ |
Philip P. Moltmann | 3858aa6 | 2017-07-11 15:22:14 -0700 | [diff] [blame] | 63 | public final class CharSequenceTransformation extends InternalTransformation implements |
| 64 | Transformation, Parcelable { |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 65 | private static final String TAG = "CharSequenceTransformation"; |
Felipe Leme | 6fb52a5 | 2018-01-17 16:08:01 -0800 | [diff] [blame] | 66 | |
| 67 | // Must use LinkedHashMap to preserve insertion order. |
| 68 | @NonNull private final LinkedHashMap<AutofillId, Pair<Pattern, String>> mFields; |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 69 | |
| 70 | private CharSequenceTransformation(Builder builder) { |
| 71 | mFields = builder.mFields; |
| 72 | } |
| 73 | |
| 74 | /** @hide */ |
| 75 | @Override |
Philip P. Moltmann | ebbe2d4 | 2017-07-10 12:07:54 -0700 | [diff] [blame] | 76 | @TestApi |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 77 | public void apply(@NonNull ValueFinder finder, @NonNull RemoteViews parentTemplate, |
Felipe Leme | 22101ca | 2017-07-18 08:58:50 -0700 | [diff] [blame] | 78 | int childViewId) throws Exception { |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 79 | final StringBuilder converted = new StringBuilder(); |
| 80 | final int size = mFields.size(); |
Felipe Leme | 5429ade | 2018-08-31 13:13:01 -0700 | [diff] [blame] | 81 | if (sDebug) Log.d(TAG, size + " fields on id " + childViewId); |
Felipe Leme | 6fb52a5 | 2018-01-17 16:08:01 -0800 | [diff] [blame] | 82 | for (Entry<AutofillId, Pair<Pattern, String>> entry : mFields.entrySet()) { |
| 83 | final AutofillId id = entry.getKey(); |
| 84 | final Pair<Pattern, String> field = entry.getValue(); |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 85 | final String value = finder.findByAutofillId(id); |
| 86 | if (value == null) { |
| 87 | Log.w(TAG, "No value for id " + id); |
| 88 | return; |
| 89 | } |
Philip P. Moltmann | ebbe2d4 | 2017-07-10 12:07:54 -0700 | [diff] [blame] | 90 | try { |
Felipe Leme | a5083c4 | 2017-08-08 12:39:04 -0700 | [diff] [blame] | 91 | final Matcher matcher = field.first.matcher(value); |
Felipe Leme | 1540bfd | 2017-09-07 15:21:01 -0700 | [diff] [blame] | 92 | if (!matcher.find()) { |
Felipe Leme | e18f51f | 2018-09-07 11:18:27 -0700 | [diff] [blame] | 93 | if (sDebug) Log.d(TAG, "Match for " + field.first + " failed on id " + id); |
Felipe Leme | a5083c4 | 2017-08-08 12:39:04 -0700 | [diff] [blame] | 94 | return; |
| 95 | } |
Philip P. Moltmann | ebbe2d4 | 2017-07-10 12:07:54 -0700 | [diff] [blame] | 96 | // replaceAll throws an exception if the subst is invalid |
Felipe Leme | a5083c4 | 2017-08-08 12:39:04 -0700 | [diff] [blame] | 97 | final String convertedValue = matcher.replaceAll(field.second); |
Philip P. Moltmann | ebbe2d4 | 2017-07-10 12:07:54 -0700 | [diff] [blame] | 98 | converted.append(convertedValue); |
| 99 | } catch (Exception e) { |
Felipe Leme | 22101ca | 2017-07-18 08:58:50 -0700 | [diff] [blame] | 100 | // Do not log full exception to avoid PII leaking |
| 101 | Log.w(TAG, "Cannot apply " + field.first.pattern() + "->" + field.second + " to " |
Philip P. Moltmann | ebbe2d4 | 2017-07-10 12:07:54 -0700 | [diff] [blame] | 102 | + "field with autofill id" + id + ": " + e.getClass()); |
Felipe Leme | 22101ca | 2017-07-18 08:58:50 -0700 | [diff] [blame] | 103 | throw e; |
Philip P. Moltmann | ebbe2d4 | 2017-07-10 12:07:54 -0700 | [diff] [blame] | 104 | } |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 105 | } |
Felipe Leme | e18f51f | 2018-09-07 11:18:27 -0700 | [diff] [blame] | 106 | // Cannot log converted, it might have PII |
| 107 | Log.d(TAG, "Converting text on child " + childViewId + " to " + converted.length() |
| 108 | + "_chars"); |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 109 | parentTemplate.setCharSequence(childViewId, "setText", converted); |
| 110 | } |
| 111 | |
| 112 | /** |
| 113 | * Builder for {@link CharSequenceTransformation} objects. |
| 114 | */ |
| 115 | public static class Builder { |
Felipe Leme | 6fb52a5 | 2018-01-17 16:08:01 -0800 | [diff] [blame] | 116 | |
| 117 | // Must use LinkedHashMap to preserve insertion order. |
| 118 | @NonNull private final LinkedHashMap<AutofillId, Pair<Pattern, String>> mFields = |
| 119 | new LinkedHashMap<>(); |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 120 | private boolean mDestroyed; |
| 121 | |
Philip P. Moltmann | ebbe2d4 | 2017-07-10 12:07:54 -0700 | [diff] [blame] | 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 |
Felipe Leme | 906b853 | 2017-07-17 14:56:17 -0700 | [diff] [blame] | 128 | * are used to substitute parts of the value. |
Philip P. Moltmann | ebbe2d4 | 2017-07-10 12:07:54 -0700 | [diff] [blame] | 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 | */ |
Felipe Leme | 906b853 | 2017-07-17 14:56:17 -0700 | [diff] [blame] | 132 | public Builder(@NonNull AutofillId id, @NonNull Pattern regex, @NonNull String subst) { |
Philip P. Moltmann | ebbe2d4 | 2017-07-10 12:07:54 -0700 | [diff] [blame] | 133 | addField(id, regex, subst); |
| 134 | } |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 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 |
Felipe Leme | 906b853 | 2017-07-17 14:56:17 -0700 | [diff] [blame] | 141 | * are used to substitute parts of the value. |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 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 | */ |
Felipe Leme | 906b853 | 2017-07-17 14:56:17 -0700 | [diff] [blame] | 147 | public Builder addField(@NonNull AutofillId id, @NonNull Pattern regex, |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 148 | @NonNull String subst) { |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 149 | throwIfDestroyed(); |
| 150 | Preconditions.checkNotNull(id); |
| 151 | Preconditions.checkNotNull(regex); |
| 152 | Preconditions.checkNotNull(subst); |
Philip P. Moltmann | ebbe2d4 | 2017-07-10 12:07:54 -0700 | [diff] [blame] | 153 | |
Felipe Leme | 906b853 | 2017-07-17 14:56:17 -0700 | [diff] [blame] | 154 | mFields.put(id, new Pair<>(regex, subst)); |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 155 | return this; |
| 156 | } |
| 157 | |
| 158 | /** |
| 159 | * Creates a new {@link CharSequenceTransformation} instance. |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 160 | */ |
| 161 | public CharSequenceTransformation build() { |
| 162 | throwIfDestroyed(); |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 163 | 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 Leme | 906b853 | 2017-07-17 14:56:17 -0700 | [diff] [blame] | 194 | final Pattern[] regexs = new Pattern[size]; |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 195 | final String[] substs = new String[size]; |
Philip P. Moltmann | ebbe2d4 | 2017-07-10 12:07:54 -0700 | [diff] [blame] | 196 | Pair<Pattern, String> pair; |
Felipe Leme | 6fb52a5 | 2018-01-17 16:08:01 -0800 | [diff] [blame] | 197 | int i = 0; |
| 198 | for (Entry<AutofillId, Pair<Pattern, String>> entry : mFields.entrySet()) { |
| 199 | ids[i] = entry.getKey(); |
| 200 | pair = entry.getValue(); |
Felipe Leme | 906b853 | 2017-07-17 14:56:17 -0700 | [diff] [blame] | 201 | regexs[i] = pair.first; |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 202 | substs[i] = pair.second; |
Felipe Leme | 6fb52a5 | 2018-01-17 16:08:01 -0800 | [diff] [blame] | 203 | i++; |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 204 | } |
Felipe Leme | 6fb52a5 | 2018-01-17 16:08:01 -0800 | [diff] [blame] | 205 | |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 206 | parcel.writeParcelableArray(ids, flags); |
Felipe Leme | 906b853 | 2017-07-17 14:56:17 -0700 | [diff] [blame] | 207 | parcel.writeSerializable(regexs); |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 208 | parcel.writeStringArray(substs); |
| 209 | } |
| 210 | |
Jeff Sharkey | 9e8f83d | 2019-02-28 12:06:45 -0700 | [diff] [blame] | 211 | public static final @android.annotation.NonNull Parcelable.Creator<CharSequenceTransformation> CREATOR = |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 212 | new Parcelable.Creator<CharSequenceTransformation>() { |
| 213 | @Override |
| 214 | public CharSequenceTransformation createFromParcel(Parcel parcel) { |
Philip P. Moltmann | ebbe2d4 | 2017-07-10 12:07:54 -0700 | [diff] [blame] | 215 | final AutofillId[] ids = parcel.readParcelableArray(null, AutofillId.class); |
Felipe Leme | 906b853 | 2017-07-17 14:56:17 -0700 | [diff] [blame] | 216 | final Pattern[] regexs = (Pattern[]) parcel.readSerializable(); |
Philip P. Moltmann | ebbe2d4 | 2017-07-10 12:07:54 -0700 | [diff] [blame] | 217 | final String[] substs = parcel.createStringArray(); |
| 218 | |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 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 = |
Philip P. Moltmann | ebbe2d4 | 2017-07-10 12:07:54 -0700 | [diff] [blame] | 223 | new CharSequenceTransformation.Builder(ids[0], regexs[0], substs[0]); |
| 224 | |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 225 | final int size = ids.length; |
Philip P. Moltmann | ebbe2d4 | 2017-07-10 12:07:54 -0700 | [diff] [blame] | 226 | for (int i = 1; i < size; i++) { |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 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 | } |