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