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 | |
Philip P. Moltmann | de78fab | 2017-07-10 16:34:05 -0700 | [diff] [blame] | 21 | import android.annotation.DrawableRes; |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 22 | import android.annotation.NonNull; |
Felipe Leme | ce8f726 | 2017-09-27 12:35:55 -0700 | [diff] [blame] | 23 | import android.annotation.Nullable; |
Philip P. Moltmann | de78fab | 2017-07-10 16:34:05 -0700 | [diff] [blame] | 24 | import android.annotation.TestApi; |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 25 | import android.os.Parcel; |
| 26 | import android.os.Parcelable; |
Felipe Leme | ce8f726 | 2017-09-27 12:35:55 -0700 | [diff] [blame] | 27 | import android.text.TextUtils; |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 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 | |
Philip P. Moltmann | 2a9a771 | 2017-07-11 08:07:27 -0700 | [diff] [blame] | 35 | import java.util.ArrayList; |
Philip P. Moltmann | de78fab | 2017-07-10 16:34:05 -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 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 Leme | 906b853 | 2017-07-17 14:56:17 -0700 | [diff] [blame] | 46 | * new ImageTransformation.Builder(ccNumberId, Pattern.compile("^4815.*$"), |
Felipe Leme | ce8f726 | 2017-09-27 12:35:55 -0700 | [diff] [blame] | 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") |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 50 | * .build(); |
| 51 | * </pre> |
| 52 | * |
| 53 | * <p>There is no imposed limit in the number of options, but keep in mind that regexs are |
Felipe Leme | 7fc29dd | 2017-07-17 11:38:40 -0700 | [diff] [blame] | 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). |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 57 | */ |
Philip P. Moltmann | 3858aa6 | 2017-07-11 15:22:14 -0700 | [diff] [blame] | 58 | public final class ImageTransformation extends InternalTransformation implements Transformation, |
| 59 | Parcelable { |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 60 | private static final String TAG = "ImageTransformation"; |
| 61 | |
| 62 | private final AutofillId mId; |
Felipe Leme | ce8f726 | 2017-09-27 12:35:55 -0700 | [diff] [blame] | 63 | private final ArrayList<Option> mOptions; |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 64 | |
| 65 | private ImageTransformation(Builder builder) { |
| 66 | mId = builder.mId; |
| 67 | mOptions = builder.mOptions; |
| 68 | } |
| 69 | |
| 70 | /** @hide */ |
Philip P. Moltmann | de78fab | 2017-07-10 16:34:05 -0700 | [diff] [blame] | 71 | @TestApi |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 72 | @Override |
| 73 | public void apply(@NonNull ValueFinder finder, @NonNull RemoteViews parentTemplate, |
Felipe Leme | 22101ca | 2017-07-18 08:58:50 -0700 | [diff] [blame] | 74 | int childViewId) throws Exception { |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 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) { |
Felipe Leme | a5083c4 | 2017-08-08 12:39:04 -0700 | [diff] [blame] | 82 | Log.d(TAG, size + " multiple options on id " + childViewId + " to compare against"); |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 83 | } |
| 84 | |
| 85 | for (int i = 0; i < size; i++) { |
Felipe Leme | ce8f726 | 2017-09-27 12:35:55 -0700 | [diff] [blame] | 86 | final Option option = mOptions.get(i); |
Felipe Leme | 22101ca | 2017-07-18 08:58:50 -0700 | [diff] [blame] | 87 | try { |
Felipe Leme | ce8f726 | 2017-09-27 12:35:55 -0700 | [diff] [blame] | 88 | if (option.pattern.matcher(value).matches()) { |
Felipe Leme | 22101ca | 2017-07-18 08:58:50 -0700 | [diff] [blame] | 89 | Log.d(TAG, "Found match at " + i + ": " + option); |
Felipe Leme | ce8f726 | 2017-09-27 12:35:55 -0700 | [diff] [blame] | 90 | parentTemplate.setImageViewResource(childViewId, option.resId); |
| 91 | if (option.contentDescription != null) { |
| 92 | parentTemplate.setContentDescription(childViewId, |
| 93 | option.contentDescription); |
| 94 | } |
Felipe Leme | 22101ca | 2017-07-18 08:58:50 -0700 | [diff] [blame] | 95 | return; |
| 96 | } |
| 97 | } catch (Exception e) { |
| 98 | // Do not log full exception to avoid PII leaking |
Felipe Leme | ce8f726 | 2017-09-27 12:35:55 -0700 | [diff] [blame] | 99 | Log.w(TAG, "Error matching regex #" + i + "(" + option.pattern + ") on id " |
| 100 | + option.resId + ": " + e.getClass()); |
Felipe Leme | 22101ca | 2017-07-18 08:58:50 -0700 | [diff] [blame] | 101 | throw e; |
| 102 | |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 103 | } |
| 104 | } |
Felipe Leme | 22101ca | 2017-07-18 08:58:50 -0700 | [diff] [blame] | 105 | if (sDebug) Log.d(TAG, "No match for " + value); |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 106 | } |
| 107 | |
| 108 | /** |
| 109 | * Builder for {@link ImageTransformation} objects. |
| 110 | */ |
| 111 | public static class Builder { |
| 112 | private final AutofillId mId; |
Felipe Leme | ce8f726 | 2017-09-27 12:35:55 -0700 | [diff] [blame] | 113 | private final ArrayList<Option> mOptions = new ArrayList<>(); |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 114 | private boolean mDestroyed; |
| 115 | |
| 116 | /** |
Felipe Leme | ce8f726 | 2017-09-27 12:35:55 -0700 | [diff] [blame] | 117 | * Creates a new builder for a autofill id and add a first option. |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 118 | * |
| 119 | * @param id id of the screen field that will be used to evaluate whether the image should |
| 120 | * be used. |
Felipe Leme | 906b853 | 2017-07-17 14:56:17 -0700 | [diff] [blame] | 121 | * @param regex regular expression defining what should be matched to use this image. |
Philip P. Moltmann | de78fab | 2017-07-10 16:34:05 -0700 | [diff] [blame] | 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. |
Felipe Leme | ce8f726 | 2017-09-27 12:35:55 -0700 | [diff] [blame] | 124 | * |
| 125 | * @deprecated use |
| 126 | * {@link #ImageTransformation.Builder(AutofillId, Pattern, int, CharSequence)} instead. |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 127 | */ |
Felipe Leme | ce8f726 | 2017-09-27 12:35:55 -0700 | [diff] [blame] | 128 | @Deprecated |
Felipe Leme | 906b853 | 2017-07-17 14:56:17 -0700 | [diff] [blame] | 129 | public Builder(@NonNull AutofillId id, @NonNull Pattern regex, @DrawableRes int resId) { |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 130 | mId = Preconditions.checkNotNull(id); |
Philip P. Moltmann | de78fab | 2017-07-10 16:34:05 -0700 | [diff] [blame] | 131 | addOption(regex, resId); |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 132 | } |
| 133 | |
| 134 | /** |
Felipe Leme | ce8f726 | 2017-09-27 12:35:55 -0700 | [diff] [blame] | 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 | /** |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 151 | * Adds an option to replace the child view with a different image when the regex matches. |
| 152 | * |
Felipe Leme | 906b853 | 2017-07-17 14:56:17 -0700 | [diff] [blame] | 153 | * @param regex regular expression defining what should be matched to use this image. |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 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 |
Felipe Leme | ce8f726 | 2017-09-27 12:35:55 -0700 | [diff] [blame] | 158 | * |
| 159 | * @deprecated use {@link #addOption(Pattern, int, CharSequence)} instead. |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 160 | */ |
Felipe Leme | ce8f726 | 2017-09-27 12:35:55 -0700 | [diff] [blame] | 161 | @Deprecated |
Felipe Leme | 906b853 | 2017-07-17 14:56:17 -0700 | [diff] [blame] | 162 | public Builder addOption(@NonNull Pattern regex, @DrawableRes int resId) { |
Felipe Leme | ce8f726 | 2017-09-27 12:35:55 -0700 | [diff] [blame] | 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) { |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 186 | throwIfDestroyed(); |
Philip P. Moltmann | de78fab | 2017-07-10 16:34:05 -0700 | [diff] [blame] | 187 | |
Felipe Leme | 906b853 | 2017-07-17 14:56:17 -0700 | [diff] [blame] | 188 | Preconditions.checkNotNull(regex); |
Philip P. Moltmann | de78fab | 2017-07-10 16:34:05 -0700 | [diff] [blame] | 189 | Preconditions.checkArgument(resId != 0); |
| 190 | |
Felipe Leme | ce8f726 | 2017-09-27 12:35:55 -0700 | [diff] [blame] | 191 | mOptions.add(new Option(regex, resId, contentDescription)); |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 192 | } |
| 193 | |
Felipe Leme | ce8f726 | 2017-09-27 12:35:55 -0700 | [diff] [blame] | 194 | |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 195 | /** |
| 196 | * Creates a new {@link ImageTransformation} instance. |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 197 | */ |
| 198 | public ImageTransformation build() { |
| 199 | throwIfDestroyed(); |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 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 | } |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 226 | @Override |
| 227 | public void writeToParcel(Parcel parcel, int flags) { |
| 228 | parcel.writeParcelable(mId, flags); |
Philip P. Moltmann | de78fab | 2017-07-10 16:34:05 -0700 | [diff] [blame] | 229 | |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 230 | final int size = mOptions.size(); |
Felipe Leme | ce8f726 | 2017-09-27 12:35:55 -0700 | [diff] [blame] | 231 | final Pattern[] patterns = new Pattern[size]; |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 232 | final int[] resIds = new int[size]; |
Felipe Leme | ce8f726 | 2017-09-27 12:35:55 -0700 | [diff] [blame] | 233 | final CharSequence[] contentDescriptions = new String[size]; |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 234 | for (int i = 0; i < size; i++) { |
Felipe Leme | ce8f726 | 2017-09-27 12:35:55 -0700 | [diff] [blame] | 235 | final Option option = mOptions.get(i); |
| 236 | patterns[i] = option.pattern; |
| 237 | resIds[i] = option.resId; |
| 238 | contentDescriptions[i] = option.contentDescription; |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 239 | } |
Felipe Leme | ce8f726 | 2017-09-27 12:35:55 -0700 | [diff] [blame] | 240 | parcel.writeSerializable(patterns); |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 241 | parcel.writeIntArray(resIds); |
Felipe Leme | ce8f726 | 2017-09-27 12:35:55 -0700 | [diff] [blame] | 242 | parcel.writeCharSequenceArray(contentDescriptions); |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 243 | } |
| 244 | |
| 245 | public static final Parcelable.Creator<ImageTransformation> CREATOR = |
| 246 | new Parcelable.Creator<ImageTransformation>() { |
| 247 | @Override |
| 248 | public ImageTransformation createFromParcel(Parcel parcel) { |
Philip P. Moltmann | de78fab | 2017-07-10 16:34:05 -0700 | [diff] [blame] | 249 | final AutofillId id = parcel.readParcelable(null); |
| 250 | |
Felipe Leme | 906b853 | 2017-07-17 14:56:17 -0700 | [diff] [blame] | 251 | final Pattern[] regexs = (Pattern[]) parcel.readSerializable(); |
Philip P. Moltmann | de78fab | 2017-07-10 16:34:05 -0700 | [diff] [blame] | 252 | final int[] resIds = parcel.createIntArray(); |
Felipe Leme | ce8f726 | 2017-09-27 12:35:55 -0700 | [diff] [blame] | 253 | final CharSequence[] contentDescriptions = parcel.readCharSequenceArray(); |
Philip P. Moltmann | de78fab | 2017-07-10 16:34:05 -0700 | [diff] [blame] | 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. |
Felipe Leme | ce8f726 | 2017-09-27 12:35:55 -0700 | [diff] [blame] | 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]); |
Philip P. Moltmann | de78fab | 2017-07-10 16:34:05 -0700 | [diff] [blame] | 261 | |
| 262 | final int size = regexs.length; |
| 263 | for (int i = 1; i < size; i++) { |
Felipe Leme | ce8f726 | 2017-09-27 12:35:55 -0700 | [diff] [blame] | 264 | if (contentDescriptions[i] != null) { |
| 265 | builder.addOption(regexs[i], resIds[i], contentDescriptions[i]); |
| 266 | } else { |
| 267 | builder.addOption(regexs[i], resIds[i]); |
| 268 | } |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 269 | } |
Philip P. Moltmann | de78fab | 2017-07-10 16:34:05 -0700 | [diff] [blame] | 270 | |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 271 | return builder.build(); |
| 272 | } |
| 273 | |
| 274 | @Override |
| 275 | public ImageTransformation[] newArray(int size) { |
| 276 | return new ImageTransformation[size]; |
| 277 | } |
| 278 | }; |
Felipe Leme | ce8f726 | 2017-09-27 12:35:55 -0700 | [diff] [blame] | 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 | } |
Felipe Leme | 979013d | 2017-06-22 10:59:23 -0700 | [diff] [blame] | 291 | } |