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.NonNull; 22 import android.annotation.Nullable; 23 import android.annotation.TestApi; 24 import android.app.Activity; 25 import android.app.PendingIntent; 26 import android.os.Parcel; 27 import android.os.Parcelable; 28 import android.util.Pair; 29 import android.util.SparseArray; 30 import android.widget.RemoteViews; 31 32 import com.android.internal.util.Preconditions; 33 34 import java.util.ArrayList; 35 36 /** 37 * Defines a custom description for the autofill save UI. 38 * 39 * <p>This is useful when the autofill service needs to show a detailed view of what would be saved; 40 * for example, when the screen contains a credit card, it could display a logo of the credit card 41 * bank, the last four digits of the credit card number, and its expiration number. 42 * 43 * <p>A custom description is made of 2 parts: 44 * <ul> 45 * <li>A {@link RemoteViews presentation template} containing children views. 46 * <li>{@link Transformation Transformations} to populate the children views. 47 * </ul> 48 * 49 * <p>For the credit card example mentioned above, the (simplified) template would be: 50 * 51 * <pre class="prettyprint"> 52 * <LinearLayout> 53 * <ImageView android:id="@+id/templateccLogo"/> 54 * <TextView android:id="@+id/templateCcNumber"/> 55 * <TextView android:id="@+id/templateExpDate"/> 56 * </LinearLayout> 57 * </pre> 58 * 59 * <p>Which in code translates to: 60 * 61 * <pre class="prettyprint"> 62 * CustomDescription.Builder buider = new Builder(new RemoteViews(pgkName, R.layout.cc_template); 63 * </pre> 64 * 65 * <p>Then the value of each of the 3 children would be changed at runtime based on the the value of 66 * the screen fields and the {@link Transformation Transformations}: 67 * 68 * <pre class="prettyprint"> 69 * // Image child - different logo for each bank, based on credit card prefix 70 * builder.addChild(R.id.templateccLogo, 71 * new ImageTransformation.Builder(ccNumberId) 72 * .addOption(Pattern.compile("^4815.*$"), R.drawable.ic_credit_card_logo1) 73 * .addOption(Pattern.compile("^1623.*$"), R.drawable.ic_credit_card_logo2) 74 * .addOption(Pattern.compile("^42.*$"), R.drawable.ic_credit_card_logo3) 75 * .build(); 76 * // Masked credit card number (as .....LAST_4_DIGITS) 77 * builder.addChild(R.id.templateCcNumber, new CharSequenceTransformation 78 * .Builder(ccNumberId, Pattern.compile("^.*(\\d\\d\\d\\d)$"), "...$1") 79 * .build(); 80 * // Expiration date as MM / YYYY: 81 * builder.addChild(R.id.templateExpDate, new CharSequenceTransformation 82 * .Builder(ccExpMonthId, Pattern.compile("^(\\d\\d)$"), "Exp: $1") 83 * .addField(ccExpYearId, Pattern.compile("^(\\d\\d)$"), "/$1") 84 * .build(); 85 * </pre> 86 * 87 * <p>See {@link ImageTransformation}, {@link CharSequenceTransformation} for more info about these 88 * transformations. 89 */ 90 public final class CustomDescription implements Parcelable { 91 92 private final RemoteViews mPresentation; 93 private final ArrayList<Pair<Integer, InternalTransformation>> mTransformations; 94 private final ArrayList<Pair<InternalValidator, BatchUpdates>> mUpdates; 95 private final SparseArray<InternalOnClickAction> mActions; 96 CustomDescription(Builder builder)97 private CustomDescription(Builder builder) { 98 mPresentation = builder.mPresentation; 99 mTransformations = builder.mTransformations; 100 mUpdates = builder.mUpdates; 101 mActions = builder.mActions; 102 } 103 104 /** @hide */ 105 @Nullable getPresentation()106 public RemoteViews getPresentation() { 107 return mPresentation; 108 } 109 110 /** @hide */ 111 @Nullable getTransformations()112 public ArrayList<Pair<Integer, InternalTransformation>> getTransformations() { 113 return mTransformations; 114 } 115 116 /** @hide */ 117 @Nullable getUpdates()118 public ArrayList<Pair<InternalValidator, BatchUpdates>> getUpdates() { 119 return mUpdates; 120 } 121 122 /** @hide */ 123 @Nullable 124 @TestApi getActions()125 public SparseArray<InternalOnClickAction> getActions() { 126 return mActions; 127 } 128 129 /** 130 * Builder for {@link CustomDescription} objects. 131 */ 132 public static class Builder { 133 private final RemoteViews mPresentation; 134 135 private boolean mDestroyed; 136 private ArrayList<Pair<Integer, InternalTransformation>> mTransformations; 137 private ArrayList<Pair<InternalValidator, BatchUpdates>> mUpdates; 138 private SparseArray<InternalOnClickAction> mActions; 139 140 /** 141 * Default constructor. 142 * 143 * <p><b>Note:</b> If any child view of presentation triggers a 144 * {@link RemoteViews#setOnClickPendingIntent(int, android.app.PendingIntent) pending intent 145 * on click}, such {@link PendingIntent} must follow the restrictions below, otherwise 146 * it might not be triggered or the autofill save UI might not be shown when its activity 147 * is finished: 148 * <ul> 149 * <li>It cannot be created with the {@link PendingIntent#FLAG_IMMUTABLE} flag. 150 * <li>It must be a PendingIntent for an {@link Activity}. 151 * <li>The activity must call {@link Activity#finish()} when done. 152 * <li>The activity should not launch other activities. 153 * </ul> 154 * 155 * @param parentPresentation template presentation with (optional) children views. 156 * @throws NullPointerException if {@code parentPresentation} is null (on Android 157 * {@link android.os.Build.VERSION_CODES#P} or higher). 158 */ Builder(@onNull RemoteViews parentPresentation)159 public Builder(@NonNull RemoteViews parentPresentation) { 160 mPresentation = Preconditions.checkNotNull(parentPresentation); 161 } 162 163 /** 164 * Adds a transformation to replace the value of a child view with the fields in the 165 * screen. 166 * 167 * <p>When multiple transformations are added for the same child view, they will be applied 168 * in the same order as added. 169 * 170 * @param id view id of the children view. 171 * @param transformation an implementation provided by the Android System. 172 * 173 * @return this builder. 174 * 175 * @throws IllegalArgumentException if {@code transformation} is not a class provided 176 * by the Android System. 177 * @throws IllegalStateException if {@link #build()} was already called. 178 */ 179 @NonNull addChild(int id, @NonNull Transformation transformation)180 public Builder addChild(int id, @NonNull Transformation transformation) { 181 throwIfDestroyed(); 182 Preconditions.checkArgument((transformation instanceof InternalTransformation), 183 "not provided by Android System: %s", transformation); 184 if (mTransformations == null) { 185 mTransformations = new ArrayList<>(); 186 } 187 mTransformations.add(new Pair<>(id, (InternalTransformation) transformation)); 188 return this; 189 } 190 191 /** 192 * Updates the {@link RemoteViews presentation template} when a condition is satisfied by 193 * applying a series of remote view operations. This allows dynamic customization of the 194 * portion of the save UI that is controlled by the autofill service. Such dynamic 195 * customization is based on the content of target views. 196 * 197 * <p>The updates are applied in the sequence they are added, after the 198 * {@link #addChild(int, Transformation) transformations} are applied to the children 199 * views. 200 * 201 * <p>For example, to make children views visible when fields are not empty: 202 * 203 * <pre class="prettyprint"> 204 * RemoteViews template = new RemoteViews(pgkName, R.layout.my_full_template); 205 * 206 * Pattern notEmptyPattern = Pattern.compile(".+"); 207 * Validator hasAddress = new RegexValidator(addressAutofillId, notEmptyPattern); 208 * Validator hasCcNumber = new RegexValidator(ccNumberAutofillId, notEmptyPattern); 209 * 210 * RemoteViews addressUpdates = new RemoteViews(pgkName, R.layout.my_full_template) 211 * addressUpdates.setViewVisibility(R.id.address, View.VISIBLE); 212 * 213 * // Make address visible 214 * BatchUpdates addressBatchUpdates = new BatchUpdates.Builder() 215 * .updateTemplate(addressUpdates) 216 * .build(); 217 * 218 * RemoteViews ccUpdates = new RemoteViews(pgkName, R.layout.my_full_template) 219 * ccUpdates.setViewVisibility(R.id.cc_number, View.VISIBLE); 220 * 221 * // Mask credit card number (as .....LAST_4_DIGITS) and make it visible 222 * BatchUpdates ccBatchUpdates = new BatchUpdates.Builder() 223 * .updateTemplate(ccUpdates) 224 * .transformChild(R.id.templateCcNumber, new CharSequenceTransformation 225 * .Builder(ccNumberId, Pattern.compile("^.*(\\d\\d\\d\\d)$"), "...$1") 226 * .build()) 227 * .build(); 228 * 229 * CustomDescription customDescription = new CustomDescription.Builder(template) 230 * .batchUpdate(hasAddress, addressBatchUpdates) 231 * .batchUpdate(hasCcNumber, ccBatchUpdates) 232 * .build(); 233 * </pre> 234 * 235 * <p>Another approach is to add a child first, then apply the transformations. Example: 236 * 237 * <pre class="prettyprint"> 238 * RemoteViews template = new RemoteViews(pgkName, R.layout.my_base_template); 239 * 240 * RemoteViews addressPresentation = new RemoteViews(pgkName, R.layout.address) 241 * RemoteViews addressUpdates = new RemoteViews(pgkName, R.layout.my_template) 242 * addressUpdates.addView(R.id.parentId, addressPresentation); 243 * BatchUpdates addressBatchUpdates = new BatchUpdates.Builder() 244 * .updateTemplate(addressUpdates) 245 * .build(); 246 * 247 * RemoteViews ccPresentation = new RemoteViews(pgkName, R.layout.cc) 248 * RemoteViews ccUpdates = new RemoteViews(pgkName, R.layout.my_template) 249 * ccUpdates.addView(R.id.parentId, ccPresentation); 250 * BatchUpdates ccBatchUpdates = new BatchUpdates.Builder() 251 * .updateTemplate(ccUpdates) 252 * .transformChild(R.id.templateCcNumber, new CharSequenceTransformation 253 * .Builder(ccNumberId, Pattern.compile("^.*(\\d\\d\\d\\d)$"), "...$1") 254 * .build()) 255 * .build(); 256 * 257 * CustomDescription customDescription = new CustomDescription.Builder(template) 258 * .batchUpdate(hasAddress, addressBatchUpdates) 259 * .batchUpdate(hasCcNumber, ccBatchUpdates) 260 * .build(); 261 * </pre> 262 * 263 * @param condition condition used to trigger the updates. 264 * @param updates actions to be applied to the 265 * {@link #Builder(RemoteViews) template presentation} when the condition 266 * is satisfied. 267 * 268 * @return this builder 269 * 270 * @throws IllegalArgumentException if {@code condition} is not a class provided 271 * by the Android System. 272 * @throws IllegalStateException if {@link #build()} was already called. 273 */ 274 @NonNull batchUpdate(@onNull Validator condition, @NonNull BatchUpdates updates)275 public Builder batchUpdate(@NonNull Validator condition, @NonNull BatchUpdates updates) { 276 throwIfDestroyed(); 277 Preconditions.checkArgument((condition instanceof InternalValidator), 278 "not provided by Android System: %s", condition); 279 Preconditions.checkNotNull(updates); 280 if (mUpdates == null) { 281 mUpdates = new ArrayList<>(); 282 } 283 mUpdates.add(new Pair<>((InternalValidator) condition, updates)); 284 return this; 285 } 286 287 /** 288 * Sets an action to be applied to the {@link RemoteViews presentation template} when the 289 * child view with the given {@code id} is clicked. 290 * 291 * <p>Typically used when the presentation uses a masked field (like {@code ****}) for 292 * sensitive fields like passwords or credit cards numbers, but offers a an icon that the 293 * user can tap to show the value for that field. 294 * 295 * <p>Example: 296 * 297 * <pre class="prettyprint"> 298 * customDescriptionBuilder 299 * .addChild(R.id.password_plain, new CharSequenceTransformation 300 * .Builder(passwordId, Pattern.compile("^(.*)$"), "$1").build()) 301 * .addOnClickAction(R.id.showIcon, new VisibilitySetterAction 302 * .Builder(R.id.hideIcon, View.VISIBLE) 303 * .setVisibility(R.id.showIcon, View.GONE) 304 * .setVisibility(R.id.password_plain, View.VISIBLE) 305 * .setVisibility(R.id.password_masked, View.GONE) 306 * .build()) 307 * .addOnClickAction(R.id.hideIcon, new VisibilitySetterAction 308 * .Builder(R.id.showIcon, View.VISIBLE) 309 * .setVisibility(R.id.hideIcon, View.GONE) 310 * .setVisibility(R.id.password_masked, View.VISIBLE) 311 * .setVisibility(R.id.password_plain, View.GONE) 312 * .build()); 313 * </pre> 314 * 315 * <p><b>Note:</b> Currently only one action can be applied to a child; if this method 316 * is called multiple times passing the same {@code id}, only the last call will be used. 317 * 318 * @param id resource id of the child view. 319 * @param action action to be performed. Must be an an implementation provided by the 320 * Android System. 321 * 322 * @return this builder 323 * 324 * @throws IllegalArgumentException if {@code action} is not a class provided 325 * by the Android System. 326 * @throws IllegalStateException if {@link #build()} was already called. 327 */ 328 @NonNull addOnClickAction(int id, @NonNull OnClickAction action)329 public Builder addOnClickAction(int id, @NonNull OnClickAction action) { 330 throwIfDestroyed(); 331 Preconditions.checkArgument((action instanceof InternalOnClickAction), 332 "not provided by Android System: %s", action); 333 if (mActions == null) { 334 mActions = new SparseArray<InternalOnClickAction>(); 335 } 336 mActions.put(id, (InternalOnClickAction) action); 337 338 return this; 339 } 340 341 /** 342 * Creates a new {@link CustomDescription} instance. 343 */ 344 @NonNull build()345 public CustomDescription build() { 346 throwIfDestroyed(); 347 mDestroyed = true; 348 return new CustomDescription(this); 349 } 350 throwIfDestroyed()351 private void throwIfDestroyed() { 352 if (mDestroyed) { 353 throw new IllegalStateException("Already called #build()"); 354 } 355 } 356 } 357 358 ///////////////////////////////////// 359 // Object "contract" methods. // 360 ///////////////////////////////////// 361 @Override toString()362 public String toString() { 363 if (!sDebug) return super.toString(); 364 365 return new StringBuilder("CustomDescription: [presentation=") 366 .append(mPresentation) 367 .append(", transformations=") 368 .append(mTransformations == null ? "N/A" : mTransformations.size()) 369 .append(", updates=") 370 .append(mUpdates == null ? "N/A" : mUpdates.size()) 371 .append(", actions=") 372 .append(mActions == null ? "N/A" : mActions.size()) 373 .append("]").toString(); 374 } 375 376 ///////////////////////////////////// 377 // Parcelable "contract" methods. // 378 ///////////////////////////////////// 379 @Override describeContents()380 public int describeContents() { 381 return 0; 382 } 383 384 @Override writeToParcel(Parcel dest, int flags)385 public void writeToParcel(Parcel dest, int flags) { 386 dest.writeParcelable(mPresentation, flags); 387 if (mPresentation == null) return; 388 389 if (mTransformations == null) { 390 dest.writeIntArray(null); 391 } else { 392 final int size = mTransformations.size(); 393 final int[] ids = new int[size]; 394 final InternalTransformation[] values = new InternalTransformation[size]; 395 for (int i = 0; i < size; i++) { 396 final Pair<Integer, InternalTransformation> pair = mTransformations.get(i); 397 ids[i] = pair.first; 398 values[i] = pair.second; 399 } 400 dest.writeIntArray(ids); 401 dest.writeParcelableArray(values, flags); 402 } 403 if (mUpdates == null) { 404 dest.writeParcelableArray(null, flags); 405 } else { 406 final int size = mUpdates.size(); 407 final InternalValidator[] conditions = new InternalValidator[size]; 408 final BatchUpdates[] updates = new BatchUpdates[size]; 409 410 for (int i = 0; i < size; i++) { 411 final Pair<InternalValidator, BatchUpdates> pair = mUpdates.get(i); 412 conditions[i] = pair.first; 413 updates[i] = pair.second; 414 } 415 dest.writeParcelableArray(conditions, flags); 416 dest.writeParcelableArray(updates, flags); 417 } 418 if (mActions == null) { 419 dest.writeIntArray(null); 420 } else { 421 final int size = mActions.size(); 422 final int[] ids = new int[size]; 423 final InternalOnClickAction[] values = new InternalOnClickAction[size]; 424 for (int i = 0; i < size; i++) { 425 ids[i] = mActions.keyAt(i); 426 values[i] = mActions.valueAt(i); 427 } 428 dest.writeIntArray(ids); 429 dest.writeParcelableArray(values, flags); 430 } 431 } 432 public static final @android.annotation.NonNull Parcelable.Creator<CustomDescription> CREATOR = 433 new Parcelable.Creator<CustomDescription>() { 434 @Override 435 public CustomDescription createFromParcel(Parcel parcel) { 436 // Always go through the builder to ensure the data ingested by 437 // the system obeys the contract of the builder to avoid attacks 438 // using specially crafted parcels. 439 final RemoteViews parentPresentation = parcel.readParcelable(null); 440 if (parentPresentation == null) return null; 441 442 final Builder builder = new Builder(parentPresentation); 443 final int[] transformationIds = parcel.createIntArray(); 444 if (transformationIds != null) { 445 final InternalTransformation[] values = 446 parcel.readParcelableArray(null, InternalTransformation.class); 447 final int size = transformationIds.length; 448 for (int i = 0; i < size; i++) { 449 builder.addChild(transformationIds[i], values[i]); 450 } 451 } 452 final InternalValidator[] conditions = 453 parcel.readParcelableArray(null, InternalValidator.class); 454 if (conditions != null) { 455 final BatchUpdates[] updates = parcel.readParcelableArray(null, BatchUpdates.class); 456 final int size = conditions.length; 457 for (int i = 0; i < size; i++) { 458 builder.batchUpdate(conditions[i], updates[i]); 459 } 460 } 461 final int[] actionIds = parcel.createIntArray(); 462 if (actionIds != null) { 463 final InternalOnClickAction[] values = 464 parcel.readParcelableArray(null, InternalOnClickAction.class); 465 final int size = actionIds.length; 466 for (int i = 0; i < size; i++) { 467 builder.addOnClickAction(actionIds[i], values[i]); 468 } 469 } 470 return builder.build(); 471 } 472 473 @Override 474 public CustomDescription[] newArray(int size) { 475 return new CustomDescription[size]; 476 } 477 }; 478 } 479