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