blob: 4afda249afea424f6ffd6829d816a4a6ac01d342 [file] [log] [blame]
Felipe Leme979013d2017-06-22 10:59:23 -07001/*
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
17package android.service.autofill;
18
19import static android.view.autofill.Helper.sDebug;
20
Philip P. Moltmannde78fab2017-07-10 16:34:05 -070021import android.annotation.DrawableRes;
Felipe Leme979013d2017-06-22 10:59:23 -070022import android.annotation.NonNull;
Felipe Lemece8f7262017-09-27 12:35:55 -070023import android.annotation.Nullable;
Philip P. Moltmannde78fab2017-07-10 16:34:05 -070024import android.annotation.TestApi;
Felipe Leme979013d2017-06-22 10:59:23 -070025import android.os.Parcel;
26import android.os.Parcelable;
Felipe Lemece8f7262017-09-27 12:35:55 -070027import android.text.TextUtils;
Felipe Leme979013d2017-06-22 10:59:23 -070028import android.util.Log;
29import android.view.autofill.AutofillId;
30import android.widget.ImageView;
31import android.widget.RemoteViews;
32
33import com.android.internal.util.Preconditions;
34
Philip P. Moltmann2a9a7712017-07-11 08:07:27 -070035import java.util.ArrayList;
Philip P. Moltmannde78fab2017-07-10 16:34:05 -070036import java.util.regex.Pattern;
37
Felipe Leme979013d2017-06-22 10:59:23 -070038/**
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">
Felipe Leme906b8532017-07-17 14:56:17 -070046 * new ImageTransformation.Builder(ccNumberId, Pattern.compile("^4815.*$"),
Felipe Lemece8f7262017-09-27 12:35:55 -070047 * 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")
Felipe Leme979013d2017-06-22 10:59:23 -070050 * .build();
51 * </pre>
52 *
53 * <p>There is no imposed limit in the number of options, but keep in mind that regexs are
Felipe Leme7fc29dd2017-07-17 11:38:40 -070054 * 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).
Felipe Leme979013d2017-06-22 10:59:23 -070057 */
Philip P. Moltmann3858aa62017-07-11 15:22:14 -070058public final class ImageTransformation extends InternalTransformation implements Transformation,
59 Parcelable {
Felipe Leme979013d2017-06-22 10:59:23 -070060 private static final String TAG = "ImageTransformation";
61
62 private final AutofillId mId;
Felipe Lemece8f7262017-09-27 12:35:55 -070063 private final ArrayList<Option> mOptions;
Felipe Leme979013d2017-06-22 10:59:23 -070064
65 private ImageTransformation(Builder builder) {
66 mId = builder.mId;
67 mOptions = builder.mOptions;
68 }
69
70 /** @hide */
Philip P. Moltmannde78fab2017-07-10 16:34:05 -070071 @TestApi
Felipe Leme979013d2017-06-22 10:59:23 -070072 @Override
73 public void apply(@NonNull ValueFinder finder, @NonNull RemoteViews parentTemplate,
Felipe Leme22101ca2017-07-18 08:58:50 -070074 int childViewId) throws Exception {
Felipe Leme979013d2017-06-22 10:59:23 -070075 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) {
Felipe Lemea5083c42017-08-08 12:39:04 -070082 Log.d(TAG, size + " multiple options on id " + childViewId + " to compare against");
Felipe Leme979013d2017-06-22 10:59:23 -070083 }
84
85 for (int i = 0; i < size; i++) {
Felipe Lemece8f7262017-09-27 12:35:55 -070086 final Option option = mOptions.get(i);
Felipe Leme22101ca2017-07-18 08:58:50 -070087 try {
Felipe Lemece8f7262017-09-27 12:35:55 -070088 if (option.pattern.matcher(value).matches()) {
Felipe Leme22101ca2017-07-18 08:58:50 -070089 Log.d(TAG, "Found match at " + i + ": " + option);
Felipe Lemece8f7262017-09-27 12:35:55 -070090 parentTemplate.setImageViewResource(childViewId, option.resId);
91 if (option.contentDescription != null) {
92 parentTemplate.setContentDescription(childViewId,
93 option.contentDescription);
94 }
Felipe Leme22101ca2017-07-18 08:58:50 -070095 return;
96 }
97 } catch (Exception e) {
98 // Do not log full exception to avoid PII leaking
Felipe Lemece8f7262017-09-27 12:35:55 -070099 Log.w(TAG, "Error matching regex #" + i + "(" + option.pattern + ") on id "
100 + option.resId + ": " + e.getClass());
Felipe Leme22101ca2017-07-18 08:58:50 -0700101 throw e;
102
Felipe Leme979013d2017-06-22 10:59:23 -0700103 }
104 }
Felipe Leme22101ca2017-07-18 08:58:50 -0700105 if (sDebug) Log.d(TAG, "No match for " + value);
Felipe Leme979013d2017-06-22 10:59:23 -0700106 }
107
108 /**
109 * Builder for {@link ImageTransformation} objects.
110 */
111 public static class Builder {
112 private final AutofillId mId;
Felipe Lemece8f7262017-09-27 12:35:55 -0700113 private final ArrayList<Option> mOptions = new ArrayList<>();
Felipe Leme979013d2017-06-22 10:59:23 -0700114 private boolean mDestroyed;
115
116 /**
Felipe Lemece8f7262017-09-27 12:35:55 -0700117 * Creates a new builder for a autofill id and add a first option.
Felipe Leme979013d2017-06-22 10:59:23 -0700118 *
119 * @param id id of the screen field that will be used to evaluate whether the image should
120 * be used.
Felipe Leme906b8532017-07-17 14:56:17 -0700121 * @param regex regular expression defining what should be matched to use this image.
Philip P. Moltmannde78fab2017-07-10 16:34:05 -0700122 * @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.
Felipe Lemece8f7262017-09-27 12:35:55 -0700124 *
125 * @deprecated use
126 * {@link #ImageTransformation.Builder(AutofillId, Pattern, int, CharSequence)} instead.
Felipe Leme979013d2017-06-22 10:59:23 -0700127 */
Felipe Lemece8f7262017-09-27 12:35:55 -0700128 @Deprecated
Felipe Leme906b8532017-07-17 14:56:17 -0700129 public Builder(@NonNull AutofillId id, @NonNull Pattern regex, @DrawableRes int resId) {
Felipe Leme979013d2017-06-22 10:59:23 -0700130 mId = Preconditions.checkNotNull(id);
Philip P. Moltmannde78fab2017-07-10 16:34:05 -0700131 addOption(regex, resId);
Felipe Leme979013d2017-06-22 10:59:23 -0700132 }
133
134 /**
Felipe Lemece8f7262017-09-27 12:35:55 -0700135 * 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 /**
Felipe Leme979013d2017-06-22 10:59:23 -0700151 * Adds an option to replace the child view with a different image when the regex matches.
152 *
Felipe Leme906b8532017-07-17 14:56:17 -0700153 * @param regex regular expression defining what should be matched to use this image.
Felipe Leme979013d2017-06-22 10:59:23 -0700154 * @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
Felipe Lemece8f7262017-09-27 12:35:55 -0700158 *
159 * @deprecated use {@link #addOption(Pattern, int, CharSequence)} instead.
Felipe Leme979013d2017-06-22 10:59:23 -0700160 */
Felipe Lemece8f7262017-09-27 12:35:55 -0700161 @Deprecated
Felipe Leme906b8532017-07-17 14:56:17 -0700162 public Builder addOption(@NonNull Pattern regex, @DrawableRes int resId) {
Felipe Lemece8f7262017-09-27 12:35:55 -0700163 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) {
Felipe Leme979013d2017-06-22 10:59:23 -0700186 throwIfDestroyed();
Philip P. Moltmannde78fab2017-07-10 16:34:05 -0700187
Felipe Leme906b8532017-07-17 14:56:17 -0700188 Preconditions.checkNotNull(regex);
Philip P. Moltmannde78fab2017-07-10 16:34:05 -0700189 Preconditions.checkArgument(resId != 0);
190
Felipe Lemece8f7262017-09-27 12:35:55 -0700191 mOptions.add(new Option(regex, resId, contentDescription));
Felipe Leme979013d2017-06-22 10:59:23 -0700192 }
193
Felipe Lemece8f7262017-09-27 12:35:55 -0700194
Felipe Leme979013d2017-06-22 10:59:23 -0700195 /**
196 * Creates a new {@link ImageTransformation} instance.
Felipe Leme979013d2017-06-22 10:59:23 -0700197 */
198 public ImageTransformation build() {
199 throwIfDestroyed();
Felipe Leme979013d2017-06-22 10:59:23 -0700200 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 }
Felipe Leme979013d2017-06-22 10:59:23 -0700226 @Override
227 public void writeToParcel(Parcel parcel, int flags) {
228 parcel.writeParcelable(mId, flags);
Philip P. Moltmannde78fab2017-07-10 16:34:05 -0700229
Felipe Leme979013d2017-06-22 10:59:23 -0700230 final int size = mOptions.size();
Felipe Lemece8f7262017-09-27 12:35:55 -0700231 final Pattern[] patterns = new Pattern[size];
Felipe Leme979013d2017-06-22 10:59:23 -0700232 final int[] resIds = new int[size];
Felipe Lemece8f7262017-09-27 12:35:55 -0700233 final CharSequence[] contentDescriptions = new String[size];
Felipe Leme979013d2017-06-22 10:59:23 -0700234 for (int i = 0; i < size; i++) {
Felipe Lemece8f7262017-09-27 12:35:55 -0700235 final Option option = mOptions.get(i);
236 patterns[i] = option.pattern;
237 resIds[i] = option.resId;
238 contentDescriptions[i] = option.contentDescription;
Felipe Leme979013d2017-06-22 10:59:23 -0700239 }
Felipe Lemece8f7262017-09-27 12:35:55 -0700240 parcel.writeSerializable(patterns);
Felipe Leme979013d2017-06-22 10:59:23 -0700241 parcel.writeIntArray(resIds);
Felipe Lemece8f7262017-09-27 12:35:55 -0700242 parcel.writeCharSequenceArray(contentDescriptions);
Felipe Leme979013d2017-06-22 10:59:23 -0700243 }
244
245 public static final Parcelable.Creator<ImageTransformation> CREATOR =
246 new Parcelable.Creator<ImageTransformation>() {
247 @Override
248 public ImageTransformation createFromParcel(Parcel parcel) {
Philip P. Moltmannde78fab2017-07-10 16:34:05 -0700249 final AutofillId id = parcel.readParcelable(null);
250
Felipe Leme906b8532017-07-17 14:56:17 -0700251 final Pattern[] regexs = (Pattern[]) parcel.readSerializable();
Philip P. Moltmannde78fab2017-07-10 16:34:05 -0700252 final int[] resIds = parcel.createIntArray();
Felipe Lemece8f7262017-09-27 12:35:55 -0700253 final CharSequence[] contentDescriptions = parcel.readCharSequenceArray();
Philip P. Moltmannde78fab2017-07-10 16:34:05 -0700254
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.
Felipe Lemece8f7262017-09-27 12:35:55 -0700257 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]);
Philip P. Moltmannde78fab2017-07-10 16:34:05 -0700261
262 final int size = regexs.length;
263 for (int i = 1; i < size; i++) {
Felipe Lemece8f7262017-09-27 12:35:55 -0700264 if (contentDescriptions[i] != null) {
265 builder.addOption(regexs[i], resIds[i], contentDescriptions[i]);
266 } else {
267 builder.addOption(regexs[i], resIds[i]);
268 }
Felipe Leme979013d2017-06-22 10:59:23 -0700269 }
Philip P. Moltmannde78fab2017-07-10 16:34:05 -0700270
Felipe Leme979013d2017-06-22 10:59:23 -0700271 return builder.build();
272 }
273
274 @Override
275 public ImageTransformation[] newArray(int size) {
276 return new ImageTransformation[size];
277 }
278 };
Felipe Lemece8f7262017-09-27 12:35:55 -0700279
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 }
Felipe Leme979013d2017-06-22 10:59:23 -0700291}