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.autofillservice.cts.testcore; 18 19 import static android.autofillservice.cts.testcore.UiBot.PORTRAIT; 20 import static android.provider.Settings.Secure.AUTOFILL_SERVICE; 21 import static android.provider.Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE; 22 import static android.provider.Settings.Secure.USER_SETUP_COMPLETE; 23 import static android.service.autofill.FillEventHistory.Event.TYPE_AUTHENTICATION_SELECTED; 24 import static android.service.autofill.FillEventHistory.Event.TYPE_CONTEXT_COMMITTED; 25 import static android.service.autofill.FillEventHistory.Event.TYPE_DATASETS_SHOWN; 26 import static android.service.autofill.FillEventHistory.Event.TYPE_DATASET_AUTHENTICATION_SELECTED; 27 import static android.service.autofill.FillEventHistory.Event.TYPE_DATASET_SELECTED; 28 import static android.service.autofill.FillEventHistory.Event.TYPE_SAVE_SHOWN; 29 30 import static com.android.compatibility.common.util.ShellUtils.runShellCommand; 31 32 import static com.google.common.truth.Truth.assertThat; 33 import static com.google.common.truth.Truth.assertWithMessage; 34 35 import android.app.Activity; 36 import android.app.PendingIntent; 37 import android.app.assist.AssistStructure; 38 import android.app.assist.AssistStructure.ViewNode; 39 import android.app.assist.AssistStructure.WindowNode; 40 import android.autofillservice.cts.R; 41 import android.content.AutofillOptions; 42 import android.content.ComponentName; 43 import android.content.ContentResolver; 44 import android.content.Context; 45 import android.content.Intent; 46 import android.content.pm.PackageManager; 47 import android.content.res.Resources; 48 import android.graphics.Bitmap; 49 import android.icu.util.Calendar; 50 import android.os.Bundle; 51 import android.os.Environment; 52 import android.provider.Settings; 53 import android.service.autofill.FieldClassification; 54 import android.service.autofill.FieldClassification.Match; 55 import android.service.autofill.FillContext; 56 import android.service.autofill.FillEventHistory; 57 import android.service.autofill.InlinePresentation; 58 import android.text.TextUtils; 59 import android.util.Log; 60 import android.util.Pair; 61 import android.util.Size; 62 import android.view.View; 63 import android.view.ViewGroup; 64 import android.view.ViewStructure.HtmlInfo; 65 import android.view.autofill.AutofillId; 66 import android.view.autofill.AutofillManager; 67 import android.view.autofill.AutofillManager.AutofillCallback; 68 import android.view.autofill.AutofillValue; 69 import android.webkit.WebView; 70 import android.widget.RemoteViews; 71 import android.widget.inline.InlinePresentationSpec; 72 73 import androidx.annotation.NonNull; 74 import androidx.annotation.Nullable; 75 import androidx.autofill.inline.v1.InlineSuggestionUi; 76 import androidx.test.platform.app.InstrumentationRegistry; 77 78 import com.android.compatibility.common.util.BitmapUtils; 79 import com.android.compatibility.common.util.OneTimeSettingsListener; 80 import com.android.compatibility.common.util.SettingsUtils; 81 import com.android.compatibility.common.util.ShellUtils; 82 import com.android.compatibility.common.util.TestNameUtils; 83 import com.android.compatibility.common.util.Timeout; 84 85 import java.io.File; 86 import java.io.IOException; 87 import java.util.List; 88 import java.util.Map; 89 import java.util.Map.Entry; 90 import java.util.concurrent.BlockingQueue; 91 import java.util.concurrent.TimeUnit; 92 import java.util.function.Function; 93 import java.util.regex.Pattern; 94 95 /** 96 * Helper for common funcionalities. 97 */ 98 public final class Helper { 99 100 public static final String TAG = "AutoFillCtsHelper"; 101 102 public static final boolean VERBOSE = false; 103 104 public static final String MY_PACKAGE = "android.autofillservice.cts"; 105 106 public static final String ID_USERNAME_LABEL = "username_label"; 107 public static final String ID_USERNAME = "username"; 108 public static final String ID_PASSWORD_LABEL = "password_label"; 109 public static final String ID_PASSWORD = "password"; 110 public static final String ID_LOGIN = "login"; 111 public static final String ID_OUTPUT = "output"; 112 public static final String ID_STATIC_TEXT = "static_text"; 113 public static final String ID_EMPTY = "empty"; 114 public static final String ID_CANCEL_FILL = "cancel_fill"; 115 116 public static final String NULL_DATASET_ID = null; 117 118 public static final char LARGE_STRING_CHAR = '6'; 119 // NOTE: cannot be much large as it could ANR and fail the test. 120 public static final int LARGE_STRING_SIZE = 100_000; 121 public static final String LARGE_STRING = com.android.compatibility.common.util.TextUtils 122 .repeat(LARGE_STRING_CHAR, LARGE_STRING_SIZE); 123 124 /** 125 * Can be used in cases where the autofill values is required by irrelevant (like adding a 126 * value to an authenticated dataset). 127 */ 128 public static final String UNUSED_AUTOFILL_VALUE = null; 129 130 private static final String ACCELLEROMETER_CHANGE = 131 "content insert --uri content://settings/system --bind name:s:accelerometer_rotation " 132 + "--bind value:i:%d"; 133 134 private static final String LOCAL_DIRECTORY = Environment.getExternalStorageDirectory() 135 + "/CtsAutoFillServiceTestCases"; 136 137 private static final Timeout SETTINGS_BASED_SHELL_CMD_TIMEOUT = new Timeout( 138 "SETTINGS_SHELL_CMD_TIMEOUT", OneTimeSettingsListener.DEFAULT_TIMEOUT_MS / 2, 2, 139 OneTimeSettingsListener.DEFAULT_TIMEOUT_MS); 140 141 /** 142 * Helper interface used to filter nodes. 143 * 144 * @param <T> node type 145 */ 146 interface NodeFilter<T> { 147 /** 148 * Returns whether the node passes the filter for such given id. 149 */ matches(T node, Object id)150 boolean matches(T node, Object id); 151 } 152 153 private static final NodeFilter<ViewNode> RESOURCE_ID_FILTER = (node, id) -> { 154 return id.equals(node.getIdEntry()); 155 }; 156 157 private static final NodeFilter<ViewNode> HTML_NAME_FILTER = (node, id) -> { 158 return id.equals(getHtmlName(node)); 159 }; 160 161 private static final NodeFilter<ViewNode> HTML_NAME_OR_RESOURCE_ID_FILTER = (node, id) -> { 162 return id.equals(getHtmlName(node)) || id.equals(node.getIdEntry()); 163 }; 164 165 private static final NodeFilter<ViewNode> TEXT_FILTER = (node, id) -> { 166 return id.equals(node.getText()); 167 }; 168 169 private static final NodeFilter<ViewNode> AUTOFILL_HINT_FILTER = (node, id) -> { 170 return hasHint(node.getAutofillHints(), id); 171 }; 172 173 private static final NodeFilter<ViewNode> WEBVIEW_FORM_FILTER = (node, id) -> { 174 final String className = node.getClassName(); 175 if (!className.equals("android.webkit.WebView")) return false; 176 177 final HtmlInfo htmlInfo = assertHasHtmlTag(node, "form"); 178 final String formName = getAttributeValue(htmlInfo, "name"); 179 return id.equals(formName); 180 }; 181 182 private static final NodeFilter<View> AUTOFILL_HINT_VIEW_FILTER = (view, id) -> { 183 return hasHint(view.getAutofillHints(), id); 184 }; 185 toString(AssistStructure structure, StringBuilder builder)186 private static String toString(AssistStructure structure, StringBuilder builder) { 187 builder.append("[component=").append(structure.getActivityComponent()); 188 final int nodes = structure.getWindowNodeCount(); 189 for (int i = 0; i < nodes; i++) { 190 final WindowNode windowNode = structure.getWindowNodeAt(i); 191 dump(builder, windowNode.getRootViewNode(), " ", 0); 192 } 193 return builder.append(']').toString(); 194 } 195 196 @NonNull toString(@onNull AssistStructure structure)197 public static String toString(@NonNull AssistStructure structure) { 198 return toString(structure, new StringBuilder()); 199 } 200 201 @Nullable toString(@ullable AutofillValue value)202 public static String toString(@Nullable AutofillValue value) { 203 if (value == null) return null; 204 if (value.isText()) { 205 // We don't care about PII... 206 final CharSequence text = value.getTextValue(); 207 return text == null ? null : text.toString(); 208 } 209 return value.toString(); 210 } 211 212 /** 213 * Dump the assist structure on logcat. 214 */ dumpStructure(String message, AssistStructure structure)215 public static void dumpStructure(String message, AssistStructure structure) { 216 Log.i(TAG, toString(structure, new StringBuilder(message))); 217 } 218 219 /** 220 * Dump the contexts on logcat. 221 */ dumpStructure(String message, List<FillContext> contexts)222 public static void dumpStructure(String message, List<FillContext> contexts) { 223 for (FillContext context : contexts) { 224 dumpStructure(message, context.getStructure()); 225 } 226 } 227 228 /** 229 * Dumps the state of the autofill service on logcat. 230 */ dumpAutofillService(@onNull String tag)231 public static void dumpAutofillService(@NonNull String tag) { 232 final String autofillDump = runShellCommand("dumpsys autofill"); 233 Log.i(tag, "dumpsys autofill\n\n" + autofillDump); 234 final String myServiceDump = runShellCommand("dumpsys activity service %s", 235 InstrumentedAutoFillService.SERVICE_NAME); 236 Log.i(tag, "my service dump: \n" + myServiceDump); 237 } 238 239 /** 240 * Dumps the state of {@link android.service.autofill.InlineSuggestionRenderService}, and assert 241 * that it says the number of active inline suggestion views is the given number. 242 * 243 * <p>Note that ideally we should have a test api to fetch the number and verify against it. 244 * But at the time this test is added for Android 11, we have passed the deadline for adding 245 * the new test api, hence this approach. 246 */ assertActiveViewCountFromInlineSuggestionRenderService(int count)247 public static void assertActiveViewCountFromInlineSuggestionRenderService(int count) { 248 String response = runShellCommand( 249 "dumpsys activity service .InlineSuggestionRenderService"); 250 Log.d(TAG, "InlineSuggestionRenderService dump: " + response); 251 Pattern pattern = Pattern.compile(".*mActiveInlineSuggestions: " + count + ".*"); 252 assertWithMessage("Expecting view count " + count 253 + ", but seeing different count from service dumpsys " + response).that( 254 pattern.matcher(response).find()).isTrue(); 255 } 256 257 /** 258 * Sets whether the user completed the initial setup. 259 */ setUserComplete(Context context, boolean complete)260 public static void setUserComplete(Context context, boolean complete) { 261 SettingsUtils.syncSet(context, USER_SETUP_COMPLETE, complete ? "1" : null); 262 } 263 dump(@onNull StringBuilder builder, @NonNull ViewNode node, @NonNull String prefix, int childId)264 private static void dump(@NonNull StringBuilder builder, @NonNull ViewNode node, 265 @NonNull String prefix, int childId) { 266 final int childrenSize = node.getChildCount(); 267 builder.append("\n").append(prefix) 268 .append("child #").append(childId).append(':'); 269 append(builder, "afId", node.getAutofillId()); 270 append(builder, "afType", node.getAutofillType()); 271 append(builder, "afValue", toString(node.getAutofillValue())); 272 append(builder, "resId", node.getIdEntry()); 273 append(builder, "class", node.getClassName()); 274 append(builder, "text", node.getText()); 275 append(builder, "webDomain", node.getWebDomain()); 276 append(builder, "checked", node.isChecked()); 277 append(builder, "focused", node.isFocused()); 278 final HtmlInfo htmlInfo = node.getHtmlInfo(); 279 if (htmlInfo != null) { 280 builder.append(", HtmlInfo[tag=").append(htmlInfo.getTag()) 281 .append(", attrs: ").append(htmlInfo.getAttributes()).append(']'); 282 } 283 if (childrenSize > 0) { 284 append(builder, "#children", childrenSize).append("\n").append(prefix); 285 prefix += " "; 286 if (childrenSize > 0) { 287 for (int i = 0; i < childrenSize; i++) { 288 dump(builder, node.getChildAt(i), prefix, i); 289 } 290 } 291 } 292 } 293 294 /** 295 * Appends a field value to a {@link StringBuilder} when it's not {@code null}. 296 */ 297 @NonNull append(@onNull StringBuilder builder, @NonNull String field, @Nullable Object value)298 public static StringBuilder append(@NonNull StringBuilder builder, @NonNull String field, 299 @Nullable Object value) { 300 if (value == null) return builder; 301 302 if ((value instanceof Boolean) && ((Boolean) value)) { 303 return builder.append(", ").append(field); 304 } 305 306 if (value instanceof Integer && ((Integer) value) == 0 307 || value instanceof CharSequence && TextUtils.isEmpty((CharSequence) value)) { 308 return builder; 309 } 310 311 return builder.append(", ").append(field).append('=').append(value); 312 } 313 314 /** 315 * Appends a field value to a {@link StringBuilder} when it's {@code true}. 316 */ 317 @NonNull append(@onNull StringBuilder builder, @NonNull String field, boolean value)318 public static StringBuilder append(@NonNull StringBuilder builder, @NonNull String field, 319 boolean value) { 320 if (value) { 321 builder.append(", ").append(field); 322 } 323 return builder; 324 } 325 326 /** 327 * Gets a node if it matches the filter criteria for the given id. 328 */ findNodeByFilter(@onNull AssistStructure structure, @NonNull Object id, @NonNull NodeFilter<ViewNode> filter)329 public static ViewNode findNodeByFilter(@NonNull AssistStructure structure, @NonNull Object id, 330 @NonNull NodeFilter<ViewNode> filter) { 331 Log.v(TAG, "Parsing request for activity " + structure.getActivityComponent()); 332 final int nodes = structure.getWindowNodeCount(); 333 for (int i = 0; i < nodes; i++) { 334 final WindowNode windowNode = structure.getWindowNodeAt(i); 335 final ViewNode rootNode = windowNode.getRootViewNode(); 336 final ViewNode node = findNodeByFilter(rootNode, id, filter); 337 if (node != null) { 338 return node; 339 } 340 } 341 return null; 342 } 343 344 /** 345 * Gets a node if it matches the filter criteria for the given id. 346 */ findNodeByFilter(@onNull List<FillContext> contexts, @NonNull Object id, @NonNull NodeFilter<ViewNode> filter)347 public static ViewNode findNodeByFilter(@NonNull List<FillContext> contexts, @NonNull Object id, 348 @NonNull NodeFilter<ViewNode> filter) { 349 for (FillContext context : contexts) { 350 ViewNode node = findNodeByFilter(context.getStructure(), id, filter); 351 if (node != null) { 352 return node; 353 } 354 } 355 return null; 356 } 357 358 /** 359 * Gets a node if it matches the filter criteria for the given id. 360 */ findNodeByFilter(@onNull ViewNode node, @NonNull Object id, @NonNull NodeFilter<ViewNode> filter)361 public static ViewNode findNodeByFilter(@NonNull ViewNode node, @NonNull Object id, 362 @NonNull NodeFilter<ViewNode> filter) { 363 if (filter.matches(node, id)) { 364 return node; 365 } 366 final int childrenSize = node.getChildCount(); 367 if (childrenSize > 0) { 368 for (int i = 0; i < childrenSize; i++) { 369 final ViewNode found = findNodeByFilter(node.getChildAt(i), id, filter); 370 if (found != null) { 371 return found; 372 } 373 } 374 } 375 return null; 376 } 377 378 /** 379 * Gets a node given its Android resource id, or {@code null} if not found. 380 */ findNodeByResourceId(AssistStructure structure, String resourceId)381 public static ViewNode findNodeByResourceId(AssistStructure structure, String resourceId) { 382 return findNodeByFilter(structure, resourceId, RESOURCE_ID_FILTER); 383 } 384 385 /** 386 * Gets a node given its Android resource id, or {@code null} if not found. 387 */ findNodeByResourceId(List<FillContext> contexts, String resourceId)388 public static ViewNode findNodeByResourceId(List<FillContext> contexts, String resourceId) { 389 return findNodeByFilter(contexts, resourceId, RESOURCE_ID_FILTER); 390 } 391 392 /** 393 * Gets a node given its Android resource id, or {@code null} if not found. 394 */ findNodeByResourceId(ViewNode node, String resourceId)395 public static ViewNode findNodeByResourceId(ViewNode node, String resourceId) { 396 return findNodeByFilter(node, resourceId, RESOURCE_ID_FILTER); 397 } 398 399 /** 400 * Gets a node given the name of its HTML INPUT tag, or {@code null} if not found. 401 */ findNodeByHtmlName(AssistStructure structure, String htmlName)402 public static ViewNode findNodeByHtmlName(AssistStructure structure, String htmlName) { 403 return findNodeByFilter(structure, htmlName, HTML_NAME_FILTER); 404 } 405 406 /** 407 * Gets a node given the name of its HTML INPUT tag, or {@code null} if not found. 408 */ findNodeByHtmlName(List<FillContext> contexts, String htmlName)409 public static ViewNode findNodeByHtmlName(List<FillContext> contexts, String htmlName) { 410 return findNodeByFilter(contexts, htmlName, HTML_NAME_FILTER); 411 } 412 413 /** 414 * Gets a node given the name of its HTML INPUT tag, or {@code null} if not found. 415 */ findNodeByHtmlName(ViewNode node, String htmlName)416 public static ViewNode findNodeByHtmlName(ViewNode node, String htmlName) { 417 return findNodeByFilter(node, htmlName, HTML_NAME_FILTER); 418 } 419 420 /** 421 * Gets a node given the value of its (single) autofill hint property, or {@code null} if not 422 * found. 423 */ findNodeByAutofillHint(ViewNode node, String hint)424 public static ViewNode findNodeByAutofillHint(ViewNode node, String hint) { 425 return findNodeByFilter(node, hint, AUTOFILL_HINT_FILTER); 426 } 427 428 /** 429 * Gets a node given the name of its HTML INPUT tag or Android resoirce id, or {@code null} if 430 * not found. 431 */ findNodeByHtmlNameOrResourceId(List<FillContext> contexts, String id)432 public static ViewNode findNodeByHtmlNameOrResourceId(List<FillContext> contexts, String id) { 433 return findNodeByFilter(contexts, id, HTML_NAME_OR_RESOURCE_ID_FILTER); 434 } 435 436 /** 437 * Gets a node given its Android resource id. 438 */ 439 @NonNull findAutofillIdByResourceId(@onNull FillContext context, @NonNull String resourceId)440 public static AutofillId findAutofillIdByResourceId(@NonNull FillContext context, 441 @NonNull String resourceId) { 442 final ViewNode node = findNodeByFilter(context.getStructure(), resourceId, 443 RESOURCE_ID_FILTER); 444 assertWithMessage("No node for resourceId %s", resourceId).that(node).isNotNull(); 445 return node.getAutofillId(); 446 } 447 448 /** 449 * Gets the {@code name} attribute of a node representing an HTML input tag. 450 */ 451 @Nullable getHtmlName(@onNull ViewNode node)452 public static String getHtmlName(@NonNull ViewNode node) { 453 final HtmlInfo htmlInfo = node.getHtmlInfo(); 454 if (htmlInfo == null) { 455 return null; 456 } 457 final String tag = htmlInfo.getTag(); 458 if (!"input".equals(tag)) { 459 Log.w(TAG, "getHtmlName(): invalid tag (" + tag + ") on " + htmlInfo); 460 return null; 461 } 462 for (Pair<String, String> attr : htmlInfo.getAttributes()) { 463 if ("name".equals(attr.first)) { 464 return attr.second; 465 } 466 } 467 Log.w(TAG, "getHtmlName(): no 'name' attribute on " + htmlInfo); 468 return null; 469 } 470 471 /** 472 * Gets a node given its expected text, or {@code null} if not found. 473 */ findNodeByText(AssistStructure structure, String text)474 public static ViewNode findNodeByText(AssistStructure structure, String text) { 475 return findNodeByFilter(structure, text, TEXT_FILTER); 476 } 477 478 /** 479 * Gets a node given its expected text, or {@code null} if not found. 480 */ findNodeByText(ViewNode node, String text)481 public static ViewNode findNodeByText(ViewNode node, String text) { 482 return findNodeByFilter(node, text, TEXT_FILTER); 483 } 484 485 /** 486 * Gets a view that contains the an autofill hint, or {@code null} if not found. 487 */ findViewByAutofillHint(Activity activity, String hint)488 public static View findViewByAutofillHint(Activity activity, String hint) { 489 final View rootView = activity.getWindow().getDecorView().getRootView(); 490 return findViewByAutofillHint(rootView, hint); 491 } 492 493 /** 494 * Gets a view (or a descendant of it) that contains the an autofill hint, or {@code null} if 495 * not found. 496 */ findViewByAutofillHint(View view, String hint)497 public static View findViewByAutofillHint(View view, String hint) { 498 if (AUTOFILL_HINT_VIEW_FILTER.matches(view, hint)) return view; 499 if ((view instanceof ViewGroup)) { 500 final ViewGroup group = (ViewGroup) view; 501 for (int i = 0; i < group.getChildCount(); i++) { 502 final View child = findViewByAutofillHint(group.getChildAt(i), hint); 503 if (child != null) return child; 504 } 505 } 506 return null; 507 } 508 509 /** 510 * Asserts a text-based node is sanitized. 511 */ assertTextIsSanitized(ViewNode node)512 public static void assertTextIsSanitized(ViewNode node) { 513 final CharSequence text = node.getText(); 514 final String resourceId = node.getIdEntry(); 515 if (!TextUtils.isEmpty(text)) { 516 throw new AssertionError("text on sanitized field " + resourceId + ": " + text); 517 } 518 519 assertNotFromResources(node); 520 assertNodeHasNoAutofillValue(node); 521 } 522 assertNotFromResources(ViewNode node)523 private static void assertNotFromResources(ViewNode node) { 524 assertThat(node.getTextIdEntry()).isNull(); 525 } 526 assertNodeHasNoAutofillValue(ViewNode node)527 public static void assertNodeHasNoAutofillValue(ViewNode node) { 528 final AutofillValue value = node.getAutofillValue(); 529 if (value != null) { 530 final String text = value.isText() ? value.getTextValue().toString() : "N/A"; 531 throw new AssertionError("node has value: " + value + " text=" + text); 532 } 533 } 534 535 /** 536 * Asserts the contents of a text-based node that is also auto-fillable. 537 */ assertTextOnly(ViewNode node, String expectedValue)538 public static void assertTextOnly(ViewNode node, String expectedValue) { 539 assertText(node, expectedValue, false); 540 assertNotFromResources(node); 541 } 542 543 /** 544 * Asserts the contents of a text-based node that is also auto-fillable. 545 */ assertTextOnly(AssistStructure structure, String resourceId, String expectedValue)546 public static void assertTextOnly(AssistStructure structure, String resourceId, 547 String expectedValue) { 548 final ViewNode node = findNodeByResourceId(structure, resourceId); 549 assertText(node, expectedValue, false); 550 assertNotFromResources(node); 551 } 552 553 /** 554 * Asserts the contents of a text-based node that is also auto-fillable. 555 */ assertTextAndValue(ViewNode node, String expectedValue)556 public static void assertTextAndValue(ViewNode node, String expectedValue) { 557 assertText(node, expectedValue, true); 558 assertNotFromResources(node); 559 } 560 561 /** 562 * Asserts a text-based node exists and verify its values. 563 */ assertTextAndValue(AssistStructure structure, String resourceId, String expectedValue)564 public static ViewNode assertTextAndValue(AssistStructure structure, String resourceId, 565 String expectedValue) { 566 final ViewNode node = findNodeByResourceId(structure, resourceId); 567 assertTextAndValue(node, expectedValue); 568 return node; 569 } 570 571 /** 572 * Asserts a text-based node exists and is sanitized. 573 */ assertValue(AssistStructure structure, String resourceId, String expectedValue)574 public static ViewNode assertValue(AssistStructure structure, String resourceId, 575 String expectedValue) { 576 final ViewNode node = findNodeByResourceId(structure, resourceId); 577 assertTextValue(node, expectedValue); 578 return node; 579 } 580 581 /** 582 * Asserts the values of a text-based node whose string come from resoruces. 583 */ assertTextFromResources(AssistStructure structure, String resourceId, String expectedValue, boolean isAutofillable, String expectedTextIdEntry)584 public static ViewNode assertTextFromResources(AssistStructure structure, String resourceId, 585 String expectedValue, boolean isAutofillable, String expectedTextIdEntry) { 586 final ViewNode node = findNodeByResourceId(structure, resourceId); 587 assertText(node, expectedValue, isAutofillable); 588 assertThat(node.getTextIdEntry()).isEqualTo(expectedTextIdEntry); 589 return node; 590 } 591 assertHintFromResources(AssistStructure structure, String resourceId, String expectedValue, String expectedHintIdEntry)592 public static ViewNode assertHintFromResources(AssistStructure structure, String resourceId, 593 String expectedValue, String expectedHintIdEntry) { 594 final ViewNode node = findNodeByResourceId(structure, resourceId); 595 assertThat(node.getHint()).isEqualTo(expectedValue); 596 assertThat(node.getHintIdEntry()).isEqualTo(expectedHintIdEntry); 597 return node; 598 } 599 assertText(ViewNode node, String expectedValue, boolean isAutofillable)600 private static void assertText(ViewNode node, String expectedValue, boolean isAutofillable) { 601 assertWithMessage("wrong text on %s", node.getAutofillId()).that(node.getText().toString()) 602 .isEqualTo(expectedValue); 603 final AutofillValue value = node.getAutofillValue(); 604 final AutofillId id = node.getAutofillId(); 605 if (isAutofillable) { 606 assertWithMessage("null auto-fill value on %s", id).that(value).isNotNull(); 607 assertWithMessage("wrong auto-fill value on %s", id) 608 .that(value.getTextValue().toString()).isEqualTo(expectedValue); 609 } else { 610 assertWithMessage("node %s should not have AutofillValue", id).that(value).isNull(); 611 } 612 } 613 614 /** 615 * Asserts the auto-fill value of a text-based node. 616 */ assertTextValue(ViewNode node, String expectedText)617 public static ViewNode assertTextValue(ViewNode node, String expectedText) { 618 final AutofillValue value = node.getAutofillValue(); 619 final AutofillId id = node.getAutofillId(); 620 assertWithMessage("null autofill value on %s", id).that(value).isNotNull(); 621 assertWithMessage("wrong autofill type on %s", id).that(value.isText()).isTrue(); 622 assertWithMessage("wrong autofill value on %s", id).that(value.getTextValue().toString()) 623 .isEqualTo(expectedText); 624 return node; 625 } 626 627 /** 628 * Asserts the auto-fill value of a list-based node. 629 */ assertListValue(ViewNode node, int expectedIndex)630 public static ViewNode assertListValue(ViewNode node, int expectedIndex) { 631 final AutofillValue value = node.getAutofillValue(); 632 final AutofillId id = node.getAutofillId(); 633 assertWithMessage("null autofill value on %s", id).that(value).isNotNull(); 634 assertWithMessage("wrong autofill type on %s", id).that(value.isList()).isTrue(); 635 assertWithMessage("wrong autofill value on %s", id).that(value.getListValue()) 636 .isEqualTo(expectedIndex); 637 return node; 638 } 639 640 /** 641 * Asserts the auto-fill value of a toggle-based node. 642 */ assertToggleValue(ViewNode node, boolean expectedToggle)643 public static void assertToggleValue(ViewNode node, boolean expectedToggle) { 644 final AutofillValue value = node.getAutofillValue(); 645 final AutofillId id = node.getAutofillId(); 646 assertWithMessage("null autofill value on %s", id).that(value).isNotNull(); 647 assertWithMessage("wrong autofill type on %s", id).that(value.isToggle()).isTrue(); 648 assertWithMessage("wrong autofill value on %s", id).that(value.getToggleValue()) 649 .isEqualTo(expectedToggle); 650 } 651 652 /** 653 * Asserts the auto-fill value of a date-based node. 654 */ assertDateValue(Object object, AutofillValue value, int year, int month, int day)655 public static void assertDateValue(Object object, AutofillValue value, int year, int month, 656 int day) { 657 assertWithMessage("null autofill value on %s", object).that(value).isNotNull(); 658 assertWithMessage("wrong autofill type on %s", object).that(value.isDate()).isTrue(); 659 660 final Calendar cal = Calendar.getInstance(); 661 cal.setTimeInMillis(value.getDateValue()); 662 663 assertWithMessage("Wrong year on AutofillValue %s", value) 664 .that(cal.get(Calendar.YEAR)).isEqualTo(year); 665 assertWithMessage("Wrong month on AutofillValue %s", value) 666 .that(cal.get(Calendar.MONTH)).isEqualTo(month); 667 assertWithMessage("Wrong day on AutofillValue %s", value) 668 .that(cal.get(Calendar.DAY_OF_MONTH)).isEqualTo(day); 669 } 670 671 /** 672 * Asserts the auto-fill value of a date-based node. 673 */ assertDateValue(ViewNode node, int year, int month, int day)674 public static void assertDateValue(ViewNode node, int year, int month, int day) { 675 assertDateValue(node, node.getAutofillValue(), year, month, day); 676 } 677 678 /** 679 * Asserts the auto-fill value of a date-based view. 680 */ assertDateValue(View view, int year, int month, int day)681 public static void assertDateValue(View view, int year, int month, int day) { 682 assertDateValue(view, view.getAutofillValue(), year, month, day); 683 } 684 685 /** 686 * Asserts the auto-fill value of a time-based node. 687 */ assertTimeValue(Object object, AutofillValue value, int hour, int minute)688 private static void assertTimeValue(Object object, AutofillValue value, int hour, int minute) { 689 assertWithMessage("null autofill value on %s", object).that(value).isNotNull(); 690 assertWithMessage("wrong autofill type on %s", object).that(value.isDate()).isTrue(); 691 692 final Calendar cal = Calendar.getInstance(); 693 cal.setTimeInMillis(value.getDateValue()); 694 695 assertWithMessage("Wrong hour on AutofillValue %s", value) 696 .that(cal.get(Calendar.HOUR_OF_DAY)).isEqualTo(hour); 697 assertWithMessage("Wrong minute on AutofillValue %s", value) 698 .that(cal.get(Calendar.MINUTE)).isEqualTo(minute); 699 } 700 701 /** 702 * Asserts the auto-fill value of a time-based node. 703 */ assertTimeValue(ViewNode node, int hour, int minute)704 public static void assertTimeValue(ViewNode node, int hour, int minute) { 705 assertTimeValue(node, node.getAutofillValue(), hour, minute); 706 } 707 708 /** 709 * Asserts the auto-fill value of a time-based view. 710 */ assertTimeValue(View view, int hour, int minute)711 public static void assertTimeValue(View view, int hour, int minute) { 712 assertTimeValue(view, view.getAutofillValue(), hour, minute); 713 } 714 715 /** 716 * Asserts a text-based node exists and is sanitized. 717 */ assertTextIsSanitized(AssistStructure structure, String resourceId)718 public static ViewNode assertTextIsSanitized(AssistStructure structure, String resourceId) { 719 final ViewNode node = findNodeByResourceId(structure, resourceId); 720 assertWithMessage("no ViewNode with id %s", resourceId).that(node).isNotNull(); 721 assertTextIsSanitized(node); 722 return node; 723 } 724 725 /** 726 * Asserts a list-based node exists and is sanitized. 727 */ assertListValueIsSanitized(AssistStructure structure, String resourceId)728 public static void assertListValueIsSanitized(AssistStructure structure, String resourceId) { 729 final ViewNode node = findNodeByResourceId(structure, resourceId); 730 assertWithMessage("no ViewNode with id %s", resourceId).that(node).isNotNull(); 731 assertTextIsSanitized(node); 732 } 733 734 /** 735 * Asserts a toggle node exists and is sanitized. 736 */ assertToggleIsSanitized(AssistStructure structure, String resourceId)737 public static void assertToggleIsSanitized(AssistStructure structure, String resourceId) { 738 final ViewNode node = findNodeByResourceId(structure, resourceId); 739 assertNodeHasNoAutofillValue(node); 740 assertWithMessage("ViewNode %s should not be checked", resourceId).that(node.isChecked()) 741 .isFalse(); 742 } 743 744 /** 745 * Asserts a node exists and has the {@code expected} number of children. 746 */ assertNumberOfChildren(AssistStructure structure, String resourceId, int expected)747 public static void assertNumberOfChildren(AssistStructure structure, String resourceId, 748 int expected) { 749 final ViewNode node = findNodeByResourceId(structure, resourceId); 750 final int actual = node.getChildCount(); 751 if (actual != expected) { 752 dumpStructure("assertNumberOfChildren()", structure); 753 throw new AssertionError("assertNumberOfChildren() for " + resourceId 754 + " failed: expected " + expected + ", got " + actual); 755 } 756 } 757 758 /** 759 * Asserts the number of children in the Assist structure. 760 */ assertNumberOfChildren(AssistStructure structure, int expected)761 public static void assertNumberOfChildren(AssistStructure structure, int expected) { 762 assertWithMessage("wrong number of nodes").that(structure.getWindowNodeCount()) 763 .isEqualTo(1); 764 final int actual = getNumberNodes(structure); 765 if (actual != expected) { 766 dumpStructure("assertNumberOfChildren()", structure); 767 throw new AssertionError("assertNumberOfChildren() for structure failed: expected " 768 + expected + ", got " + actual); 769 } 770 } 771 772 /** 773 * Gets the total number of nodes in an structure. 774 */ getNumberNodes(AssistStructure structure)775 public static int getNumberNodes(AssistStructure structure) { 776 int count = 0; 777 final int nodes = structure.getWindowNodeCount(); 778 for (int i = 0; i < nodes; i++) { 779 final WindowNode windowNode = structure.getWindowNodeAt(i); 780 final ViewNode rootNode = windowNode.getRootViewNode(); 781 count += getNumberNodes(rootNode); 782 } 783 return count; 784 } 785 786 /** 787 * Gets the total number of nodes in an node, including all descendants and the node itself. 788 */ getNumberNodes(ViewNode node)789 public static int getNumberNodes(ViewNode node) { 790 int count = 1; 791 final int childrenSize = node.getChildCount(); 792 if (childrenSize > 0) { 793 for (int i = 0; i < childrenSize; i++) { 794 count += getNumberNodes(node.getChildAt(i)); 795 } 796 } 797 return count; 798 } 799 800 /** 801 * Creates an array of {@link AutofillId} mapped from the {@code structure} nodes with the given 802 * {@code resourceIds}. 803 */ getAutofillIds(Function<String, ViewNode> nodeResolver, String[] resourceIds)804 public static AutofillId[] getAutofillIds(Function<String, ViewNode> nodeResolver, 805 String[] resourceIds) { 806 if (resourceIds == null) return null; 807 808 final AutofillId[] requiredIds = new AutofillId[resourceIds.length]; 809 for (int i = 0; i < resourceIds.length; i++) { 810 final String resourceId = resourceIds[i]; 811 final ViewNode node = nodeResolver.apply(resourceId); 812 if (node == null) { 813 throw new AssertionError("No node with resourceId " + resourceId); 814 } 815 requiredIds[i] = node.getAutofillId(); 816 817 } 818 return requiredIds; 819 } 820 821 /** 822 * Get an {@link AutofillId} mapped from the {@code structure} node with the given 823 * {@code resourceId}. 824 */ getAutofillId(Function<String, ViewNode> nodeResolver, String resourceId)825 public static AutofillId getAutofillId(Function<String, ViewNode> nodeResolver, 826 String resourceId) { 827 if (resourceId == null) return null; 828 829 final ViewNode node = nodeResolver.apply(resourceId); 830 if (node == null) { 831 throw new AssertionError("No node with resourceId " + resourceId); 832 } 833 return node.getAutofillId(); 834 } 835 836 /** 837 * Prevents the screen to rotate by itself 838 */ disableAutoRotation(UiBot uiBot)839 public static void disableAutoRotation(UiBot uiBot) throws Exception { 840 runShellCommand(ACCELLEROMETER_CHANGE, 0); 841 uiBot.setScreenOrientation(PORTRAIT); 842 } 843 844 /** 845 * Allows the screen to rotate by itself 846 */ allowAutoRotation()847 public static void allowAutoRotation() { 848 runShellCommand(ACCELLEROMETER_CHANGE, 1); 849 } 850 851 /** 852 * Gets the maximum number of partitions per session. 853 */ getMaxPartitions()854 public static int getMaxPartitions() { 855 return Integer.parseInt(runShellCommand("cmd autofill get max_partitions")); 856 } 857 858 /** 859 * Sets the maximum number of partitions per session. 860 */ setMaxPartitions(int value)861 public static void setMaxPartitions(int value) throws Exception { 862 runShellCommand("cmd autofill set max_partitions %d", value); 863 SETTINGS_BASED_SHELL_CMD_TIMEOUT.run("get max_partitions", () -> { 864 return getMaxPartitions() == value ? Boolean.TRUE : null; 865 }); 866 } 867 868 /** 869 * Gets the maximum number of visible datasets. 870 */ getMaxVisibleDatasets()871 public static int getMaxVisibleDatasets() { 872 return Integer.parseInt(runShellCommand("cmd autofill get max_visible_datasets")); 873 } 874 875 /** 876 * Sets the maximum number of visible datasets. 877 */ setMaxVisibleDatasets(int value)878 public static void setMaxVisibleDatasets(int value) throws Exception { 879 runShellCommand("cmd autofill set max_visible_datasets %d", value); 880 SETTINGS_BASED_SHELL_CMD_TIMEOUT.run("get max_visible_datasets", () -> { 881 return getMaxVisibleDatasets() == value ? Boolean.TRUE : null; 882 }); 883 } 884 885 /** 886 * Checks if autofill window is fullscreen, see com.android.server.autofill.ui.FillUi. 887 */ isAutofillWindowFullScreen(Context context)888 public static boolean isAutofillWindowFullScreen(Context context) { 889 return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK); 890 } 891 892 /** 893 * Checks if screen orientation can be changed. 894 */ isRotationSupported(Context context)895 public static boolean isRotationSupported(Context context) { 896 final PackageManager packageManager = context.getPackageManager(); 897 if (packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) { 898 Log.v(TAG, "isRotationSupported(): is auto"); 899 return false; 900 } 901 if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) { 902 Log.v(TAG, "isRotationSupported(): has leanback feature"); 903 return false; 904 } 905 if (packageManager.hasSystemFeature(PackageManager.FEATURE_PC)) { 906 Log.v(TAG, "isRotationSupported(): is PC"); 907 return false; 908 } 909 return true; 910 } 911 getBoolean(Context context, String id)912 private static boolean getBoolean(Context context, String id) { 913 final Resources resources = context.getResources(); 914 final int booleanId = resources.getIdentifier(id, "bool", "android"); 915 return resources.getBoolean(booleanId); 916 } 917 918 /** 919 * Uses Shell command to get the Autofill logging level. 920 */ getLoggingLevel()921 public static String getLoggingLevel() { 922 return runShellCommand("cmd autofill get log_level"); 923 } 924 925 /** 926 * Uses Shell command to set the Autofill logging level. 927 */ setLoggingLevel(String level)928 public static void setLoggingLevel(String level) { 929 runShellCommand("cmd autofill set log_level %s", level); 930 } 931 932 /** 933 * Uses Settings to enable the given autofill service for the default user, and checks the 934 * value was properly check, throwing an exception if it was not. 935 */ enableAutofillService(@onNull Context context, @NonNull String serviceName)936 public static void enableAutofillService(@NonNull Context context, 937 @NonNull String serviceName) { 938 if (isAutofillServiceEnabled(serviceName)) return; 939 940 // Sets the setting synchronously. Note that the config itself is sets synchronously but 941 // launch of the service is asynchronous after the config is updated. 942 SettingsUtils.syncSet(context, AUTOFILL_SERVICE, serviceName); 943 944 // Waits until the service is actually enabled. 945 try { 946 Timeouts.CONNECTION_TIMEOUT.run("Enabling Autofill service", () -> { 947 return isAutofillServiceEnabled(serviceName) ? serviceName : null; 948 }); 949 } catch (Exception e) { 950 throw new AssertionError("Enabling Autofill service failed."); 951 } 952 } 953 954 /** 955 * Uses Settings to disable the given autofill service for the default user, and waits until 956 * the setting is deleted. 957 */ disableAutofillService(@onNull Context context)958 public static void disableAutofillService(@NonNull Context context) { 959 final String currentService = SettingsUtils.get(AUTOFILL_SERVICE); 960 if (currentService == null) { 961 Log.v(TAG, "disableAutofillService(): already disabled"); 962 return; 963 } 964 Log.v(TAG, "Disabling " + currentService); 965 SettingsUtils.syncDelete(context, AUTOFILL_SERVICE); 966 } 967 968 /** 969 * Checks whether the given service is set as the autofill service for the default user. 970 */ isAutofillServiceEnabled(@onNull String serviceName)971 public static boolean isAutofillServiceEnabled(@NonNull String serviceName) { 972 final String actualName = getAutofillServiceName(); 973 return serviceName.equals(actualName); 974 } 975 976 /** 977 * Gets then name of the autofill service for the default user. 978 */ getAutofillServiceName()979 public static String getAutofillServiceName() { 980 return SettingsUtils.get(AUTOFILL_SERVICE); 981 } 982 983 /** 984 * Asserts whether the given service is enabled as the autofill service for the default user. 985 */ assertAutofillServiceStatus(@onNull String serviceName, boolean enabled)986 public static void assertAutofillServiceStatus(@NonNull String serviceName, boolean enabled) { 987 final String actual = SettingsUtils.get(AUTOFILL_SERVICE); 988 final String expected = enabled ? serviceName : null; 989 assertWithMessage("Invalid value for secure setting %s", AUTOFILL_SERVICE) 990 .that(actual).isEqualTo(expected); 991 } 992 993 /** 994 * Enables / disables the default augmented autofill service. 995 */ setDefaultAugmentedAutofillServiceEnabled(boolean enabled)996 public static void setDefaultAugmentedAutofillServiceEnabled(boolean enabled) { 997 Log.d(TAG, "setDefaultAugmentedAutofillServiceEnabled(): " + enabled); 998 runShellCommand("cmd autofill set default-augmented-service-enabled 0 %s", 999 Boolean.toString(enabled)); 1000 } 1001 1002 /** 1003 * Gets the instrumentation context. 1004 */ getContext()1005 public static Context getContext() { 1006 return InstrumentationRegistry.getInstrumentation().getContext(); 1007 } 1008 1009 /** 1010 * Asserts the node has an {@code HTMLInfo} property, with the given tag. 1011 */ assertHasHtmlTag(ViewNode node, String expectedTag)1012 public static HtmlInfo assertHasHtmlTag(ViewNode node, String expectedTag) { 1013 final HtmlInfo info = node.getHtmlInfo(); 1014 assertWithMessage("node doesn't have htmlInfo").that(info).isNotNull(); 1015 assertWithMessage("wrong tag").that(info.getTag()).isEqualTo(expectedTag); 1016 return info; 1017 } 1018 1019 /** 1020 * Gets the value of an {@code HTMLInfo} attribute. 1021 */ 1022 @Nullable getAttributeValue(HtmlInfo info, String attribute)1023 public static String getAttributeValue(HtmlInfo info, String attribute) { 1024 for (Pair<String, String> pair : info.getAttributes()) { 1025 if (pair.first.equals(attribute)) { 1026 return pair.second; 1027 } 1028 } 1029 return null; 1030 } 1031 1032 /** 1033 * Asserts a {@code HTMLInfo} has an attribute with a given value. 1034 */ assertHasAttribute(HtmlInfo info, String attribute, String expectedValue)1035 public static void assertHasAttribute(HtmlInfo info, String attribute, String expectedValue) { 1036 final String actualValue = getAttributeValue(info, attribute); 1037 assertWithMessage("Attribute %s not found", attribute).that(actualValue).isNotNull(); 1038 assertWithMessage("Wrong value for Attribute %s", attribute) 1039 .that(actualValue).isEqualTo(expectedValue); 1040 } 1041 1042 /** 1043 * Finds a {@link WebView} node given its expected form name. 1044 */ findWebViewNodeByFormName(AssistStructure structure, String formName)1045 public static ViewNode findWebViewNodeByFormName(AssistStructure structure, String formName) { 1046 return findNodeByFilter(structure, formName, WEBVIEW_FORM_FILTER); 1047 } 1048 assertClientState(Object container, Bundle clientState, String key, String value)1049 private static void assertClientState(Object container, Bundle clientState, 1050 String key, String value) { 1051 assertWithMessage("'%s' should have client state", container) 1052 .that(clientState).isNotNull(); 1053 assertWithMessage("Wrong number of client state extras on '%s'", container) 1054 .that(clientState.keySet().size()).isEqualTo(1); 1055 assertWithMessage("Wrong value for client state key (%s) on '%s'", key, container) 1056 .that(clientState.getString(key)).isEqualTo(value); 1057 } 1058 1059 /** 1060 * Asserts the content of a {@link FillEventHistory#getClientState()}. 1061 * 1062 * @param history event to be asserted 1063 * @param key the only key expected in the client state bundle 1064 * @param value the only value expected in the client state bundle 1065 */ 1066 @SuppressWarnings("javadoc") assertDeprecatedClientState(@onNull FillEventHistory history, @NonNull String key, @NonNull String value)1067 public static void assertDeprecatedClientState(@NonNull FillEventHistory history, 1068 @NonNull String key, @NonNull String value) { 1069 assertThat(history).isNotNull(); 1070 @SuppressWarnings("deprecation") 1071 final Bundle clientState = history.getClientState(); 1072 assertClientState(history, clientState, key, value); 1073 } 1074 1075 /** 1076 * Asserts the {@link FillEventHistory#getClientState()} is not set. 1077 * 1078 * @param history event to be asserted 1079 */ 1080 @SuppressWarnings("javadoc") assertNoDeprecatedClientState(@onNull FillEventHistory history)1081 public static void assertNoDeprecatedClientState(@NonNull FillEventHistory history) { 1082 assertThat(history).isNotNull(); 1083 @SuppressWarnings("deprecation") 1084 final Bundle clientState = history.getClientState(); 1085 assertWithMessage("History '%s' should not have client state", history) 1086 .that(clientState).isNull(); 1087 } 1088 1089 /** 1090 * Asserts the content of a {@link android.service.autofill.FillEventHistory.Event}. 1091 * 1092 * @param event event to be asserted 1093 * @param eventType expected type 1094 * @param datasetId dataset set id expected in the event 1095 * @param key the only key expected in the client state bundle (or {@code null} if it shouldn't 1096 * have client state) 1097 * @param value the only value expected in the client state bundle (or {@code null} if it 1098 * shouldn't have client state) 1099 * @param fieldClassificationResults expected results when asserting field classification 1100 */ assertFillEvent(@onNull FillEventHistory.Event event, int eventType, @Nullable String datasetId, @Nullable String key, @Nullable String value, @Nullable FieldClassificationResult[] fieldClassificationResults)1101 private static void assertFillEvent(@NonNull FillEventHistory.Event event, 1102 int eventType, @Nullable String datasetId, 1103 @Nullable String key, @Nullable String value, 1104 @Nullable FieldClassificationResult[] fieldClassificationResults) { 1105 assertThat(event).isNotNull(); 1106 assertWithMessage("Wrong type for %s", event).that(event.getType()).isEqualTo(eventType); 1107 if (datasetId == null) { 1108 assertWithMessage("Event %s should not have dataset id", event) 1109 .that(event.getDatasetId()).isNull(); 1110 } else { 1111 assertWithMessage("Wrong dataset id for %s", event) 1112 .that(event.getDatasetId()).isEqualTo(datasetId); 1113 } 1114 final Bundle clientState = event.getClientState(); 1115 if (key == null) { 1116 assertWithMessage("Event '%s' should not have client state", event) 1117 .that(clientState).isNull(); 1118 } else { 1119 assertClientState(event, clientState, key, value); 1120 } 1121 assertWithMessage("Event '%s' should not have selected datasets", event) 1122 .that(event.getSelectedDatasetIds()).isEmpty(); 1123 assertWithMessage("Event '%s' should not have ignored datasets", event) 1124 .that(event.getIgnoredDatasetIds()).isEmpty(); 1125 assertWithMessage("Event '%s' should not have changed fields", event) 1126 .that(event.getChangedFields()).isEmpty(); 1127 assertWithMessage("Event '%s' should not have manually-entered fields", event) 1128 .that(event.getManuallyEnteredField()).isEmpty(); 1129 final Map<AutofillId, FieldClassification> detectedFields = event.getFieldsClassification(); 1130 if (fieldClassificationResults == null) { 1131 assertThat(detectedFields).isEmpty(); 1132 } else { 1133 assertThat(detectedFields).hasSize(fieldClassificationResults.length); 1134 int i = 0; 1135 for (Entry<AutofillId, FieldClassification> entry : detectedFields.entrySet()) { 1136 assertMatches(i, entry, fieldClassificationResults[i]); 1137 i++; 1138 } 1139 } 1140 } 1141 assertMatches(int i, Entry<AutofillId, FieldClassification> actualResult, FieldClassificationResult expectedResult)1142 private static void assertMatches(int i, Entry<AutofillId, FieldClassification> actualResult, 1143 FieldClassificationResult expectedResult) { 1144 assertWithMessage("Wrong field id at index %s", i).that(actualResult.getKey()) 1145 .isEqualTo(expectedResult.id); 1146 final List<Match> matches = actualResult.getValue().getMatches(); 1147 assertWithMessage("Wrong number of matches: " + matches).that(matches.size()) 1148 .isEqualTo(expectedResult.categoryIds.length); 1149 for (int j = 0; j < matches.size(); j++) { 1150 final Match match = matches.get(j); 1151 assertWithMessage("Wrong categoryId at (%s, %s): %s", i, j, match) 1152 .that(match.getCategoryId()).isEqualTo(expectedResult.categoryIds[j]); 1153 assertWithMessage("Wrong score at (%s, %s): %s", i, j, match) 1154 .that(match.getScore()).isWithin(0.01f).of(expectedResult.scores[j]); 1155 } 1156 } 1157 1158 /** 1159 * Asserts the content of a 1160 * {@link android.service.autofill.FillEventHistory.Event#TYPE_DATASET_SELECTED} event. 1161 * 1162 * @param event event to be asserted 1163 * @param datasetId dataset set id expected in the event 1164 */ assertFillEventForDatasetSelected(@onNull FillEventHistory.Event event, @Nullable String datasetId)1165 public static void assertFillEventForDatasetSelected(@NonNull FillEventHistory.Event event, 1166 @Nullable String datasetId) { 1167 assertFillEvent(event, TYPE_DATASET_SELECTED, datasetId, null, null, null); 1168 } 1169 1170 /** 1171 * Asserts the content of a 1172 * {@link android.service.autofill.FillEventHistory.Event#TYPE_DATASET_SELECTED} event. 1173 * 1174 * @param event event to be asserted 1175 * @param datasetId dataset set id expected in the event 1176 * @param key the only key expected in the client state bundle 1177 * @param value the only value expected in the client state bundle 1178 */ assertFillEventForDatasetSelected(@onNull FillEventHistory.Event event, @Nullable String datasetId, @Nullable String key, @Nullable String value)1179 public static void assertFillEventForDatasetSelected(@NonNull FillEventHistory.Event event, 1180 @Nullable String datasetId, @Nullable String key, @Nullable String value) { 1181 assertFillEvent(event, TYPE_DATASET_SELECTED, datasetId, key, value, null); 1182 } 1183 1184 /** 1185 * Asserts the content of a 1186 * {@link android.service.autofill.FillEventHistory.Event#TYPE_SAVE_SHOWN} event. 1187 * 1188 * @param event event to be asserted 1189 * @param datasetId dataset set id expected in the event 1190 * @param key the only key expected in the client state bundle 1191 * @param value the only value expected in the client state bundle 1192 */ assertFillEventForSaveShown(@onNull FillEventHistory.Event event, @Nullable String datasetId, @NonNull String key, @NonNull String value)1193 public static void assertFillEventForSaveShown(@NonNull FillEventHistory.Event event, 1194 @Nullable String datasetId, @NonNull String key, @NonNull String value) { 1195 assertFillEvent(event, TYPE_SAVE_SHOWN, datasetId, key, value, null); 1196 } 1197 1198 /** 1199 * Asserts the content of a 1200 * {@link android.service.autofill.FillEventHistory.Event#TYPE_SAVE_SHOWN} event. 1201 * 1202 * @param event event to be asserted 1203 * @param datasetId dataset set id expected in the event 1204 */ assertFillEventForSaveShown(@onNull FillEventHistory.Event event, @Nullable String datasetId)1205 public static void assertFillEventForSaveShown(@NonNull FillEventHistory.Event event, 1206 @Nullable String datasetId) { 1207 assertFillEvent(event, TYPE_SAVE_SHOWN, datasetId, null, null, null); 1208 } 1209 1210 /** 1211 * Asserts the content of a 1212 * {@link android.service.autofill.FillEventHistory.Event#TYPE_DATASETS_SHOWN} event. 1213 * 1214 * @param event event to be asserted 1215 * @param key the only key expected in the client state bundle 1216 * @param value the only value expected in the client state bundle 1217 */ assertFillEventForDatasetShown(@onNull FillEventHistory.Event event, @NonNull String key, @NonNull String value)1218 public static void assertFillEventForDatasetShown(@NonNull FillEventHistory.Event event, 1219 @NonNull String key, @NonNull String value) { 1220 assertFillEvent(event, TYPE_DATASETS_SHOWN, NULL_DATASET_ID, key, value, null); 1221 } 1222 1223 /** 1224 * Asserts the content of a 1225 * {@link android.service.autofill.FillEventHistory.Event#TYPE_DATASETS_SHOWN} event. 1226 * 1227 * @param event event to be asserted 1228 */ assertFillEventForDatasetShown(@onNull FillEventHistory.Event event)1229 public static void assertFillEventForDatasetShown(@NonNull FillEventHistory.Event event) { 1230 assertFillEvent(event, TYPE_DATASETS_SHOWN, NULL_DATASET_ID, null, null, null); 1231 } 1232 1233 /** 1234 * Asserts the content of a 1235 * {@link android.service.autofill.FillEventHistory.Event#TYPE_DATASET_AUTHENTICATION_SELECTED} 1236 * event. 1237 * 1238 * @param event event to be asserted 1239 * @param datasetId dataset set id expected in the event 1240 * @param key the only key expected in the client state bundle 1241 * @param value the only value expected in the client state bundle 1242 */ assertFillEventForDatasetAuthenticationSelected( @onNull FillEventHistory.Event event, @Nullable String datasetId, @NonNull String key, @NonNull String value)1243 public static void assertFillEventForDatasetAuthenticationSelected( 1244 @NonNull FillEventHistory.Event event, 1245 @Nullable String datasetId, @NonNull String key, @NonNull String value) { 1246 assertFillEvent(event, TYPE_DATASET_AUTHENTICATION_SELECTED, datasetId, key, value, null); 1247 } 1248 1249 /** 1250 * Asserts the content of a 1251 * {@link android.service.autofill.FillEventHistory.Event#TYPE_AUTHENTICATION_SELECTED} event. 1252 * 1253 * @param event event to be asserted 1254 * @param datasetId dataset set id expected in the event 1255 * @param key the only key expected in the client state bundle 1256 * @param value the only value expected in the client state bundle 1257 */ assertFillEventForAuthenticationSelected( @onNull FillEventHistory.Event event, @Nullable String datasetId, @NonNull String key, @NonNull String value)1258 public static void assertFillEventForAuthenticationSelected( 1259 @NonNull FillEventHistory.Event event, 1260 @Nullable String datasetId, @NonNull String key, @NonNull String value) { 1261 assertFillEvent(event, TYPE_AUTHENTICATION_SELECTED, datasetId, key, value, null); 1262 } 1263 assertFillEventForFieldsClassification(@onNull FillEventHistory.Event event, @NonNull AutofillId fieldId, @NonNull String categoryId, float score)1264 public static void assertFillEventForFieldsClassification(@NonNull FillEventHistory.Event event, 1265 @NonNull AutofillId fieldId, @NonNull String categoryId, float score) { 1266 assertFillEvent(event, TYPE_CONTEXT_COMMITTED, null, null, null, 1267 new FieldClassificationResult[] { 1268 new FieldClassificationResult(fieldId, categoryId, score) 1269 }); 1270 } 1271 assertFillEventForFieldsClassification(@onNull FillEventHistory.Event event, @NonNull FieldClassificationResult[] results)1272 public static void assertFillEventForFieldsClassification(@NonNull FillEventHistory.Event event, 1273 @NonNull FieldClassificationResult[] results) { 1274 assertFillEvent(event, TYPE_CONTEXT_COMMITTED, null, null, null, results); 1275 } 1276 assertFillEventForContextCommitted(@onNull FillEventHistory.Event event)1277 public static void assertFillEventForContextCommitted(@NonNull FillEventHistory.Event event) { 1278 assertFillEvent(event, TYPE_CONTEXT_COMMITTED, null, null, null, null); 1279 } 1280 1281 @NonNull getActivityName(List<FillContext> contexts)1282 public static String getActivityName(List<FillContext> contexts) { 1283 if (contexts == null) return "N/A (null contexts)"; 1284 1285 if (contexts.isEmpty()) return "N/A (empty contexts)"; 1286 1287 final AssistStructure structure = contexts.get(contexts.size() - 1).getStructure(); 1288 if (structure == null) return "N/A (no AssistStructure)"; 1289 1290 final ComponentName componentName = structure.getActivityComponent(); 1291 if (componentName == null) return "N/A (no component name)"; 1292 1293 return componentName.flattenToShortString(); 1294 } 1295 assertFloat(float actualValue, float expectedValue)1296 public static void assertFloat(float actualValue, float expectedValue) { 1297 assertThat(actualValue).isWithin(1.0e-10f).of(expectedValue); 1298 } 1299 assertHasFlags(int actualFlags, int expectedFlags)1300 public static void assertHasFlags(int actualFlags, int expectedFlags) { 1301 assertWithMessage("Flags %s not in %s", expectedFlags, actualFlags) 1302 .that(actualFlags & expectedFlags).isEqualTo(expectedFlags); 1303 } 1304 callbackEventAsString(int event)1305 public static String callbackEventAsString(int event) { 1306 switch (event) { 1307 case AutofillCallback.EVENT_INPUT_HIDDEN: 1308 return "HIDDEN"; 1309 case AutofillCallback.EVENT_INPUT_SHOWN: 1310 return "SHOWN"; 1311 case AutofillCallback.EVENT_INPUT_UNAVAILABLE: 1312 return "UNAVAILABLE"; 1313 default: 1314 return "UNKNOWN:" + event; 1315 } 1316 } 1317 importantForAutofillAsString(int mode)1318 public static String importantForAutofillAsString(int mode) { 1319 switch (mode) { 1320 case View.IMPORTANT_FOR_AUTOFILL_AUTO: 1321 return "IMPORTANT_FOR_AUTOFILL_AUTO"; 1322 case View.IMPORTANT_FOR_AUTOFILL_YES: 1323 return "IMPORTANT_FOR_AUTOFILL_YES"; 1324 case View.IMPORTANT_FOR_AUTOFILL_YES_EXCLUDE_DESCENDANTS: 1325 return "IMPORTANT_FOR_AUTOFILL_YES_EXCLUDE_DESCENDANTS"; 1326 case View.IMPORTANT_FOR_AUTOFILL_NO: 1327 return "IMPORTANT_FOR_AUTOFILL_NO"; 1328 case View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS: 1329 return "IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS"; 1330 default: 1331 return "UNKNOWN:" + mode; 1332 } 1333 } 1334 hasHint(@ullable String[] hints, @Nullable Object expectedHint)1335 public static boolean hasHint(@Nullable String[] hints, @Nullable Object expectedHint) { 1336 if (hints == null || expectedHint == null) return false; 1337 for (String actualHint : hints) { 1338 if (expectedHint.equals(actualHint)) return true; 1339 } 1340 return false; 1341 } 1342 newClientState(String key, String value)1343 public static Bundle newClientState(String key, String value) { 1344 final Bundle clientState = new Bundle(); 1345 clientState.putString(key, value); 1346 return clientState; 1347 } 1348 assertAuthenticationClientState(String where, Bundle data, String expectedKey, String expectedValue)1349 public static void assertAuthenticationClientState(String where, Bundle data, 1350 String expectedKey, String expectedValue) { 1351 assertWithMessage("no client state on %s", where).that(data).isNotNull(); 1352 final String extraValue = data.getString(expectedKey); 1353 assertWithMessage("invalid value for %s on %s", expectedKey, where) 1354 .that(extraValue).isEqualTo(expectedValue); 1355 } 1356 1357 /** 1358 * Asserts that 2 bitmaps have are the same. If they aren't throws an exception and dump them 1359 * locally so their can be visually inspected. 1360 * 1361 * @param filename base name of the files generated in case of error 1362 * @param bitmap1 first bitmap to be compared 1363 * @param bitmap2 second bitmap to be compared 1364 */ 1365 // TODO: move to common code assertBitmapsAreSame(@onNull String filename, @Nullable Bitmap bitmap1, @Nullable Bitmap bitmap2)1366 public static void assertBitmapsAreSame(@NonNull String filename, @Nullable Bitmap bitmap1, 1367 @Nullable Bitmap bitmap2) throws IOException { 1368 assertWithMessage("1st bitmap is null").that(bitmap1).isNotNull(); 1369 assertWithMessage("2nd bitmap is null").that(bitmap2).isNotNull(); 1370 final boolean same = bitmap1.sameAs(bitmap2); 1371 if (same) { 1372 Log.v(TAG, "bitmap comparison passed for " + filename); 1373 return; 1374 } 1375 1376 final File dir = getLocalDirectory(); 1377 if (dir == null) { 1378 throw new AssertionError("bitmap comparison failed for " + filename 1379 + ", and bitmaps could not be dumped on " + dir); 1380 } 1381 final File dump1 = dumpBitmap(bitmap1, dir, filename + "-1.png"); 1382 final File dump2 = dumpBitmap(bitmap2, dir, filename + "-2.png"); 1383 throw new AssertionError( 1384 "bitmap comparison failed; check contents of " + dump1 + " and " + dump2); 1385 } 1386 1387 @Nullable getLocalDirectory()1388 private static File getLocalDirectory() { 1389 final File dir = new File(LOCAL_DIRECTORY); 1390 dir.mkdirs(); 1391 if (!dir.exists()) { 1392 Log.e(TAG, "Could not create directory " + dir); 1393 return null; 1394 } 1395 return dir; 1396 } 1397 1398 @Nullable createFile(@onNull File dir, @NonNull String filename)1399 private static File createFile(@NonNull File dir, @NonNull String filename) throws IOException { 1400 final File file = new File(dir, filename); 1401 if (file.exists()) { 1402 Log.v(TAG, "Deleting file " + file); 1403 file.delete(); 1404 } 1405 if (!file.createNewFile()) { 1406 Log.e(TAG, "Could not create file " + file); 1407 return null; 1408 } 1409 return file; 1410 } 1411 1412 @Nullable dumpBitmap(@onNull Bitmap bitmap, @NonNull File dir, @NonNull String filename)1413 private static File dumpBitmap(@NonNull Bitmap bitmap, @NonNull File dir, 1414 @NonNull String filename) throws IOException { 1415 final File file = createFile(dir, filename); 1416 if (file != null) { 1417 dumpBitmap(bitmap, file); 1418 1419 } 1420 return file; 1421 } 1422 1423 @Nullable dumpBitmap(@onNull Bitmap bitmap, @NonNull File file)1424 public static File dumpBitmap(@NonNull Bitmap bitmap, @NonNull File file) { 1425 Log.i(TAG, "Dumping bitmap at " + file); 1426 BitmapUtils.saveBitmap(bitmap, file.getParent(), file.getName()); 1427 return file; 1428 } 1429 1430 /** 1431 * Creates a file in the device, using the name of the current test as a prefix. 1432 */ 1433 @Nullable createTestFile(@onNull String name)1434 public static File createTestFile(@NonNull String name) throws IOException { 1435 final File dir = getLocalDirectory(); 1436 if (dir == null) return null; 1437 1438 final String prefix = TestNameUtils.getCurrentTestName().replaceAll("\\.|\\(|\\/", "_") 1439 .replaceAll("\\)", ""); 1440 final String filename = prefix + "-" + name; 1441 1442 return createFile(dir, filename); 1443 } 1444 1445 /** 1446 * Offers an object to a queue or times out. 1447 * 1448 * @return {@code true} if the offer was accepted, {$code false} if it timed out or was 1449 * interrupted. 1450 */ offer(BlockingQueue<T> queue, T obj, long timeoutMs)1451 public static <T> boolean offer(BlockingQueue<T> queue, T obj, long timeoutMs) { 1452 boolean offered = false; 1453 try { 1454 offered = queue.offer(obj, timeoutMs, TimeUnit.MILLISECONDS); 1455 } catch (InterruptedException e) { 1456 Log.w(TAG, "interrupted offering", e); 1457 Thread.currentThread().interrupt(); 1458 } 1459 if (!offered) { 1460 Log.e(TAG, "could not offer " + obj + " in " + timeoutMs + "ms"); 1461 } 1462 return offered; 1463 } 1464 1465 /** 1466 * Calls this method to assert given {@code string} is equal to {@link #LARGE_STRING}, as 1467 * comparing its value using standard assertions might ANR. 1468 */ assertEqualsToLargeString(@onNull String string)1469 public static void assertEqualsToLargeString(@NonNull String string) { 1470 assertThat(string).isNotNull(); 1471 assertThat(string).hasLength(LARGE_STRING_SIZE); 1472 assertThat(string.charAt(0)).isEqualTo(LARGE_STRING_CHAR); 1473 assertThat(string.charAt(LARGE_STRING_SIZE - 1)).isEqualTo(LARGE_STRING_CHAR); 1474 } 1475 1476 /** 1477 * Asserts that autofill is enabled in the context, retrying if necessariy. 1478 */ assertAutofillEnabled(@onNull Context context, boolean expected)1479 public static void assertAutofillEnabled(@NonNull Context context, boolean expected) 1480 throws Exception { 1481 assertAutofillEnabled(context.getSystemService(AutofillManager.class), expected); 1482 } 1483 1484 /** 1485 * Asserts that autofill is enabled in the manager, retrying if necessariy. 1486 */ assertAutofillEnabled(@onNull AutofillManager afm, boolean expected)1487 public static void assertAutofillEnabled(@NonNull AutofillManager afm, boolean expected) 1488 throws Exception { 1489 Timeouts.IDLE_UNBIND_TIMEOUT.run("assertEnabled(" + expected + ")", () -> { 1490 final boolean actual = afm.isEnabled(); 1491 Log.v(TAG, "assertEnabled(): expected=" + expected + ", actual=" + actual); 1492 return actual == expected ? "not_used" : null; 1493 }); 1494 } 1495 1496 /** 1497 * Asserts these autofill ids are the same, except for the session. 1498 */ assertEqualsIgnoreSession(@onNull AutofillId id1, @NonNull AutofillId id2)1499 public static void assertEqualsIgnoreSession(@NonNull AutofillId id1, @NonNull AutofillId id2) { 1500 assertWithMessage("id1 is null").that(id1).isNotNull(); 1501 assertWithMessage("id2 is null").that(id2).isNotNull(); 1502 assertWithMessage("%s is not equal to %s", id1, id2).that(id1.equalsIgnoreSession(id2)) 1503 .isTrue(); 1504 } 1505 1506 /** 1507 * Asserts {@link View#isAutofilled()} state of the given view, waiting if necessarity to avoid 1508 * race conditions. 1509 */ assertViewAutofillState(@onNull View view, boolean expected)1510 public static void assertViewAutofillState(@NonNull View view, boolean expected) 1511 throws Exception { 1512 Timeouts.FILL_TIMEOUT.run("assertViewAutofillState(" + view + ", " + expected + ")", 1513 () -> { 1514 final boolean actual = view.isAutofilled(); 1515 Log.v(TAG, "assertViewAutofillState(): expected=" + expected + ", actual=" 1516 + actual); 1517 return actual == expected ? "not_used" : null; 1518 }); 1519 } 1520 1521 /** 1522 * Allows the test to draw overlaid windows. 1523 * 1524 * <p>Should call {@link #disallowOverlays()} afterwards. 1525 */ allowOverlays()1526 public static void allowOverlays() { 1527 ShellUtils.setOverlayPermissions(MY_PACKAGE, true); 1528 } 1529 1530 /** 1531 * Disallow the test to draw overlaid windows. 1532 * 1533 * <p>Should call {@link #disallowOverlays()} afterwards. 1534 */ disallowOverlays()1535 public static void disallowOverlays() { 1536 ShellUtils.setOverlayPermissions(MY_PACKAGE, false); 1537 } 1538 createPresentation(String message)1539 public static RemoteViews createPresentation(String message) { 1540 final RemoteViews presentation = new RemoteViews(getContext() 1541 .getPackageName(), R.layout.list_item); 1542 presentation.setTextViewText(R.id.text1, message); 1543 return presentation; 1544 } 1545 createInlinePresentation(String message)1546 public static InlinePresentation createInlinePresentation(String message) { 1547 final PendingIntent dummyIntent = PendingIntent.getActivity(getContext(), 0, new Intent(), 1548 PendingIntent.FLAG_IMMUTABLE); 1549 return createInlinePresentation(message, dummyIntent, false); 1550 } 1551 createInlinePresentation(String message, PendingIntent attribution)1552 public static InlinePresentation createInlinePresentation(String message, 1553 PendingIntent attribution) { 1554 return createInlinePresentation(message, attribution, false); 1555 } 1556 createPinnedInlinePresentation(String message)1557 public static InlinePresentation createPinnedInlinePresentation(String message) { 1558 final PendingIntent dummyIntent = PendingIntent.getActivity(getContext(), 0, new Intent(), 1559 PendingIntent.FLAG_IMMUTABLE); 1560 return createInlinePresentation(message, dummyIntent, true); 1561 } 1562 createInlinePresentation(@onNull String message, @NonNull PendingIntent attribution, boolean pinned)1563 private static InlinePresentation createInlinePresentation(@NonNull String message, 1564 @NonNull PendingIntent attribution, boolean pinned) { 1565 return new InlinePresentation( 1566 InlineSuggestionUi.newContentBuilder(attribution) 1567 .setTitle(message).build().getSlice(), 1568 new InlinePresentationSpec.Builder(new Size(100, 100), new Size(400, 100)) 1569 .build(), /* pinned= */ pinned); 1570 } 1571 createInlineTooltipPresentation( @onNull String message)1572 public static InlinePresentation createInlineTooltipPresentation( 1573 @NonNull String message) { 1574 final PendingIntent dummyIntent = PendingIntent.getActivity(getContext(), 0, new Intent(), 1575 PendingIntent.FLAG_IMMUTABLE); 1576 return createInlineTooltipPresentation(message, dummyIntent); 1577 } 1578 createInlineTooltipPresentation( @onNull String message, @NonNull PendingIntent attribution)1579 private static InlinePresentation createInlineTooltipPresentation( 1580 @NonNull String message, @NonNull PendingIntent attribution) { 1581 return InlinePresentation.createTooltipPresentation( 1582 InlineSuggestionUi.newContentBuilder(attribution) 1583 .setTitle(message).build().getSlice(), 1584 new InlinePresentationSpec.Builder(new Size(100, 100), new Size(400, 100)) 1585 .build()); 1586 } 1587 mockSwitchInputMethod(@onNull Context context)1588 public static void mockSwitchInputMethod(@NonNull Context context) throws Exception { 1589 final ContentResolver cr = context.getContentResolver(); 1590 final int subtype = Settings.Secure.getInt(cr, SELECTED_INPUT_METHOD_SUBTYPE); 1591 Settings.Secure.putInt(cr, SELECTED_INPUT_METHOD_SUBTYPE, subtype); 1592 } 1593 1594 /** 1595 * Reset AutofillOptions to avoid cts package was added to augmented autofill allowlist. 1596 */ resetApplicationAutofillOptions(@onNull Context context)1597 public static void resetApplicationAutofillOptions(@NonNull Context context) { 1598 AutofillOptions options = AutofillOptions.forWhitelistingItself(); 1599 options.augmentedAutofillEnabled = false; 1600 context.getApplicationContext().setAutofillOptions(options); 1601 } 1602 1603 /** 1604 * Clear AutofillOptions. 1605 */ clearApplicationAutofillOptions(@onNull Context context)1606 public static void clearApplicationAutofillOptions(@NonNull Context context) { 1607 context.getApplicationContext().setAutofillOptions(null); 1608 } 1609 Helper()1610 private Helper() { 1611 throw new UnsupportedOperationException("contain static methods only"); 1612 } 1613 1614 public static class FieldClassificationResult { 1615 public final AutofillId id; 1616 public final String[] categoryIds; 1617 public final float[] scores; 1618 FieldClassificationResult(@onNull AutofillId id, @NonNull String categoryId, float score)1619 public FieldClassificationResult(@NonNull AutofillId id, @NonNull String categoryId, 1620 float score) { 1621 this(id, new String[]{categoryId}, new float[]{score}); 1622 } 1623 FieldClassificationResult(@onNull AutofillId id, @NonNull String[] categoryIds, float[] scores)1624 public FieldClassificationResult(@NonNull AutofillId id, @NonNull String[] categoryIds, 1625 float[] scores) { 1626 this.id = id; 1627 this.categoryIds = categoryIds; 1628 this.scores = scores; 1629 } 1630 } 1631 } 1632