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