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