1 /* 2 * Copyright (C) 2016 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.SuppressLint; 24 import android.annotation.SystemApi; 25 import android.annotation.TestApi; 26 import android.content.ClipData; 27 import android.content.IntentSender; 28 import android.os.Parcel; 29 import android.os.Parcelable; 30 import android.view.autofill.AutofillId; 31 import android.view.autofill.AutofillValue; 32 import android.widget.RemoteViews; 33 34 import com.android.internal.util.Preconditions; 35 36 import java.util.ArrayList; 37 import java.util.regex.Pattern; 38 39 /** 40 * <p>A <code>Dataset</code> object represents a group of fields (key / value pairs) used 41 * to autofill parts of a screen. 42 * 43 * <p>For more information about the role of datasets in the autofill workflow, read 44 * <a href="/guide/topics/text/autofill-services">Build autofill services</a> and the 45 * <code><a href="/reference/android/service/autofill/AutofillService">AutofillService</a></code> 46 * documentation. 47 * 48 * <a name="BasicUsage"></a> 49 * <h3>Basic usage</h3> 50 * 51 * <p>In its simplest form, a dataset contains one or more fields (comprised of 52 * an {@link AutofillId id}, a {@link AutofillValue value}, and an optional filter 53 * {@link Pattern regex}); and one or more {@link RemoteViews presentations} for these fields 54 * (each field could have its own {@link RemoteViews presentation}, or use the default 55 * {@link RemoteViews presentation} associated with the whole dataset). 56 * 57 * <p>When an autofill service returns datasets in a {@link FillResponse} 58 * and the screen input is focused in a view that is present in at least one of these datasets, 59 * the Android System displays a UI containing the {@link RemoteViews presentation} of 60 * all datasets pairs that have that view's {@link AutofillId}. Then, when the user selects a 61 * dataset from the UI, all views in that dataset are autofilled. 62 * 63 * <p>If both the current Input Method and autofill service supports inline suggestions, the Dataset 64 * can be shown by the keyboard as a suggestion. To use this feature, the Dataset should contain 65 * an {@link InlinePresentation} representing how the inline suggestion UI will be rendered. 66 * 67 * <a name="Authentication"></a> 68 * <h3>Dataset authentication</h3> 69 * 70 * <p>In a more sophisticated form, the dataset values can be protected until the user authenticates 71 * the dataset—in that case, when a dataset is selected by the user, the Android System 72 * launches an intent set by the service to "unlock" the dataset. 73 * 74 * <p>For example, when a data set contains credit card information (such as number, 75 * expiration date, and verification code), you could provide a dataset presentation saying 76 * "Tap to authenticate". Then when the user taps that option, you would launch an activity asking 77 * the user to enter the credit card code, and if the user enters a valid code, you could then 78 * "unlock" the dataset. 79 * 80 * <p>You can also use authenticated datasets to offer an interactive UI for the user. For example, 81 * if the activity being autofilled is an account creation screen, you could use an authenticated 82 * dataset to automatically generate a random password for the user. 83 * 84 * <p>See {@link Dataset.Builder#setAuthentication(IntentSender)} for more details about the dataset 85 * authentication mechanism. 86 * 87 * <a name="Filtering"></a> 88 * <h3>Filtering</h3> 89 * <p>The autofill UI automatically changes which values are shown based on value of the view 90 * anchoring it, following the rules below: 91 * <ol> 92 * <li>If the view's {@link android.view.View#getAutofillValue() autofill value} is not 93 * {@link AutofillValue#isText() text} or is empty, all datasets are shown. 94 * <li>Datasets that have a filter regex (set through 95 * {@link Dataset.Builder#setValue(AutofillId, AutofillValue, Pattern)} or 96 * {@link Dataset.Builder#setValue(AutofillId, AutofillValue, Pattern, RemoteViews)}) and whose 97 * regex matches the view's text value converted to lower case are shown. 98 * <li>Datasets that do not require authentication, have a field value that is 99 * {@link AutofillValue#isText() text} and whose {@link AutofillValue#getTextValue() value} starts 100 * with the lower case value of the view's text are shown. 101 * <li>All other datasets are hidden. 102 * </ol> 103 */ 104 public final class Dataset implements Parcelable { 105 106 private final ArrayList<AutofillId> mFieldIds; 107 private final ArrayList<AutofillValue> mFieldValues; 108 private final ArrayList<RemoteViews> mFieldPresentations; 109 private final ArrayList<InlinePresentation> mFieldInlinePresentations; 110 private final ArrayList<InlinePresentation> mFieldInlineTooltipPresentations; 111 private final ArrayList<DatasetFieldFilter> mFieldFilters; 112 @Nullable private final ClipData mFieldContent; 113 private final RemoteViews mPresentation; 114 @Nullable private final InlinePresentation mInlinePresentation; 115 @Nullable private final InlinePresentation mInlineTooltipPresentation; 116 private final IntentSender mAuthentication; 117 @Nullable String mId; 118 Dataset(Builder builder)119 private Dataset(Builder builder) { 120 mFieldIds = builder.mFieldIds; 121 mFieldValues = builder.mFieldValues; 122 mFieldPresentations = builder.mFieldPresentations; 123 mFieldInlinePresentations = builder.mFieldInlinePresentations; 124 mFieldInlineTooltipPresentations = builder.mFieldInlineTooltipPresentations; 125 mFieldFilters = builder.mFieldFilters; 126 mFieldContent = builder.mFieldContent; 127 mPresentation = builder.mPresentation; 128 mInlinePresentation = builder.mInlinePresentation; 129 mInlineTooltipPresentation = builder.mInlineTooltipPresentation; 130 mAuthentication = builder.mAuthentication; 131 mId = builder.mId; 132 } 133 134 /** @hide */ 135 @TestApi 136 @SuppressLint({"ConcreteCollection", "NullableCollection"}) getFieldIds()137 public @Nullable ArrayList<AutofillId> getFieldIds() { 138 return mFieldIds; 139 } 140 141 /** @hide */ 142 @TestApi 143 @SuppressLint({"ConcreteCollection", "NullableCollection"}) getFieldValues()144 public @Nullable ArrayList<AutofillValue> getFieldValues() { 145 return mFieldValues; 146 } 147 148 /** @hide */ getFieldPresentation(int index)149 public RemoteViews getFieldPresentation(int index) { 150 final RemoteViews customPresentation = mFieldPresentations.get(index); 151 return customPresentation != null ? customPresentation : mPresentation; 152 } 153 154 /** @hide */ getFieldInlinePresentation(int index)155 public @Nullable InlinePresentation getFieldInlinePresentation(int index) { 156 final InlinePresentation inlinePresentation = mFieldInlinePresentations.get(index); 157 return inlinePresentation != null ? inlinePresentation : mInlinePresentation; 158 } 159 160 /** @hide */ getFieldInlineTooltipPresentation(int index)161 public @Nullable InlinePresentation getFieldInlineTooltipPresentation(int index) { 162 final InlinePresentation inlineTooltipPresentation = 163 mFieldInlineTooltipPresentations.get(index); 164 return inlineTooltipPresentation != null 165 ? inlineTooltipPresentation : mInlineTooltipPresentation; 166 } 167 168 /** @hide */ getFilter(int index)169 public @Nullable DatasetFieldFilter getFilter(int index) { 170 return mFieldFilters.get(index); 171 } 172 173 /** 174 * Returns the content to be filled for a non-text suggestion. This is only applicable to 175 * augmented autofill. The target field for the content is available via {@link #getFieldIds()} 176 * (guaranteed to have a single field id set when the return value here is non-null). See 177 * {@link Builder#setContent(AutofillId, ClipData)} for more info. 178 * 179 * @hide 180 */ 181 @TestApi getFieldContent()182 public @Nullable ClipData getFieldContent() { 183 return mFieldContent; 184 } 185 186 /** @hide */ 187 @TestApi getAuthentication()188 public @Nullable IntentSender getAuthentication() { 189 return mAuthentication; 190 } 191 192 /** @hide */ 193 @TestApi isEmpty()194 public boolean isEmpty() { 195 return mFieldIds == null || mFieldIds.isEmpty(); 196 } 197 198 @Override toString()199 public String toString() { 200 if (!sDebug) return super.toString(); 201 202 final StringBuilder builder = new StringBuilder("Dataset["); 203 if (mId == null) { 204 builder.append("noId"); 205 } else { 206 // Cannot disclose id because it could contain PII. 207 builder.append("id=").append(mId.length()).append("_chars"); 208 } 209 if (mFieldIds != null) { 210 builder.append(", fieldIds=").append(mFieldIds); 211 } 212 if (mFieldValues != null) { 213 builder.append(", fieldValues=").append(mFieldValues); 214 } 215 if (mFieldContent != null) { 216 builder.append(", fieldContent=").append(mFieldContent); 217 } 218 if (mFieldPresentations != null) { 219 builder.append(", fieldPresentations=").append(mFieldPresentations.size()); 220 } 221 if (mFieldInlinePresentations != null) { 222 builder.append(", fieldInlinePresentations=").append(mFieldInlinePresentations.size()); 223 } 224 if (mFieldInlineTooltipPresentations != null) { 225 builder.append(", fieldInlineTooltipInlinePresentations=").append( 226 mFieldInlineTooltipPresentations.size()); 227 } 228 if (mFieldFilters != null) { 229 builder.append(", fieldFilters=").append(mFieldFilters.size()); 230 } 231 if (mPresentation != null) { 232 builder.append(", hasPresentation"); 233 } 234 if (mInlinePresentation != null) { 235 builder.append(", hasInlinePresentation"); 236 } 237 if (mInlineTooltipPresentation != null) { 238 builder.append(", hasInlineTooltipPresentation"); 239 } 240 if (mAuthentication != null) { 241 builder.append(", hasAuthentication"); 242 } 243 return builder.append(']').toString(); 244 } 245 246 /** 247 * Gets the id of this dataset. 248 * 249 * @return The id of this dataset or {@code null} if not set 250 * 251 * @hide 252 */ 253 @TestApi getId()254 public @Nullable String getId() { 255 return mId; 256 } 257 258 /** 259 * A builder for {@link Dataset} objects. You must provide at least 260 * one value for a field or set an authentication intent. 261 */ 262 public static final class Builder { 263 private ArrayList<AutofillId> mFieldIds; 264 private ArrayList<AutofillValue> mFieldValues; 265 private ArrayList<RemoteViews> mFieldPresentations; 266 private ArrayList<InlinePresentation> mFieldInlinePresentations; 267 private ArrayList<InlinePresentation> mFieldInlineTooltipPresentations; 268 private ArrayList<DatasetFieldFilter> mFieldFilters; 269 @Nullable private ClipData mFieldContent; 270 private RemoteViews mPresentation; 271 @Nullable private InlinePresentation mInlinePresentation; 272 @Nullable private InlinePresentation mInlineTooltipPresentation; 273 private IntentSender mAuthentication; 274 private boolean mDestroyed; 275 @Nullable private String mId; 276 277 /** 278 * Creates a new builder. 279 * 280 * @param presentation The presentation used to visualize this dataset. 281 */ Builder(@onNull RemoteViews presentation)282 public Builder(@NonNull RemoteViews presentation) { 283 Preconditions.checkNotNull(presentation, "presentation must be non-null"); 284 mPresentation = presentation; 285 } 286 287 /** 288 * Creates a new builder. 289 * 290 * <p>Only called by augmented autofill. 291 * 292 * @param inlinePresentation The {@link InlinePresentation} used to visualize this dataset 293 * as inline suggestions. If the dataset supports inline suggestions, 294 * this should not be null. 295 * @hide 296 */ 297 @SystemApi Builder(@onNull InlinePresentation inlinePresentation)298 public Builder(@NonNull InlinePresentation inlinePresentation) { 299 Preconditions.checkNotNull(inlinePresentation, "inlinePresentation must be non-null"); 300 mInlinePresentation = inlinePresentation; 301 } 302 303 /** 304 * Creates a new builder for a dataset where each field will be visualized independently. 305 * 306 * <p>When using this constructor, fields must be set through 307 * {@link #setValue(AutofillId, AutofillValue, RemoteViews)} or 308 * {@link #setValue(AutofillId, AutofillValue, Pattern, RemoteViews)}. 309 */ Builder()310 public Builder() { 311 } 312 313 /** 314 * Sets the {@link InlinePresentation} used to visualize this dataset as inline suggestions. 315 * If the dataset supports inline suggestions this should not be null. 316 * 317 * @throws IllegalStateException if {@link #build()} was already called. 318 * 319 * @return this builder. 320 */ setInlinePresentation( @onNull InlinePresentation inlinePresentation)321 public @NonNull Builder setInlinePresentation( 322 @NonNull InlinePresentation inlinePresentation) { 323 throwIfDestroyed(); 324 Preconditions.checkNotNull(inlinePresentation, "inlinePresentation must be non-null"); 325 mInlinePresentation = inlinePresentation; 326 return this; 327 } 328 329 /** 330 * Visualizes this dataset as inline suggestions. 331 * 332 * @param inlinePresentation the {@link InlinePresentation} used to visualize this 333 * dataset as inline suggestions. If the dataset supports inline suggestions this 334 * should not be null. 335 * @param inlineTooltipPresentation the {@link InlinePresentation} used to show 336 * the tooltip for the {@code inlinePresentation}. 337 * 338 * @throws IllegalStateException if {@link #build()} was already called. 339 * 340 * @return this builder. 341 */ setInlinePresentation( @onNull InlinePresentation inlinePresentation, @NonNull InlinePresentation inlineTooltipPresentation)342 public @NonNull Builder setInlinePresentation( 343 @NonNull InlinePresentation inlinePresentation, 344 @NonNull InlinePresentation inlineTooltipPresentation) { 345 throwIfDestroyed(); 346 Preconditions.checkNotNull(inlinePresentation, "inlinePresentation must be non-null"); 347 Preconditions.checkNotNull(inlineTooltipPresentation, 348 "inlineTooltipPresentation must be non-null"); 349 mInlinePresentation = inlinePresentation; 350 mInlineTooltipPresentation = inlineTooltipPresentation; 351 return this; 352 } 353 354 /** 355 * Triggers a custom UI before before autofilling the screen with the contents of this 356 * dataset. 357 * 358 * <p><b>Note:</b> Although the name of this method suggests that it should be used just for 359 * authentication flow, it can be used for other advanced flows; see {@link AutofillService} 360 * for examples. 361 * 362 * <p>This method is called when you need to provide an authentication 363 * UI for the data set. For example, when a data set contains credit card information 364 * (such as number, expiration date, and verification code), you can display UI 365 * asking for the verification code before filing in the data. Even if the 366 * data set is completely populated the system will launch the specified authentication 367 * intent and will need your approval to fill it in. Since the data set is "locked" 368 * until the user authenticates it, typically this data set name is masked 369 * (for example, "VISA....1234"). Typically you would want to store the data set 370 * labels non-encrypted and the actual sensitive data encrypted and not in memory. 371 * This allows showing the labels in the UI while involving the user if one of 372 * the items with these labels is chosen. Note that if you use sensitive data as 373 * a label, for example an email address, then it should also be encrypted.</p> 374 * 375 * <p>When a user triggers autofill, the system launches the provided intent 376 * whose extras will have the {@link 377 * android.view.autofill.AutofillManager#EXTRA_ASSIST_STRUCTURE screen content}, 378 * and your {@link android.view.autofill.AutofillManager#EXTRA_CLIENT_STATE client 379 * state}. Once you complete your authentication flow you should set the activity 380 * result to {@link android.app.Activity#RESULT_OK} and provide the fully populated 381 * {@link Dataset dataset} or a fully-populated {@link FillResponse response} by 382 * setting it to the {@link 383 * android.view.autofill.AutofillManager#EXTRA_AUTHENTICATION_RESULT} extra. If you 384 * provide a dataset in the result, it will replace the authenticated dataset and 385 * will be immediately filled in. An exception to this behavior is if the original 386 * dataset represents a pinned inline suggestion (i.e. any of the field in the dataset 387 * has a pinned inline presentation, see {@link InlinePresentation#isPinned()}), then 388 * the original dataset will not be replaced, 389 * so that it can be triggered as a pending intent again. 390 * If you provide a response, it will replace the 391 * current response and the UI will be refreshed. For example, if you provided 392 * credit card information without the CVV for the data set in the {@link FillResponse 393 * response} then the returned data set should contain the CVV entry. 394 * 395 * <p><b>Note:</b> Do not make the provided pending intent 396 * immutable by using {@link android.app.PendingIntent#FLAG_IMMUTABLE} as the 397 * platform needs to fill in the authentication arguments. 398 * 399 * @param authentication Intent to an activity with your authentication flow. 400 * 401 * @throws IllegalStateException if {@link #build()} was already called. 402 * 403 * @return this builder. 404 * 405 * @see android.app.PendingIntent 406 */ setAuthentication(@ullable IntentSender authentication)407 public @NonNull Builder setAuthentication(@Nullable IntentSender authentication) { 408 throwIfDestroyed(); 409 mAuthentication = authentication; 410 return this; 411 } 412 413 /** 414 * Sets the id for the dataset so its usage can be tracked. 415 * 416 * <p>Dataset usage can be tracked for 2 purposes: 417 * 418 * <ul> 419 * <li>For statistical purposes, the service can call 420 * {@link AutofillService#getFillEventHistory()} when handling {@link 421 * AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, FillCallback)} 422 * calls. 423 * <li>For normal autofill workflow, the service can call 424 * {@link SaveRequest#getDatasetIds()} when handling 425 * {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)} calls. 426 * </ul> 427 * 428 * @param id id for this dataset or {@code null} to unset. 429 * 430 * @throws IllegalStateException if {@link #build()} was already called. 431 * 432 * @return this builder. 433 */ setId(@ullable String id)434 public @NonNull Builder setId(@Nullable String id) { 435 throwIfDestroyed(); 436 mId = id; 437 return this; 438 } 439 440 /** 441 * Sets the content for a field. 442 * 443 * <p>Only called by augmented autofill. 444 * 445 * <p>For a given field, either a {@link AutofillValue value} or content can be filled, but 446 * not both. Furthermore, when filling content, only a single field can be filled. 447 * 448 * <p>The provided {@link ClipData} can contain content URIs (e.g. a URI for an image). 449 * The augmented autofill provider setting the content here must itself have at least 450 * read permissions to any passed content URIs. If the user accepts the suggestion backed 451 * by the content URI(s), the platform will automatically grant read URI permissions to 452 * the app being autofilled, just before passing the content URI(s) to it. The granted 453 * permissions will be transient and tied to the lifecycle of the activity being filled 454 * (when the activity finishes, permissions will automatically be revoked by the platform). 455 * 456 * @param id id returned by 457 * {@link android.app.assist.AssistStructure.ViewNode#getAutofillId()}. 458 * @param content content to be autofilled. Pass {@code null} if you do not have the content 459 * but the target view is a logical part of the dataset. For example, if the dataset needs 460 * authentication. 461 * 462 * @throws IllegalStateException if {@link #build()} was already called. 463 * @throws IllegalArgumentException if the provided content 464 * {@link ClipData.Item#getIntent() contains an intent} 465 * 466 * @return this builder. 467 * 468 * @hide 469 */ 470 @TestApi 471 @SystemApi 472 @SuppressLint("MissingGetterMatchingBuilder") setContent(@onNull AutofillId id, @Nullable ClipData content)473 public @NonNull Builder setContent(@NonNull AutofillId id, @Nullable ClipData content) { 474 throwIfDestroyed(); 475 if (content != null) { 476 for (int i = 0; i < content.getItemCount(); i++) { 477 Preconditions.checkArgument(content.getItemAt(i).getIntent() == null, 478 "Content items cannot contain an Intent: content=" + content); 479 } 480 } 481 setLifeTheUniverseAndEverything(id, null, null, null, null); 482 mFieldContent = content; 483 return this; 484 } 485 486 /** 487 * Sets the value of a field. 488 * 489 * <b>Note:</b> Prior to Android {@link android.os.Build.VERSION_CODES#P}, this method would 490 * throw an {@link IllegalStateException} if this builder was constructed without a 491 * {@link RemoteViews presentation}. Android {@link android.os.Build.VERSION_CODES#P} and 492 * higher removed this restriction because datasets used as an 493 * {@link android.view.autofill.AutofillManager#EXTRA_AUTHENTICATION_RESULT 494 * authentication result} do not need a presentation. But if you don't set the presentation 495 * in the constructor in a dataset that is meant to be shown to the user, the autofill UI 496 * for this field will not be displayed. 497 * 498 * <p><b>Note:</b> On Android {@link android.os.Build.VERSION_CODES#P} and 499 * higher, datasets that require authentication can be also be filtered by passing a 500 * {@link AutofillValue#forText(CharSequence) text value} as the {@code value} parameter. 501 * 502 * @param id id returned by {@link 503 * android.app.assist.AssistStructure.ViewNode#getAutofillId()}. 504 * @param value value to be autofilled. Pass {@code null} if you do not have the value 505 * but the target view is a logical part of the dataset. For example, if 506 * the dataset needs authentication and you have no access to the value. 507 * 508 * @throws IllegalStateException if {@link #build()} was already called. 509 * 510 * @return this builder. 511 */ setValue(@onNull AutofillId id, @Nullable AutofillValue value)512 public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value) { 513 throwIfDestroyed(); 514 setLifeTheUniverseAndEverything(id, value, null, null, null); 515 return this; 516 } 517 518 /** 519 * Sets the value of a field, using a custom {@link RemoteViews presentation} to 520 * visualize it. 521 * 522 * <p><b>Note:</b> On Android {@link android.os.Build.VERSION_CODES#P} and 523 * higher, datasets that require authentication can be also be filtered by passing a 524 * {@link AutofillValue#forText(CharSequence) text value} as the {@code value} parameter. 525 * 526 * <p>Theme does not work with RemoteViews layout. Avoid hardcoded text color 527 * or background color: Autofill on different platforms may have different themes. 528 * 529 * @param id id returned by {@link 530 * android.app.assist.AssistStructure.ViewNode#getAutofillId()}. 531 * @param value the value to be autofilled. Pass {@code null} if you do not have the value 532 * but the target view is a logical part of the dataset. For example, if 533 * the dataset needs authentication and you have no access to the value. 534 * @param presentation the presentation used to visualize this field. 535 * 536 * @throws IllegalStateException if {@link #build()} was already called. 537 * 538 * @return this builder. 539 */ setValue(@onNull AutofillId id, @Nullable AutofillValue value, @NonNull RemoteViews presentation)540 public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value, 541 @NonNull RemoteViews presentation) { 542 throwIfDestroyed(); 543 Preconditions.checkNotNull(presentation, "presentation cannot be null"); 544 setLifeTheUniverseAndEverything(id, value, presentation, null, null); 545 return this; 546 } 547 548 /** 549 * Sets the value of a field using an <a href="#Filtering">explicit filter</a>. 550 * 551 * <p>This method is typically used when the dataset requires authentication and the service 552 * does not know its value but wants to hide the dataset after the user enters a minimum 553 * number of characters. For example, if the dataset represents a credit card number and the 554 * service does not want to show the "Tap to authenticate" message until the user tapped 555 * 4 digits, in which case the filter would be {@code Pattern.compile("\\d.{4,}")}. 556 * 557 * <p><b>Note:</b> If the dataset requires authentication but the service knows its text 558 * value it's easier to filter by calling {@link #setValue(AutofillId, AutofillValue)} and 559 * use the value to filter. 560 * 561 * @param id id returned by {@link 562 * android.app.assist.AssistStructure.ViewNode#getAutofillId()}. 563 * @param value the value to be autofilled. Pass {@code null} if you do not have the value 564 * but the target view is a logical part of the dataset. For example, if 565 * the dataset needs authentication and you have no access to the value. 566 * @param filter regex used to determine if the dataset should be shown in the autofill UI; 567 * when {@code null}, it disables filtering on that dataset (this is the recommended 568 * approach when {@code value} is not {@code null} and field contains sensitive data 569 * such as passwords). 570 * 571 * @return this builder. 572 * @throws IllegalStateException if the builder was constructed without a 573 * {@link RemoteViews presentation} or {@link #build()} was already called. 574 */ setValue(@onNull AutofillId id, @Nullable AutofillValue value, @Nullable Pattern filter)575 public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value, 576 @Nullable Pattern filter) { 577 throwIfDestroyed(); 578 Preconditions.checkState(mPresentation != null, 579 "Dataset presentation not set on constructor"); 580 setLifeTheUniverseAndEverything(id, value, null, null, new DatasetFieldFilter(filter)); 581 return this; 582 } 583 584 /** 585 * Sets the value of a field, using a custom {@link RemoteViews presentation} to 586 * visualize it and a <a href="#Filtering">explicit filter</a>. 587 * 588 * <p>This method is typically used when the dataset requires authentication and the service 589 * does not know its value but wants to hide the dataset after the user enters a minimum 590 * number of characters. For example, if the dataset represents a credit card number and the 591 * service does not want to show the "Tap to authenticate" message until the user tapped 592 * 4 digits, in which case the filter would be {@code Pattern.compile("\\d.{4,}")}. 593 * 594 * <p><b>Note:</b> If the dataset requires authentication but the service knows its text 595 * value it's easier to filter by calling 596 * {@link #setValue(AutofillId, AutofillValue, RemoteViews)} and using the value to filter. 597 * 598 * @param id id returned by {@link 599 * android.app.assist.AssistStructure.ViewNode#getAutofillId()}. 600 * @param value the value to be autofilled. Pass {@code null} if you do not have the value 601 * but the target view is a logical part of the dataset. For example, if 602 * the dataset needs authentication and you have no access to the value. 603 * @param filter regex used to determine if the dataset should be shown in the autofill UI; 604 * when {@code null}, it disables filtering on that dataset (this is the recommended 605 * approach when {@code value} is not {@code null} and field contains sensitive data 606 * such as passwords). 607 * @param presentation the presentation used to visualize this field. 608 * 609 * @throws IllegalStateException if {@link #build()} was already called. 610 * 611 * @return this builder. 612 */ setValue(@onNull AutofillId id, @Nullable AutofillValue value, @Nullable Pattern filter, @NonNull RemoteViews presentation)613 public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value, 614 @Nullable Pattern filter, @NonNull RemoteViews presentation) { 615 throwIfDestroyed(); 616 Preconditions.checkNotNull(presentation, "presentation cannot be null"); 617 setLifeTheUniverseAndEverything(id, value, presentation, null, 618 new DatasetFieldFilter(filter)); 619 return this; 620 } 621 622 /** 623 * Sets the value of a field, using a custom {@link RemoteViews presentation} to 624 * visualize it and an {@link InlinePresentation} to visualize it as an inline suggestion. 625 * 626 * <p><b>Note:</b> If the dataset requires authentication but the service knows its text 627 * value it's easier to filter by calling 628 * {@link #setValue(AutofillId, AutofillValue, RemoteViews)} and using the value to filter. 629 * 630 * @param id id returned by {@link 631 * android.app.assist.AssistStructure.ViewNode#getAutofillId()}. 632 * @param value the value to be autofilled. Pass {@code null} if you do not have the value 633 * but the target view is a logical part of the dataset. For example, if 634 * the dataset needs authentication and you have no access to the value. 635 * @param presentation the presentation used to visualize this field. 636 * @param inlinePresentation The {@link InlinePresentation} used to visualize this dataset 637 * as inline suggestions. If the dataset supports inline suggestions, 638 * this should not be null. 639 * 640 * @throws IllegalStateException if {@link #build()} was already called. 641 * 642 * @return this builder. 643 */ setValue(@onNull AutofillId id, @Nullable AutofillValue value, @NonNull RemoteViews presentation, @NonNull InlinePresentation inlinePresentation)644 public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value, 645 @NonNull RemoteViews presentation, @NonNull InlinePresentation inlinePresentation) { 646 throwIfDestroyed(); 647 Preconditions.checkNotNull(presentation, "presentation cannot be null"); 648 Preconditions.checkNotNull(inlinePresentation, "inlinePresentation cannot be null"); 649 setLifeTheUniverseAndEverything(id, value, presentation, inlinePresentation, null); 650 return this; 651 } 652 653 /** 654 * Sets the value of a field, using a custom {@link RemoteViews presentation} to 655 * visualize it and an {@link InlinePresentation} to visualize it as an inline suggestion. 656 * 657 * @see #setValue(AutofillId, AutofillValue, RemoteViews, InlinePresentation) 658 * 659 * @param id id returned by {@link 660 * android.app.assist.AssistStructure.ViewNode#getAutofillId()}. 661 * @param value the value to be autofilled. Pass {@code null} if you do not have the value 662 * but the target view is a logical part of the dataset. For example, if 663 * the dataset needs authentication and you have no access to the value. 664 * @param presentation the presentation used to visualize this field. 665 * @param inlinePresentation The {@link InlinePresentation} used to visualize this dataset 666 * as inline suggestions. If the dataset supports inline suggestions, 667 * this should not be null. 668 * @param inlineTooltipPresentation The {@link InlinePresentation} used to show 669 * the tooltip for the {@code inlinePresentation}. 670 * 671 * @throws IllegalStateException if {@link #build()} was already called. 672 * 673 * @return this builder. 674 */ setValue(@onNull AutofillId id, @Nullable AutofillValue value, @NonNull RemoteViews presentation, @NonNull InlinePresentation inlinePresentation, @NonNull InlinePresentation inlineTooltipPresentation)675 public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value, 676 @NonNull RemoteViews presentation, @NonNull InlinePresentation inlinePresentation, 677 @NonNull InlinePresentation inlineTooltipPresentation) { 678 throwIfDestroyed(); 679 Preconditions.checkNotNull(presentation, "presentation cannot be null"); 680 Preconditions.checkNotNull(inlinePresentation, "inlinePresentation cannot be null"); 681 Preconditions.checkNotNull(inlineTooltipPresentation, 682 "inlineTooltipPresentation cannot be null"); 683 setLifeTheUniverseAndEverything(id, value, presentation, inlinePresentation, 684 inlineTooltipPresentation, null); 685 return this; 686 } 687 688 /** 689 * Sets the value of a field, using a custom {@link RemoteViews presentation} to 690 * visualize it and a <a href="#Filtering">explicit filter</a>, and an 691 * {@link InlinePresentation} to visualize it as an inline suggestion. 692 * 693 * <p>This method is typically used when the dataset requires authentication and the service 694 * does not know its value but wants to hide the dataset after the user enters a minimum 695 * number of characters. For example, if the dataset represents a credit card number and the 696 * service does not want to show the "Tap to authenticate" message until the user tapped 697 * 4 digits, in which case the filter would be {@code Pattern.compile("\\d.{4,}")}. 698 * 699 * <p><b>Note:</b> If the dataset requires authentication but the service knows its text 700 * value it's easier to filter by calling 701 * {@link #setValue(AutofillId, AutofillValue, RemoteViews)} and using the value to filter. 702 * 703 * @param id id returned by {@link 704 * android.app.assist.AssistStructure.ViewNode#getAutofillId()}. 705 * @param value the value to be autofilled. Pass {@code null} if you do not have the value 706 * but the target view is a logical part of the dataset. For example, if 707 * the dataset needs authentication and you have no access to the value. 708 * @param filter regex used to determine if the dataset should be shown in the autofill UI; 709 * when {@code null}, it disables filtering on that dataset (this is the recommended 710 * approach when {@code value} is not {@code null} and field contains sensitive data 711 * such as passwords). 712 * @param presentation the presentation used to visualize this field. 713 * @param inlinePresentation The {@link InlinePresentation} used to visualize this dataset 714 * as inline suggestions. If the dataset supports inline suggestions, this 715 * should not be null. 716 * 717 * @throws IllegalStateException if {@link #build()} was already called. 718 * 719 * @return this builder. 720 */ setValue(@onNull AutofillId id, @Nullable AutofillValue value, @Nullable Pattern filter, @NonNull RemoteViews presentation, @NonNull InlinePresentation inlinePresentation)721 public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value, 722 @Nullable Pattern filter, @NonNull RemoteViews presentation, 723 @NonNull InlinePresentation inlinePresentation) { 724 throwIfDestroyed(); 725 Preconditions.checkNotNull(presentation, "presentation cannot be null"); 726 Preconditions.checkNotNull(inlinePresentation, "inlinePresentation cannot be null"); 727 setLifeTheUniverseAndEverything(id, value, presentation, inlinePresentation, 728 new DatasetFieldFilter(filter)); 729 return this; 730 } 731 732 /** 733 * Sets the value of a field, using a custom {@link RemoteViews presentation} to 734 * visualize it and a <a href="#Filtering">explicit filter</a>, and an 735 * {@link InlinePresentation} to visualize it as an inline suggestion. 736 * 737 * @see #setValue(AutofillId, AutofillValue, Pattern, RemoteViews, InlinePresentation) 738 * 739 * @param id id returned by {@link 740 * android.app.assist.AssistStructure.ViewNode#getAutofillId()}. 741 * @param value the value to be autofilled. Pass {@code null} if you do not have the value 742 * but the target view is a logical part of the dataset. For example, if 743 * the dataset needs authentication and you have no access to the value. 744 * @param filter regex used to determine if the dataset should be shown in the autofill UI; 745 * when {@code null}, it disables filtering on that dataset (this is the recommended 746 * approach when {@code value} is not {@code null} and field contains sensitive data 747 * such as passwords). 748 * @param presentation the presentation used to visualize this field. 749 * @param inlinePresentation The {@link InlinePresentation} used to visualize this dataset 750 * as inline suggestions. If the dataset supports inline suggestions, this 751 * should not be null. 752 * @param inlineTooltipPresentation The {@link InlinePresentation} used to show 753 * the tooltip for the {@code inlinePresentation}. 754 * 755 * @throws IllegalStateException if {@link #build()} was already called. 756 * 757 * @return this builder. 758 */ setValue(@onNull AutofillId id, @Nullable AutofillValue value, @Nullable Pattern filter, @NonNull RemoteViews presentation, @NonNull InlinePresentation inlinePresentation, @NonNull InlinePresentation inlineTooltipPresentation)759 public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value, 760 @Nullable Pattern filter, @NonNull RemoteViews presentation, 761 @NonNull InlinePresentation inlinePresentation, 762 @NonNull InlinePresentation inlineTooltipPresentation) { 763 throwIfDestroyed(); 764 Preconditions.checkNotNull(presentation, "presentation cannot be null"); 765 Preconditions.checkNotNull(inlinePresentation, "inlinePresentation cannot be null"); 766 Preconditions.checkNotNull(inlineTooltipPresentation, 767 "inlineTooltipPresentation cannot be null"); 768 setLifeTheUniverseAndEverything(id, value, presentation, inlinePresentation, 769 inlineTooltipPresentation, new DatasetFieldFilter(filter)); 770 return this; 771 } 772 773 /** 774 * Sets the value of a field with an <a href="#Filtering">explicit filter</a>, and using an 775 * {@link InlinePresentation} to visualize it as an inline suggestion. 776 * 777 * <p>Only called by augmented autofill. 778 * 779 * @param id id returned by {@link 780 * android.app.assist.AssistStructure.ViewNode#getAutofillId()}. 781 * @param value the value to be autofilled. Pass {@code null} if you do not have the value 782 * but the target view is a logical part of the dataset. For example, if 783 * the dataset needs authentication and you have no access to the value. 784 * @param filter regex used to determine if the dataset should be shown in the autofill UI; 785 * when {@code null}, it disables filtering on that dataset (this is the recommended 786 * approach when {@code value} is not {@code null} and field contains sensitive data 787 * such as passwords). 788 * @param inlinePresentation The {@link InlinePresentation} used to visualize this dataset 789 * as inline suggestions. If the dataset supports inline suggestions, this 790 * should not be null. 791 * 792 * @throws IllegalStateException if {@link #build()} was already called. 793 * 794 * @return this builder. 795 * 796 * @hide 797 */ 798 @SystemApi setFieldInlinePresentation(@onNull AutofillId id, @Nullable AutofillValue value, @Nullable Pattern filter, @NonNull InlinePresentation inlinePresentation)799 public @NonNull Builder setFieldInlinePresentation(@NonNull AutofillId id, 800 @Nullable AutofillValue value, @Nullable Pattern filter, 801 @NonNull InlinePresentation inlinePresentation) { 802 throwIfDestroyed(); 803 Preconditions.checkNotNull(inlinePresentation, "inlinePresentation cannot be null"); 804 setLifeTheUniverseAndEverything(id, value, null, inlinePresentation, 805 new DatasetFieldFilter(filter)); 806 return this; 807 } 808 setLifeTheUniverseAndEverything(@onNull AutofillId id, @Nullable AutofillValue value, @Nullable RemoteViews presentation, @Nullable InlinePresentation inlinePresentation, @Nullable DatasetFieldFilter filter)809 private void setLifeTheUniverseAndEverything(@NonNull AutofillId id, 810 @Nullable AutofillValue value, @Nullable RemoteViews presentation, 811 @Nullable InlinePresentation inlinePresentation, 812 @Nullable DatasetFieldFilter filter) { 813 setLifeTheUniverseAndEverything(id, value, presentation, inlinePresentation, null, 814 filter); 815 } 816 setLifeTheUniverseAndEverything(@onNull AutofillId id, @Nullable AutofillValue value, @Nullable RemoteViews presentation, @Nullable InlinePresentation inlinePresentation, @Nullable InlinePresentation tooltip, @Nullable DatasetFieldFilter filter)817 private void setLifeTheUniverseAndEverything(@NonNull AutofillId id, 818 @Nullable AutofillValue value, @Nullable RemoteViews presentation, 819 @Nullable InlinePresentation inlinePresentation, 820 @Nullable InlinePresentation tooltip, 821 @Nullable DatasetFieldFilter filter) { 822 Preconditions.checkNotNull(id, "id cannot be null"); 823 if (mFieldIds != null) { 824 final int existingIdx = mFieldIds.indexOf(id); 825 if (existingIdx >= 0) { 826 mFieldValues.set(existingIdx, value); 827 mFieldPresentations.set(existingIdx, presentation); 828 mFieldInlinePresentations.set(existingIdx, inlinePresentation); 829 mFieldInlineTooltipPresentations.set(existingIdx, tooltip); 830 mFieldFilters.set(existingIdx, filter); 831 return; 832 } 833 } else { 834 mFieldIds = new ArrayList<>(); 835 mFieldValues = new ArrayList<>(); 836 mFieldPresentations = new ArrayList<>(); 837 mFieldInlinePresentations = new ArrayList<>(); 838 mFieldInlineTooltipPresentations = new ArrayList<>(); 839 mFieldFilters = new ArrayList<>(); 840 } 841 mFieldIds.add(id); 842 mFieldValues.add(value); 843 mFieldPresentations.add(presentation); 844 mFieldInlinePresentations.add(inlinePresentation); 845 mFieldInlineTooltipPresentations.add(tooltip); 846 mFieldFilters.add(filter); 847 } 848 849 /** 850 * Creates a new {@link Dataset} instance. 851 * 852 * <p>You should not interact with this builder once this method is called. 853 * 854 * @throws IllegalStateException if no field was set (through 855 * {@link #setValue(AutofillId, AutofillValue)} or 856 * {@link #setValue(AutofillId, AutofillValue, RemoteViews)} or 857 * {@link #setValue(AutofillId, AutofillValue, RemoteViews, InlinePresentation)}), 858 * or if {@link #build()} was already called. 859 * 860 * @return The built dataset. 861 */ build()862 public @NonNull Dataset build() { 863 throwIfDestroyed(); 864 mDestroyed = true; 865 if (mFieldIds == null) { 866 throw new IllegalStateException("at least one value must be set"); 867 } 868 if (mFieldContent != null) { 869 if (mFieldIds.size() > 1) { 870 throw new IllegalStateException( 871 "when filling content, only one field can be filled"); 872 } 873 if (mFieldValues.get(0) != null) { 874 throw new IllegalStateException("cannot fill both content and values"); 875 } 876 } 877 return new Dataset(this); 878 } 879 throwIfDestroyed()880 private void throwIfDestroyed() { 881 if (mDestroyed) { 882 throw new IllegalStateException("Already called #build()"); 883 } 884 } 885 } 886 887 ///////////////////////////////////// 888 // Parcelable "contract" methods. // 889 ///////////////////////////////////// 890 891 @Override describeContents()892 public int describeContents() { 893 return 0; 894 } 895 896 @Override writeToParcel(Parcel parcel, int flags)897 public void writeToParcel(Parcel parcel, int flags) { 898 parcel.writeParcelable(mPresentation, flags); 899 parcel.writeParcelable(mInlinePresentation, flags); 900 parcel.writeParcelable(mInlineTooltipPresentation, flags); 901 parcel.writeTypedList(mFieldIds, flags); 902 parcel.writeTypedList(mFieldValues, flags); 903 parcel.writeTypedList(mFieldPresentations, flags); 904 parcel.writeTypedList(mFieldInlinePresentations, flags); 905 parcel.writeTypedList(mFieldInlineTooltipPresentations, flags); 906 parcel.writeTypedList(mFieldFilters, flags); 907 parcel.writeParcelable(mFieldContent, flags); 908 parcel.writeParcelable(mAuthentication, flags); 909 parcel.writeString(mId); 910 } 911 912 public static final @NonNull Creator<Dataset> CREATOR = new Creator<Dataset>() { 913 @Override 914 public Dataset createFromParcel(Parcel parcel) { 915 final RemoteViews presentation = parcel.readParcelable(null); 916 final InlinePresentation inlinePresentation = parcel.readParcelable(null); 917 final InlinePresentation inlineTooltipPresentation = 918 parcel.readParcelable(null); 919 final ArrayList<AutofillId> ids = 920 parcel.createTypedArrayList(AutofillId.CREATOR); 921 final ArrayList<AutofillValue> values = 922 parcel.createTypedArrayList(AutofillValue.CREATOR); 923 final ArrayList<RemoteViews> presentations = 924 parcel.createTypedArrayList(RemoteViews.CREATOR); 925 final ArrayList<InlinePresentation> inlinePresentations = 926 parcel.createTypedArrayList(InlinePresentation.CREATOR); 927 final ArrayList<InlinePresentation> inlineTooltipPresentations = 928 parcel.createTypedArrayList(InlinePresentation.CREATOR); 929 final ArrayList<DatasetFieldFilter> filters = 930 parcel.createTypedArrayList(DatasetFieldFilter.CREATOR); 931 final ClipData fieldContent = parcel.readParcelable(null); 932 final IntentSender authentication = parcel.readParcelable(null); 933 final String datasetId = parcel.readString(); 934 935 // Always go through the builder to ensure the data ingested by 936 // the system obeys the contract of the builder to avoid attacks 937 // using specially crafted parcels. 938 final Builder builder = (presentation != null) ? new Builder(presentation) 939 : new Builder(); 940 if (inlinePresentation != null) { 941 if (inlineTooltipPresentation != null) { 942 builder.setInlinePresentation(inlinePresentation, inlineTooltipPresentation); 943 } else { 944 builder.setInlinePresentation(inlinePresentation); 945 } 946 } 947 948 if (fieldContent != null) { 949 builder.setContent(ids.get(0), fieldContent); 950 } 951 final int inlinePresentationsSize = inlinePresentations.size(); 952 for (int i = 0; i < ids.size(); i++) { 953 final AutofillId id = ids.get(i); 954 final AutofillValue value = values.get(i); 955 final RemoteViews fieldPresentation = presentations.get(i); 956 final InlinePresentation fieldInlinePresentation = 957 i < inlinePresentationsSize ? inlinePresentations.get(i) : null; 958 final InlinePresentation fieldInlineTooltipPresentation = 959 i < inlinePresentationsSize ? inlineTooltipPresentations.get(i) : null; 960 final DatasetFieldFilter filter = filters.get(i); 961 builder.setLifeTheUniverseAndEverything(id, value, fieldPresentation, 962 fieldInlinePresentation, fieldInlineTooltipPresentation, filter); 963 } 964 builder.setAuthentication(authentication); 965 builder.setId(datasetId); 966 return builder.build(); 967 } 968 969 @Override 970 public Dataset[] newArray(int size) { 971 return new Dataset[size]; 972 } 973 }; 974 975 /** 976 * Helper class used to indicate when the service explicitly set a {@link Pattern} filter for a 977 * dataset field‐ we cannot use a {@link Pattern} directly because then we wouldn't be 978 * able to differentiate whether the service explicitly passed a {@code null} filter to disable 979 * filter, or when it called the methods that does not take a filter {@link Pattern}. 980 * 981 * @hide 982 */ 983 public static final class DatasetFieldFilter implements Parcelable { 984 985 @Nullable 986 public final Pattern pattern; 987 DatasetFieldFilter(@ullable Pattern pattern)988 private DatasetFieldFilter(@Nullable Pattern pattern) { 989 this.pattern = pattern; 990 } 991 992 @Override toString()993 public String toString() { 994 if (!sDebug) return super.toString(); 995 996 // Cannot log pattern because it could contain PII 997 return pattern == null ? "null" : pattern.pattern().length() + "_chars"; 998 } 999 1000 @Override describeContents()1001 public int describeContents() { 1002 return 0; 1003 } 1004 1005 @Override writeToParcel(Parcel parcel, int flags)1006 public void writeToParcel(Parcel parcel, int flags) { 1007 parcel.writeSerializable(pattern); 1008 } 1009 1010 @SuppressWarnings("hiding") 1011 public static final @android.annotation.NonNull Creator<DatasetFieldFilter> CREATOR = 1012 new Creator<DatasetFieldFilter>() { 1013 1014 @Override 1015 public DatasetFieldFilter createFromParcel(Parcel parcel) { 1016 return new DatasetFieldFilter((Pattern) parcel.readSerializable()); 1017 } 1018 1019 @Override 1020 public DatasetFieldFilter[] newArray(int size) { 1021 return new DatasetFieldFilter[size]; 1022 } 1023 }; 1024 } 1025 } 1026