Home | History | Annotate | Download | only in cts
      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          */
    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 
    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
    182     public static String toString(@NonNull AssistStructure structure) {
    183         return toString(structure, new StringBuilder());
    184     }
    185 
    186     @Nullable
    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      */
    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      */
    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      */
    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      */
    227     public static void setUserComplete(Context context, boolean complete) {
    228         SettingsUtils.syncSet(context, USER_SETUP_COMPLETE, complete ? "1" : null);
    229     }
    230 
    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
    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
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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
    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
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    490     private static void assertNotFromResources(ViewNode node) {
    491         assertThat(node.getTextIdEntry()).isNull();
    492     }
    493 
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    806     public static void allowAutoRotation() {
    807         runShellCommand(ACCELLEROMETER_CHANGE, 1);
    808     }
    809 
    810     /**
    811      * Gets the maximum number of partitions per session.
    812      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    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      */
    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      */
    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      */
    895     public static void enableAutofillService(@NonNull Context context,
    896             @NonNull String serviceName) {
    897         if (isAutofillServiceEnabled(serviceName)) return;
    898 
    899         SettingsUtils.syncSet(context, AUTOFILL_SERVICE, serviceName);
    900     }
    901 
    902     /**
    903      * Uses Settings to disable the given autofill service for the default user, and waits until
    904      * the setting is deleted.
    905      */
    906     public static void disableAutofillService(@NonNull Context context) {
    907         final String currentService = SettingsUtils.get(AUTOFILL_SERVICE);
    908         if (currentService == null) {
    909             Log.v(TAG, "disableAutofillService(): already disabled");
    910             return;
    911         }
    912         Log.v(TAG, "Disabling " + currentService);
    913         SettingsUtils.syncDelete(context, AUTOFILL_SERVICE);
    914     }
    915 
    916     /**
    917      * Checks whether the given service is set as the autofill service for the default user.
    918      */
    919     public static boolean isAutofillServiceEnabled(@NonNull String serviceName) {
    920         final String actualName = getAutofillServiceName();
    921         return serviceName.equals(actualName);
    922     }
    923 
    924     /**
    925      * Gets then name of the autofill service for the default user.
    926      */
    927     public static String getAutofillServiceName() {
    928         return SettingsUtils.get(AUTOFILL_SERVICE);
    929     }
    930 
    931     /**
    932      * Asserts whether the given service is enabled as the autofill service for the default user.
    933      */
    934     public static void assertAutofillServiceStatus(@NonNull String serviceName, boolean enabled) {
    935         final String actual = SettingsUtils.get(AUTOFILL_SERVICE);
    936         final String expected = enabled ? serviceName : null;
    937         assertWithMessage("Invalid value for secure setting %s", AUTOFILL_SERVICE)
    938                 .that(actual).isEqualTo(expected);
    939     }
    940 
    941     /**
    942      * Enables / disables the default augmented autofill service.
    943      */
    944     public static void setDefaultAugmentedAutofillServiceEnabled(boolean enabled) {
    945         Log.d(TAG, "setDefaultAugmentedAutofillServiceEnabled(): " + enabled);
    946         runShellCommand("cmd autofill set default-augmented-service-enabled 0 %s",
    947                 Boolean.toString(enabled));
    948     }
    949 
    950     /**
    951      * Gets the instrumentation context.
    952      */
    953     public static Context getContext() {
    954         return InstrumentationRegistry.getInstrumentation().getContext();
    955     }
    956 
    957     /**
    958      * Asserts the node has an {@code HTMLInfo} property, with the given tag.
    959      */
    960     public static HtmlInfo assertHasHtmlTag(ViewNode node, String expectedTag) {
    961         final HtmlInfo info = node.getHtmlInfo();
    962         assertWithMessage("node doesn't have htmlInfo").that(info).isNotNull();
    963         assertWithMessage("wrong tag").that(info.getTag()).isEqualTo(expectedTag);
    964         return info;
    965     }
    966 
    967     /**
    968      * Gets the value of an {@code HTMLInfo} attribute.
    969      */
    970     @Nullable
    971     public static String getAttributeValue(HtmlInfo info, String attribute) {
    972         for (Pair<String, String> pair : info.getAttributes()) {
    973             if (pair.first.equals(attribute)) {
    974                 return pair.second;
    975             }
    976         }
    977         return null;
    978     }
    979 
    980     /**
    981      * Asserts a {@code HTMLInfo} has an attribute with a given value.
    982      */
    983     public static void assertHasAttribute(HtmlInfo info, String attribute, String expectedValue) {
    984         final String actualValue = getAttributeValue(info, attribute);
    985         assertWithMessage("Attribute %s not found", attribute).that(actualValue).isNotNull();
    986         assertWithMessage("Wrong value for Attribute %s", attribute)
    987             .that(actualValue).isEqualTo(expectedValue);
    988     }
    989 
    990     /**
    991      * Finds a {@link WebView} node given its expected form name.
    992      */
    993     public static ViewNode findWebViewNodeByFormName(AssistStructure structure, String formName) {
    994         return findNodeByFilter(structure, formName, WEBVIEW_FORM_FILTER);
    995     }
    996 
    997     private static void assertClientState(Object container, Bundle clientState,
    998             String key, String value) {
    999         assertWithMessage("'%s' should have client state", container)
   1000             .that(clientState).isNotNull();
   1001         assertWithMessage("Wrong number of client state extras on '%s'", container)
   1002             .that(clientState.keySet().size()).isEqualTo(1);
   1003         assertWithMessage("Wrong value for client state key (%s) on '%s'", key, container)
   1004             .that(clientState.getString(key)).isEqualTo(value);
   1005     }
   1006 
   1007     /**
   1008      * Asserts the content of a {@link FillEventHistory#getClientState()}.
   1009      *
   1010      * @param history event to be asserted
   1011      * @param key the only key expected in the client state bundle
   1012      * @param value the only value expected in the client state bundle
   1013      */
   1014     @SuppressWarnings("javadoc")
   1015     public static void assertDeprecatedClientState(@NonNull FillEventHistory history,
   1016             @NonNull String key, @NonNull String value) {
   1017         assertThat(history).isNotNull();
   1018         @SuppressWarnings("deprecation")
   1019         final Bundle clientState = history.getClientState();
   1020         assertClientState(history, clientState, key, value);
   1021     }
   1022 
   1023     /**
   1024      * Asserts the {@link FillEventHistory#getClientState()} is not set.
   1025      *
   1026      * @param history event to be asserted
   1027      */
   1028     @SuppressWarnings("javadoc")
   1029     public static void assertNoDeprecatedClientState(@NonNull FillEventHistory history) {
   1030         assertThat(history).isNotNull();
   1031         @SuppressWarnings("deprecation")
   1032         final Bundle clientState = history.getClientState();
   1033         assertWithMessage("History '%s' should not have client state", history)
   1034              .that(clientState).isNull();
   1035     }
   1036 
   1037     /**
   1038      * Asserts the content of a {@link android.service.autofill.FillEventHistory.Event}.
   1039      *
   1040      * @param event event to be asserted
   1041      * @param eventType expected type
   1042      * @param datasetId dataset set id expected in the event
   1043      * @param key the only key expected in the client state bundle (or {@code null} if it shouldn't
   1044      * have client state)
   1045      * @param value the only value expected in the client state bundle (or {@code null} if it
   1046      * shouldn't have client state)
   1047      * @param fieldClassificationResults expected results when asserting field classification
   1048      */
   1049     private static void assertFillEvent(@NonNull FillEventHistory.Event event,
   1050             int eventType, @Nullable String datasetId,
   1051             @Nullable String key, @Nullable String value,
   1052             @Nullable FieldClassificationResult[] fieldClassificationResults) {
   1053         assertThat(event).isNotNull();
   1054         assertWithMessage("Wrong type for %s", event).that(event.getType()).isEqualTo(eventType);
   1055         if (datasetId == null) {
   1056             assertWithMessage("Event %s should not have dataset id", event)
   1057                 .that(event.getDatasetId()).isNull();
   1058         } else {
   1059             assertWithMessage("Wrong dataset id for %s", event)
   1060                 .that(event.getDatasetId()).isEqualTo(datasetId);
   1061         }
   1062         final Bundle clientState = event.getClientState();
   1063         if (key == null) {
   1064             assertWithMessage("Event '%s' should not have client state", event)
   1065                 .that(clientState).isNull();
   1066         } else {
   1067             assertClientState(event, clientState, key, value);
   1068         }
   1069         assertWithMessage("Event '%s' should not have selected datasets", event)
   1070                 .that(event.getSelectedDatasetIds()).isEmpty();
   1071         assertWithMessage("Event '%s' should not have ignored datasets", event)
   1072                 .that(event.getIgnoredDatasetIds()).isEmpty();
   1073         assertWithMessage("Event '%s' should not have changed fields", event)
   1074                 .that(event.getChangedFields()).isEmpty();
   1075         assertWithMessage("Event '%s' should not have manually-entered fields", event)
   1076                 .that(event.getManuallyEnteredField()).isEmpty();
   1077         final Map<AutofillId, FieldClassification> detectedFields = event.getFieldsClassification();
   1078         if (fieldClassificationResults == null) {
   1079             assertThat(detectedFields).isEmpty();
   1080         } else {
   1081             assertThat(detectedFields).hasSize(fieldClassificationResults.length);
   1082             int i = 0;
   1083             for (Entry<AutofillId, FieldClassification> entry : detectedFields.entrySet()) {
   1084                 assertMatches(i, entry, fieldClassificationResults[i]);
   1085                 i++;
   1086             }
   1087         }
   1088     }
   1089 
   1090     private static void assertMatches(int i, Entry<AutofillId, FieldClassification> actualResult,
   1091             FieldClassificationResult expectedResult) {
   1092         assertWithMessage("Wrong field id at index %s", i).that(actualResult.getKey())
   1093                 .isEqualTo(expectedResult.id);
   1094         final List<Match> matches = actualResult.getValue().getMatches();
   1095         assertWithMessage("Wrong number of matches: " + matches).that(matches.size())
   1096                 .isEqualTo(expectedResult.categoryIds.length);
   1097         for (int j = 0; j < matches.size(); j++) {
   1098             final Match match = matches.get(j);
   1099             assertWithMessage("Wrong categoryId at (%s, %s): %s", i, j, match)
   1100                 .that(match.getCategoryId()).isEqualTo(expectedResult.categoryIds[j]);
   1101             assertWithMessage("Wrong score at (%s, %s): %s", i, j, match)
   1102                 .that(match.getScore()).isWithin(0.01f).of(expectedResult.scores[j]);
   1103         }
   1104     }
   1105 
   1106     /**
   1107      * Asserts the content of a
   1108      * {@link android.service.autofill.FillEventHistory.Event#TYPE_DATASET_SELECTED} event.
   1109      *
   1110      * @param event event to be asserted
   1111      * @param datasetId dataset set id expected in the event
   1112      */
   1113     public static void assertFillEventForDatasetSelected(@NonNull FillEventHistory.Event event,
   1114             @Nullable String datasetId) {
   1115         assertFillEvent(event, TYPE_DATASET_SELECTED, datasetId, null, null, null);
   1116     }
   1117 
   1118     /**
   1119      * Asserts the content of a
   1120      * {@link android.service.autofill.FillEventHistory.Event#TYPE_DATASET_SELECTED} event.
   1121      *
   1122      * @param event event to be asserted
   1123      * @param datasetId dataset set id expected in the event
   1124      * @param key the only key expected in the client state bundle
   1125      * @param value the only value expected in the client state bundle
   1126      */
   1127     public static void assertFillEventForDatasetSelected(@NonNull FillEventHistory.Event event,
   1128             @Nullable String datasetId, @Nullable String key, @Nullable String value) {
   1129         assertFillEvent(event, TYPE_DATASET_SELECTED, datasetId, key, value, null);
   1130     }
   1131 
   1132     /**
   1133      * Asserts the content of a
   1134      * {@link android.service.autofill.FillEventHistory.Event#TYPE_SAVE_SHOWN} event.
   1135      *
   1136      * @param event event to be asserted
   1137      * @param datasetId dataset set id expected in the event
   1138      * @param key the only key expected in the client state bundle
   1139      * @param value the only value expected in the client state bundle
   1140      */
   1141     public static void assertFillEventForSaveShown(@NonNull FillEventHistory.Event event,
   1142             @NonNull String datasetId, @NonNull String key, @NonNull String value) {
   1143         assertFillEvent(event, TYPE_SAVE_SHOWN, datasetId, key, value, null);
   1144     }
   1145 
   1146     /**
   1147      * Asserts the content of a
   1148      * {@link android.service.autofill.FillEventHistory.Event#TYPE_SAVE_SHOWN} event.
   1149      *
   1150      * @param event event to be asserted
   1151      * @param datasetId dataset set id expected in the event
   1152      */
   1153     public static void assertFillEventForSaveShown(@NonNull FillEventHistory.Event event,
   1154             @NonNull String datasetId) {
   1155         assertFillEvent(event, TYPE_SAVE_SHOWN, datasetId, null, null, null);
   1156     }
   1157 
   1158     /**
   1159      * Asserts the content of a
   1160      * {@link android.service.autofill.FillEventHistory.Event#TYPE_DATASET_AUTHENTICATION_SELECTED}
   1161      * event.
   1162      *
   1163      * @param event event to be asserted
   1164      * @param datasetId dataset set id expected in the event
   1165      * @param key the only key expected in the client state bundle
   1166      * @param value the only value expected in the client state bundle
   1167      */
   1168     public static void assertFillEventForDatasetAuthenticationSelected(
   1169             @NonNull FillEventHistory.Event event,
   1170             @Nullable String datasetId, @NonNull String key, @NonNull String value) {
   1171         assertFillEvent(event, TYPE_DATASET_AUTHENTICATION_SELECTED, datasetId, key, value, null);
   1172     }
   1173 
   1174     /**
   1175      * Asserts the content of a
   1176      * {@link android.service.autofill.FillEventHistory.Event#TYPE_AUTHENTICATION_SELECTED} event.
   1177      *
   1178      * @param event event to be asserted
   1179      * @param datasetId dataset set id expected in the event
   1180      * @param key the only key expected in the client state bundle
   1181      * @param value the only value expected in the client state bundle
   1182      */
   1183     public static void assertFillEventForAuthenticationSelected(
   1184             @NonNull FillEventHistory.Event event,
   1185             @Nullable String datasetId, @NonNull String key, @NonNull String value) {
   1186         assertFillEvent(event, TYPE_AUTHENTICATION_SELECTED, datasetId, key, value, null);
   1187     }
   1188 
   1189     public static void assertFillEventForFieldsClassification(@NonNull FillEventHistory.Event event,
   1190             @NonNull AutofillId fieldId, @NonNull String categoryId, float score) {
   1191         assertFillEvent(event, TYPE_CONTEXT_COMMITTED, null, null, null,
   1192                 new FieldClassificationResult[] {
   1193                         new FieldClassificationResult(fieldId, categoryId, score)
   1194                 });
   1195     }
   1196 
   1197     public static void assertFillEventForFieldsClassification(@NonNull FillEventHistory.Event event,
   1198             @NonNull FieldClassificationResult[] results) {
   1199         assertFillEvent(event, TYPE_CONTEXT_COMMITTED, null, null, null, results);
   1200     }
   1201 
   1202     public static void assertFillEventForContextCommitted(@NonNull FillEventHistory.Event event) {
   1203         assertFillEvent(event, TYPE_CONTEXT_COMMITTED, null, null, null, null);
   1204     }
   1205 
   1206     @NonNull
   1207     public static String getActivityName(List<FillContext> contexts) {
   1208         if (contexts == null) return "N/A (null contexts)";
   1209 
   1210         if (contexts.isEmpty()) return "N/A (empty contexts)";
   1211 
   1212         final AssistStructure structure = contexts.get(contexts.size() - 1).getStructure();
   1213         if (structure == null) return "N/A (no AssistStructure)";
   1214 
   1215         final ComponentName componentName = structure.getActivityComponent();
   1216         if (componentName == null) return "N/A (no component name)";
   1217 
   1218         return componentName.flattenToShortString();
   1219     }
   1220 
   1221     public static void assertFloat(float actualValue, float expectedValue) {
   1222         assertThat(actualValue).isWithin(1.0e-10f).of(expectedValue);
   1223     }
   1224 
   1225     public static void assertHasFlags(int actualFlags, int expectedFlags) {
   1226         assertWithMessage("Flags %s not in %s", expectedFlags, actualFlags)
   1227                 .that(actualFlags & expectedFlags).isEqualTo(expectedFlags);
   1228     }
   1229 
   1230     public static String callbackEventAsString(int event) {
   1231         switch (event) {
   1232             case AutofillCallback.EVENT_INPUT_HIDDEN:
   1233                 return "HIDDEN";
   1234             case AutofillCallback.EVENT_INPUT_SHOWN:
   1235                 return "SHOWN";
   1236             case AutofillCallback.EVENT_INPUT_UNAVAILABLE:
   1237                 return "UNAVAILABLE";
   1238             default:
   1239                 return "UNKNOWN:" + event;
   1240         }
   1241     }
   1242 
   1243     public static String importantForAutofillAsString(int mode) {
   1244         switch (mode) {
   1245             case View.IMPORTANT_FOR_AUTOFILL_AUTO:
   1246                 return "IMPORTANT_FOR_AUTOFILL_AUTO";
   1247             case View.IMPORTANT_FOR_AUTOFILL_YES:
   1248                 return "IMPORTANT_FOR_AUTOFILL_YES";
   1249             case View.IMPORTANT_FOR_AUTOFILL_YES_EXCLUDE_DESCENDANTS:
   1250                 return "IMPORTANT_FOR_AUTOFILL_YES_EXCLUDE_DESCENDANTS";
   1251             case View.IMPORTANT_FOR_AUTOFILL_NO:
   1252                 return "IMPORTANT_FOR_AUTOFILL_NO";
   1253             case View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS:
   1254                 return "IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS";
   1255             default:
   1256                 return "UNKNOWN:" + mode;
   1257         }
   1258     }
   1259 
   1260     public static boolean hasHint(@Nullable String[] hints, @Nullable Object expectedHint) {
   1261         if (hints == null || expectedHint == null) return false;
   1262         for (String actualHint : hints) {
   1263             if (expectedHint.equals(actualHint)) return true;
   1264         }
   1265         return false;
   1266     }
   1267 
   1268     /**
   1269      * Asserts that 2 bitmaps have are the same. If they aren't throws an exception and dump them
   1270      * locally so their can be visually inspected.
   1271      *
   1272      * @param filename base name of the files generated in case of error
   1273      * @param bitmap1 first bitmap to be compared
   1274      * @param bitmap2 second bitmap to be compared
   1275      */
   1276     // TODO: move to common code
   1277     public static void assertBitmapsAreSame(@NonNull String filename, @Nullable Bitmap bitmap1,
   1278             @Nullable Bitmap bitmap2) throws IOException {
   1279         assertWithMessage("1st bitmap is null").that(bitmap1).isNotNull();
   1280         assertWithMessage("2nd bitmap is null").that(bitmap2).isNotNull();
   1281         final boolean same = bitmap1.sameAs(bitmap2);
   1282         if (same) {
   1283             Log.v(TAG, "bitmap comparison passed for " + filename);
   1284             return;
   1285         }
   1286 
   1287         final File dir = getLocalDirectory();
   1288         if (dir == null) {
   1289             throw new AssertionError("bitmap comparison failed for " + filename
   1290                     + ", and bitmaps could not be dumped on " + dir);
   1291         }
   1292         final File dump1 = dumpBitmap(bitmap1, dir, filename + "-1.png");
   1293         final File dump2 = dumpBitmap(bitmap2, dir, filename + "-2.png");
   1294         throw new AssertionError(
   1295                 "bitmap comparison failed; check contents of " + dump1 + " and " + dump2);
   1296     }
   1297 
   1298     @Nullable
   1299     private static File getLocalDirectory() {
   1300         final File dir = new File(LOCAL_DIRECTORY);
   1301         dir.mkdirs();
   1302         if (!dir.exists()) {
   1303             Log.e(TAG, "Could not create directory " + dir);
   1304             return null;
   1305         }
   1306         return dir;
   1307     }
   1308 
   1309     @Nullable
   1310     private static File createFile(@NonNull File dir, @NonNull String filename) throws IOException {
   1311         final File file = new File(dir, filename);
   1312         if (file.exists()) {
   1313             Log.v(TAG, "Deleting file " + file);
   1314             file.delete();
   1315         }
   1316         if (!file.createNewFile()) {
   1317             Log.e(TAG, "Could not create file " + file);
   1318             return null;
   1319         }
   1320         return file;
   1321     }
   1322 
   1323     @Nullable
   1324     private static File dumpBitmap(@NonNull Bitmap bitmap, @NonNull File dir,
   1325             @NonNull String filename) throws IOException {
   1326         final File file = createFile(dir, filename);
   1327         if (file != null) {
   1328             dumpBitmap(bitmap, file);
   1329 
   1330         }
   1331         return file;
   1332     }
   1333 
   1334     @Nullable
   1335     public static File dumpBitmap(@NonNull Bitmap bitmap, @NonNull File file) {
   1336         Log.i(TAG, "Dumping bitmap at " + file);
   1337         BitmapUtils.saveBitmap(bitmap, file.getParent(), file.getName());
   1338         return file;
   1339     }
   1340 
   1341     /**
   1342      * Creates a file in the device, using the name of the current test as a prefix.
   1343      */
   1344     @Nullable
   1345     public static File createTestFile(@NonNull String name) throws IOException {
   1346         final File dir = getLocalDirectory();
   1347         if (dir == null) return null;
   1348 
   1349         final String prefix = TestNameUtils.getCurrentTestName().replaceAll("\\.|\\(|\\/", "_")
   1350                 .replaceAll("\\)", "");
   1351         final String filename = prefix + "-" + name;
   1352 
   1353         return createFile(dir, filename);
   1354     }
   1355 
   1356     /**
   1357      * Offers an object to a queue or times out.
   1358      *
   1359      * @return {@code true} if the offer was accepted, {$code false} if it timed out or was
   1360      * interrupted.
   1361      */
   1362     public static <T> boolean offer(BlockingQueue<T> queue, T obj, long timeoutMs) {
   1363         boolean offered = false;
   1364         try {
   1365             offered = queue.offer(obj, timeoutMs, TimeUnit.MILLISECONDS);
   1366         } catch (InterruptedException e) {
   1367             Log.w(TAG, "interrupted offering", e);
   1368             Thread.currentThread().interrupt();
   1369         }
   1370         if (!offered) {
   1371             Log.e(TAG, "could not offer " + obj + " in " + timeoutMs + "ms");
   1372         }
   1373         return offered;
   1374     }
   1375 
   1376     /**
   1377      * Calls this method to assert given {@code string} is equal to {@link #LARGE_STRING}, as
   1378      * comparing its value using standard assertions might ANR.
   1379      */
   1380     public static void assertEqualsToLargeString(@NonNull String string) {
   1381         assertThat(string).isNotNull();
   1382         assertThat(string).hasLength(LARGE_STRING_SIZE);
   1383         assertThat(string.charAt(0)).isEqualTo(LARGE_STRING_CHAR);
   1384         assertThat(string.charAt(LARGE_STRING_SIZE - 1)).isEqualTo(LARGE_STRING_CHAR);
   1385     }
   1386 
   1387     /**
   1388      * Asserts that autofill is enabled in the context, retrying if necessariy.
   1389      */
   1390     public static void assertAutofillEnabled(@NonNull Context context, boolean expected)
   1391             throws Exception {
   1392         assertAutofillEnabled(context.getSystemService(AutofillManager.class), expected);
   1393     }
   1394 
   1395     /**
   1396      * Asserts that autofill is enabled in the manager, retrying if necessariy.
   1397      */
   1398     public static void assertAutofillEnabled(@NonNull AutofillManager afm, boolean expected)
   1399             throws Exception {
   1400         Timeouts.IDLE_UNBIND_TIMEOUT.run("assertEnabled(" + expected + ")", () -> {
   1401             final boolean actual = afm.isEnabled();
   1402             Log.v(TAG, "assertEnabled(): expected=" + expected + ", actual=" + actual);
   1403             return actual == expected ? "not_used" : null;
   1404         });
   1405     }
   1406 
   1407     /**
   1408      * Asserts these autofill ids are the same, except for the session.
   1409      */
   1410     public static void assertEqualsIgnoreSession(@NonNull AutofillId id1, @NonNull AutofillId id2) {
   1411         assertWithMessage("id1 is null").that(id1).isNotNull();
   1412         assertWithMessage("id2 is null").that(id2).isNotNull();
   1413         assertWithMessage("%s is not equal to %s", id1, id2).that(id1.equalsIgnoreSession(id2))
   1414                 .isTrue();
   1415     }
   1416 
   1417     /**
   1418      * Asserts {@link View#isAutofilled()} state of the given view, waiting if necessarity to avoid
   1419      * race conditions.
   1420      */
   1421     public static void assertViewAutofillState(@NonNull View view, boolean expected)
   1422             throws Exception {
   1423         Timeouts.FILL_TIMEOUT.run("assertViewAutofillState(" + view + ", " + expected + ")",
   1424                 () -> {
   1425                     final boolean actual = view.isAutofilled();
   1426                     Log.v(TAG, "assertViewAutofillState(): expected=" + expected + ", actual="
   1427                             + actual);
   1428                     return actual == expected ? "not_used" : null;
   1429                 });
   1430     }
   1431 
   1432     /**
   1433      * Allows the test to draw overlaid windows.
   1434      *
   1435      * <p>Should call {@link #disallowOverlays()} afterwards.
   1436      */
   1437     public static void allowOverlays() {
   1438         ShellUtils.setOverlayPermissions(MY_PACKAGE, true);
   1439     }
   1440 
   1441     /**
   1442      * Disallow the test to draw overlaid windows.
   1443      *
   1444      * <p>Should call {@link #disallowOverlays()} afterwards.
   1445      */
   1446     public static void disallowOverlays() {
   1447         ShellUtils.setOverlayPermissions(MY_PACKAGE, false);
   1448     }
   1449 
   1450     private Helper() {
   1451         throw new UnsupportedOperationException("contain static methods only");
   1452     }
   1453 
   1454     static class FieldClassificationResult {
   1455         public final AutofillId id;
   1456         public final String[] categoryIds;
   1457         public final float[] scores;
   1458 
   1459         FieldClassificationResult(@NonNull AutofillId id, @NonNull String categoryId, float score) {
   1460             this(id, new String[] { categoryId }, new float[] { score });
   1461         }
   1462 
   1463         FieldClassificationResult(@NonNull AutofillId id, @NonNull String[] categoryIds,
   1464                 float[] scores) {
   1465             this.id = id;
   1466             this.categoryIds = categoryIds;
   1467             this.scores = scores;
   1468         }
   1469     }
   1470 }
   1471