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.DrawableRes; 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.annotation.TestApi; 25 import android.os.Parcel; 26 import android.os.Parcelable; 27 import android.text.TextUtils; 28 import android.util.Log; 29 import android.view.autofill.AutofillId; 30 import android.widget.ImageView; 31 import android.widget.RemoteViews; 32 33 import com.android.internal.util.Preconditions; 34 35 import java.util.ArrayList; 36 import java.util.Objects; 37 import java.util.regex.Pattern; 38 39 /** 40 * Replaces the content of a child {@link ImageView} of a 41 * {@link RemoteViews presentation template} with the first image that matches a regular expression 42 * (regex). 43 * 44 * <p>Typically used to display credit card logos. Example: 45 * 46 * <pre class="prettyprint"> 47 * new ImageTransformation.Builder(ccNumberId, Pattern.compile("^4815.*$"), 48 * R.drawable.ic_credit_card_logo1, "Brand 1") 49 * .addOption(Pattern.compile("^1623.*$"), R.drawable.ic_credit_card_logo2, "Brand 2") 50 * .addOption(Pattern.compile("^42.*$"), R.drawable.ic_credit_card_logo3, "Brand 3") 51 * .build(); 52 * </pre> 53 * 54 * <p>There is no imposed limit in the number of options, but keep in mind that regexs are 55 * expensive to evaluate, so use the minimum number of regexs and add the most common first 56 * (for example, if this is a tranformation for a credit card logo and the most common credit card 57 * issuers are banks X and Y, add the regexes that resolves these 2 banks first). 58 */ 59 public final class ImageTransformation extends InternalTransformation implements Transformation, 60 Parcelable { 61 private static final String TAG = "ImageTransformation"; 62 63 private final AutofillId mId; 64 private final ArrayList<Option> mOptions; 65 ImageTransformation(Builder builder)66 private ImageTransformation(Builder builder) { 67 mId = builder.mId; 68 mOptions = builder.mOptions; 69 } 70 71 /** @hide */ 72 @TestApi 73 @Override apply(@onNull ValueFinder finder, @NonNull RemoteViews parentTemplate, int childViewId)74 public void apply(@NonNull ValueFinder finder, @NonNull RemoteViews parentTemplate, 75 int childViewId) throws Exception { 76 final String value = finder.findByAutofillId(mId); 77 if (value == null) { 78 Log.w(TAG, "No view for id " + mId); 79 return; 80 } 81 final int size = mOptions.size(); 82 if (sDebug) { 83 Log.d(TAG, size + " multiple options on id " + childViewId + " to compare against"); 84 } 85 86 for (int i = 0; i < size; i++) { 87 final Option option = mOptions.get(i); 88 try { 89 if (option.pattern.matcher(value).matches()) { 90 Log.d(TAG, "Found match at " + i + ": " + option); 91 parentTemplate.setImageViewResource(childViewId, option.resId); 92 if (option.contentDescription != null) { 93 parentTemplate.setContentDescription(childViewId, 94 option.contentDescription); 95 } 96 return; 97 } 98 } catch (Exception e) { 99 // Do not log full exception to avoid PII leaking 100 Log.w(TAG, "Error matching regex #" + i + "(" + option.pattern + ") on id " 101 + option.resId + ": " + e.getClass()); 102 throw e; 103 104 } 105 } 106 if (sDebug) Log.d(TAG, "No match for " + value); 107 } 108 109 /** 110 * Builder for {@link ImageTransformation} objects. 111 */ 112 public static class Builder { 113 private final AutofillId mId; 114 private final ArrayList<Option> mOptions = new ArrayList<>(); 115 private boolean mDestroyed; 116 117 /** 118 * Creates a new builder for a autofill id and add a first option. 119 * 120 * @param id id of the screen field that will be used to evaluate whether the image should 121 * be used. 122 * @param regex regular expression defining what should be matched to use this image. 123 * @param resId resource id of the image (in the autofill service's package). The 124 * {@link RemoteViews presentation} must contain a {@link ImageView} child with that id. 125 * 126 * @deprecated use 127 * {@link #Builder(AutofillId, Pattern, int, CharSequence)} instead. 128 */ 129 @Deprecated Builder(@onNull AutofillId id, @NonNull Pattern regex, @DrawableRes int resId)130 public Builder(@NonNull AutofillId id, @NonNull Pattern regex, @DrawableRes int resId) { 131 mId = Objects.requireNonNull(id); 132 addOption(regex, resId); 133 } 134 135 /** 136 * Creates a new builder for a autofill id and add a first option. 137 * 138 * @param id id of the screen field that will be used to evaluate whether the image should 139 * be used. 140 * @param regex regular expression defining what should be matched to use this image. 141 * @param resId resource id of the image (in the autofill service's package). The 142 * {@link RemoteViews presentation} must contain a {@link ImageView} child with that id. 143 * @param contentDescription content description to be applied in the child view. 144 */ Builder(@onNull AutofillId id, @NonNull Pattern regex, @DrawableRes int resId, @NonNull CharSequence contentDescription)145 public Builder(@NonNull AutofillId id, @NonNull Pattern regex, @DrawableRes int resId, 146 @NonNull CharSequence contentDescription) { 147 mId = Objects.requireNonNull(id); 148 addOption(regex, resId, contentDescription); 149 } 150 151 /** 152 * Adds an option to replace the child view with a different image when the regex matches. 153 * 154 * @param regex regular expression defining what should be matched to use this image. 155 * @param resId resource id of the image (in the autofill service's package). The 156 * {@link RemoteViews presentation} must contain a {@link ImageView} child with that id. 157 * 158 * @return this build 159 * 160 * @deprecated use {@link #addOption(Pattern, int, CharSequence)} instead. 161 */ 162 @Deprecated addOption(@onNull Pattern regex, @DrawableRes int resId)163 public Builder addOption(@NonNull Pattern regex, @DrawableRes int resId) { 164 addOptionInternal(regex, resId, null); 165 return this; 166 } 167 168 /** 169 * Adds an option to replace the child view with a different image and content description 170 * when the regex matches. 171 * 172 * @param regex regular expression defining what should be matched to use this image. 173 * @param resId resource id of the image (in the autofill service's package). The 174 * {@link RemoteViews presentation} must contain a {@link ImageView} child with that id. 175 * @param contentDescription content description to be applied in the child view. 176 * 177 * @return this build 178 */ addOption(@onNull Pattern regex, @DrawableRes int resId, @NonNull CharSequence contentDescription)179 public Builder addOption(@NonNull Pattern regex, @DrawableRes int resId, 180 @NonNull CharSequence contentDescription) { 181 addOptionInternal(regex, resId, Objects.requireNonNull(contentDescription)); 182 return this; 183 } 184 addOptionInternal(@onNull Pattern regex, @DrawableRes int resId, @Nullable CharSequence contentDescription)185 private void addOptionInternal(@NonNull Pattern regex, @DrawableRes int resId, 186 @Nullable CharSequence contentDescription) { 187 throwIfDestroyed(); 188 189 Objects.requireNonNull(regex); 190 Preconditions.checkArgument(resId != 0); 191 192 mOptions.add(new Option(regex, resId, contentDescription)); 193 } 194 195 196 /** 197 * Creates a new {@link ImageTransformation} instance. 198 */ build()199 public ImageTransformation build() { 200 throwIfDestroyed(); 201 mDestroyed = true; 202 return new ImageTransformation(this); 203 } 204 throwIfDestroyed()205 private void throwIfDestroyed() { 206 Preconditions.checkState(!mDestroyed, "Already called build()"); 207 } 208 } 209 210 ///////////////////////////////////// 211 // Object "contract" methods. // 212 ///////////////////////////////////// 213 @Override toString()214 public String toString() { 215 if (!sDebug) return super.toString(); 216 217 return "ImageTransformation: [id=" + mId + ", options=" + mOptions + "]"; 218 } 219 220 ///////////////////////////////////// 221 // Parcelable "contract" methods. // 222 ///////////////////////////////////// 223 @Override describeContents()224 public int describeContents() { 225 return 0; 226 } 227 @Override writeToParcel(Parcel parcel, int flags)228 public void writeToParcel(Parcel parcel, int flags) { 229 parcel.writeParcelable(mId, flags); 230 231 final int size = mOptions.size(); 232 final Pattern[] patterns = new Pattern[size]; 233 final int[] resIds = new int[size]; 234 final CharSequence[] contentDescriptions = new String[size]; 235 for (int i = 0; i < size; i++) { 236 final Option option = mOptions.get(i); 237 patterns[i] = option.pattern; 238 resIds[i] = option.resId; 239 contentDescriptions[i] = option.contentDescription; 240 } 241 parcel.writeSerializable(patterns); 242 parcel.writeIntArray(resIds); 243 parcel.writeCharSequenceArray(contentDescriptions); 244 } 245 246 public static final @android.annotation.NonNull Parcelable.Creator<ImageTransformation> CREATOR = 247 new Parcelable.Creator<ImageTransformation>() { 248 @Override 249 public ImageTransformation createFromParcel(Parcel parcel) { 250 final AutofillId id = parcel.readParcelable(null, android.view.autofill.AutofillId.class); 251 252 final Pattern[] regexs = (Pattern[]) parcel.readSerializable(); 253 final int[] resIds = parcel.createIntArray(); 254 final CharSequence[] contentDescriptions = parcel.readCharSequenceArray(); 255 256 // Always go through the builder to ensure the data ingested by the system obeys the 257 // contract of the builder to avoid attacks using specially crafted parcels. 258 final CharSequence contentDescription = contentDescriptions[0]; 259 final ImageTransformation.Builder builder = (contentDescription != null) 260 ? new ImageTransformation.Builder(id, regexs[0], resIds[0], contentDescription) 261 : new ImageTransformation.Builder(id, regexs[0], resIds[0]); 262 263 final int size = regexs.length; 264 for (int i = 1; i < size; i++) { 265 if (contentDescriptions[i] != null) { 266 builder.addOption(regexs[i], resIds[i], contentDescriptions[i]); 267 } else { 268 builder.addOption(regexs[i], resIds[i]); 269 } 270 } 271 272 return builder.build(); 273 } 274 275 @Override 276 public ImageTransformation[] newArray(int size) { 277 return new ImageTransformation[size]; 278 } 279 }; 280 281 private static final class Option { 282 public final Pattern pattern; 283 public final int resId; 284 public final CharSequence contentDescription; 285 Option(Pattern pattern, int resId, CharSequence contentDescription)286 Option(Pattern pattern, int resId, CharSequence contentDescription) { 287 this.pattern = pattern; 288 this.resId = resId; 289 this.contentDescription = TextUtils.trimNoCopySpans(contentDescription); 290 } 291 } 292 } 293