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