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