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.InstrumentedAutoFillService.SERVICE_NAME;
     20 import static android.autofillservice.cts.UiBot.PORTRAIT;
     21 import static android.autofillservice.cts.common.ShellHelper.runShellCommand;
     22 import static android.provider.Settings.Secure.AUTOFILL_SERVICE;
     23 import static android.provider.Settings.Secure.USER_SETUP_COMPLETE;
     24 import static android.service.autofill.FillEventHistory.Event.TYPE_AUTHENTICATION_SELECTED;
     25 import static android.service.autofill.FillEventHistory.Event.TYPE_CONTEXT_COMMITTED;
     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.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.autofillservice.cts.common.SettingsHelper;
     38 import android.content.ComponentName;
     39 import android.content.Context;
     40 import android.content.pm.PackageManager;
     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.support.test.InstrumentationRegistry;
     50 import android.text.TextUtils;
     51 import android.util.Log;
     52 import android.util.Pair;
     53 import android.view.View;
     54 import android.view.ViewGroup;
     55 import android.view.ViewStructure.HtmlInfo;
     56 import android.view.autofill.AutofillId;
     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 
     64 import com.android.compatibility.common.util.BitmapUtils;
     65 import com.android.compatibility.common.util.RequiredFeatureRule;
     66 
     67 import java.io.File;
     68 import java.io.IOException;
     69 import java.util.List;
     70 import java.util.Map;
     71 import java.util.Map.Entry;
     72 import java.util.function.Function;
     73 
     74 /**
     75  * Helper for common funcionalities.
     76  */
     77 final class Helper {
     78 
     79     static final String TAG = "AutoFillCtsHelper";
     80 
     81     static final boolean VERBOSE = false;
     82 
     83     static final String MY_PACKAGE = "android.autofillservice.cts";
     84 
     85     static final String ID_USERNAME_LABEL = "username_label";
     86     static final String ID_USERNAME = "username";
     87     static final String ID_PASSWORD_LABEL = "password_label";
     88     static final String ID_PASSWORD = "password";
     89     static final String ID_LOGIN = "login";
     90     static final String ID_OUTPUT = "output";
     91     static final String ID_STATIC_TEXT = "static_text";
     92 
     93     public static final String NULL_DATASET_ID = null;
     94 
     95     /**
     96      * Can be used in cases where the autofill values is required by irrelevant (like adding a
     97      * value to an authenticated dataset).
     98      */
     99     public static final String UNUSED_AUTOFILL_VALUE = null;
    100 
    101     private static final String CMD_LIST_SESSIONS = "cmd autofill list sessions";
    102 
    103     private static final String ACCELLEROMETER_CHANGE =
    104             "content insert --uri content://settings/system --bind name:s:accelerometer_rotation "
    105                     + "--bind value:i:%d";
    106 
    107     private static final String LOCAL_DIRECTORY = Environment.getExternalStorageDirectory()
    108             + "/CtsAutoFillServiceTestCases";
    109 
    110     /**
    111      * Helper interface used to filter nodes.
    112      *
    113      * @param <T> node type
    114      */
    115     interface NodeFilter<T> {
    116         /**
    117          * Returns whether the node passes the filter for such given id.
    118          */
    119         boolean matches(T node, Object id);
    120     }
    121 
    122     private static final NodeFilter<ViewNode> RESOURCE_ID_FILTER = (node, id) -> {
    123         return id.equals(node.getIdEntry());
    124     };
    125 
    126     private static final NodeFilter<ViewNode> AUTOFILL_ID_FILTER = (node, id) -> {
    127         return id.equals(node.getAutofillId());
    128     };
    129 
    130     private static final NodeFilter<ViewNode> HTML_NAME_FILTER = (node, id) -> {
    131         return id.equals(getHtmlName(node));
    132     };
    133 
    134     private static final NodeFilter<ViewNode> HTML_NAME_OR_RESOURCE_ID_FILTER = (node, id) -> {
    135         return id.equals(getHtmlName(node)) || id.equals(node.getIdEntry());
    136     };
    137 
    138     private static final NodeFilter<ViewNode> TEXT_FILTER = (node, id) -> {
    139         return id.equals(node.getText());
    140     };
    141 
    142     private static final NodeFilter<ViewNode> AUTOFILL_HINT_FILTER = (node, id) -> {
    143         return hasHint(node.getAutofillHints(), id);
    144     };
    145 
    146     private static final NodeFilter<ViewNode> WEBVIEW_FORM_FILTER = (node, id) -> {
    147         final String className = node.getClassName();
    148         if (!className.equals("android.webkit.WebView")) return false;
    149 
    150         final HtmlInfo htmlInfo = assertHasHtmlTag(node, "form");
    151         final String formName = getAttributeValue(htmlInfo, "name");
    152         return id.equals(formName);
    153     };
    154 
    155     private static final NodeFilter<View> AUTOFILL_HINT_VIEW_FILTER = (view, id) -> {
    156         return hasHint(view.getAutofillHints(), id);
    157     };
    158 
    159     /**
    160      * Dump the assist structure on logcat.
    161      */
    162     static void dumpStructure(String message, AssistStructure structure) {
    163         final StringBuffer buffer = new StringBuffer(message)
    164                 .append(": component=")
    165                 .append(structure.getActivityComponent());
    166         final int nodes = structure.getWindowNodeCount();
    167         for (int i = 0; i < nodes; i++) {
    168             final WindowNode windowNode = structure.getWindowNodeAt(i);
    169             dump(buffer, windowNode.getRootViewNode(), " ", 0);
    170         }
    171         Log.i(TAG, buffer.toString());
    172     }
    173 
    174     /**
    175      * Dump the contexts on logcat.
    176      */
    177     static void dumpStructure(String message, List<FillContext> contexts) {
    178         for (FillContext context : contexts) {
    179             dumpStructure(message, context.getStructure());
    180         }
    181     }
    182 
    183     /**
    184      * Dumps the state of the autofill service on logcat.
    185      */
    186     static void dumpAutofillService() {
    187         Log.i(TAG, "dumpsys autofill\n\n" + runShellCommand("dumpsys autofill"));
    188     }
    189 
    190     /**
    191      * Sets whether the user completed the initial setup.
    192      */
    193     static void setUserComplete(Context context, boolean complete) {
    194         SettingsHelper.syncSet(context, USER_SETUP_COMPLETE, complete ? "1" : null);
    195     }
    196 
    197     private static void dump(StringBuffer buffer, ViewNode node, String prefix, int childId) {
    198         final int childrenSize = node.getChildCount();
    199         buffer.append("\n").append(prefix)
    200             .append('#').append(childId).append(':')
    201             .append("resId=").append(node.getIdEntry())
    202             .append(" class=").append(node.getClassName())
    203             .append(" text=").append(node.getText())
    204             .append(" class=").append(node.getClassName())
    205             .append(" webDomain=").append(node.getWebDomain())
    206             .append(" #children=").append(childrenSize);
    207 
    208         buffer.append("\n").append(prefix)
    209             .append("   afId=").append(node.getAutofillId())
    210             .append(" afType=").append(node.getAutofillType())
    211             .append(" afValue=").append(node.getAutofillValue())
    212             .append(" checked=").append(node.isChecked())
    213             .append(" focused=").append(node.isFocused());
    214 
    215         final HtmlInfo htmlInfo = node.getHtmlInfo();
    216         if (htmlInfo != null) {
    217             buffer.append("\nHtmlInfo: tag=").append(htmlInfo.getTag())
    218                 .append(", attrs: ").append(htmlInfo.getAttributes());
    219         }
    220 
    221         prefix += " ";
    222         if (childrenSize > 0) {
    223             for (int i = 0; i < childrenSize; i++) {
    224                 dump(buffer, node.getChildAt(i), prefix, i);
    225             }
    226         }
    227     }
    228 
    229     /**
    230      * Gets a node if it matches the filter criteria for the given id.
    231      */
    232     static ViewNode findNodeByFilter(@NonNull AssistStructure structure, @NonNull Object id,
    233             @NonNull NodeFilter<ViewNode> filter) {
    234         Log.v(TAG, "Parsing request for activity " + structure.getActivityComponent());
    235         final int nodes = structure.getWindowNodeCount();
    236         for (int i = 0; i < nodes; i++) {
    237             final WindowNode windowNode = structure.getWindowNodeAt(i);
    238             final ViewNode rootNode = windowNode.getRootViewNode();
    239             final ViewNode node = findNodeByFilter(rootNode, id, filter);
    240             if (node != null) {
    241                 return node;
    242             }
    243         }
    244         return null;
    245     }
    246 
    247     /**
    248      * Gets a node if it matches the filter criteria for the given id.
    249      */
    250     static ViewNode findNodeByFilter(@NonNull List<FillContext> contexts, @NonNull Object id,
    251             @NonNull NodeFilter<ViewNode> filter) {
    252         for (FillContext context : contexts) {
    253             ViewNode node = findNodeByFilter(context.getStructure(), id, filter);
    254             if (node != null) {
    255                 return node;
    256             }
    257         }
    258         return null;
    259     }
    260 
    261     /**
    262      * Gets a node if it matches the filter criteria for the given id.
    263      */
    264     static ViewNode findNodeByFilter(@NonNull ViewNode node, @NonNull Object id,
    265             @NonNull NodeFilter<ViewNode> filter) {
    266         if (filter.matches(node, id)) {
    267             return node;
    268         }
    269         final int childrenSize = node.getChildCount();
    270         if (childrenSize > 0) {
    271             for (int i = 0; i < childrenSize; i++) {
    272                 final ViewNode found = findNodeByFilter(node.getChildAt(i), id, filter);
    273                 if (found != null) {
    274                     return found;
    275                 }
    276             }
    277         }
    278         return null;
    279     }
    280 
    281     /**
    282      * Gets a node given its Android resource id, or {@code null} if not found.
    283      */
    284     static ViewNode findNodeByResourceId(AssistStructure structure, String resourceId) {
    285         return findNodeByFilter(structure, resourceId, RESOURCE_ID_FILTER);
    286     }
    287 
    288     /**
    289      * Gets a node given its Android resource id, or {@code null} if not found.
    290      */
    291     static ViewNode findNodeByResourceId(List<FillContext> contexts, String resourceId) {
    292         return findNodeByFilter(contexts, resourceId, RESOURCE_ID_FILTER);
    293     }
    294 
    295     /**
    296      * Gets a node given its Android resource id, or {@code null} if not found.
    297      */
    298     static ViewNode findNodeByResourceId(ViewNode node, String resourceId) {
    299         return findNodeByFilter(node, resourceId, RESOURCE_ID_FILTER);
    300     }
    301 
    302     /**
    303      * Gets a node given the name of its HTML INPUT tag, or {@code null} if not found.
    304      */
    305     static ViewNode findNodeByHtmlName(AssistStructure structure, String htmlName) {
    306         return findNodeByFilter(structure, htmlName, HTML_NAME_FILTER);
    307     }
    308 
    309     /**
    310      * Gets a node given the name of its HTML INPUT tag, or {@code null} if not found.
    311      */
    312     static ViewNode findNodeByHtmlName(List<FillContext> contexts, String htmlName) {
    313         return findNodeByFilter(contexts, htmlName, HTML_NAME_FILTER);
    314     }
    315 
    316     /**
    317      * Gets a node given the name of its HTML INPUT tag, or {@code null} if not found.
    318      */
    319     static ViewNode findNodeByHtmlName(ViewNode node, String htmlName) {
    320         return findNodeByFilter(node, htmlName, HTML_NAME_FILTER);
    321     }
    322 
    323     /**
    324      * Gets a node given the value of its (single) autofill hint property, or {@code null} if not
    325      * found.
    326      */
    327     static ViewNode findNodeByAutofillHint(ViewNode node, String hint) {
    328         return findNodeByFilter(node, hint, AUTOFILL_HINT_FILTER);
    329     }
    330 
    331     /**
    332      * Gets a node given the name of its HTML INPUT tag or Android resoirce id, or {@code null} if
    333      * not found.
    334      */
    335     static ViewNode findNodeByHtmlNameOrResourceId(List<FillContext> contexts, String id) {
    336         return findNodeByFilter(contexts, id, HTML_NAME_OR_RESOURCE_ID_FILTER);
    337     }
    338 
    339     /**
    340      * Gets the {@code name} attribute of a node representing an HTML input tag.
    341      */
    342     @Nullable
    343     static String getHtmlName(@NonNull ViewNode node) {
    344         final HtmlInfo htmlInfo = node.getHtmlInfo();
    345         if (htmlInfo == null) {
    346             return null;
    347         }
    348         final String tag = htmlInfo.getTag();
    349         if (!"input".equals(tag)) {
    350             Log.w(TAG, "getHtmlName(): invalid tag (" + tag + ") on " + htmlInfo);
    351             return null;
    352         }
    353         for (Pair<String, String> attr : htmlInfo.getAttributes()) {
    354             if ("name".equals(attr.first)) {
    355                 return attr.second;
    356             }
    357         }
    358         Log.w(TAG, "getHtmlName(): no 'name' attribute on " + htmlInfo);
    359         return null;
    360     }
    361 
    362     /**
    363      * Gets a node given its expected text, or {@code null} if not found.
    364      */
    365     static ViewNode findNodeByText(AssistStructure structure, String text) {
    366         return findNodeByFilter(structure, text, TEXT_FILTER);
    367     }
    368 
    369     /**
    370      * Gets a node given its expected text, or {@code null} if not found.
    371      */
    372     static ViewNode findNodeByText(ViewNode node, String text) {
    373         return findNodeByFilter(node, text, TEXT_FILTER);
    374     }
    375 
    376     /**
    377      * Gets a view that contains the an autofill hint, or {@code null} if not found.
    378      */
    379     static View findViewByAutofillHint(Activity activity, String hint) {
    380         final View rootView = activity.getWindow().getDecorView().getRootView();
    381         return findViewByAutofillHint(rootView, hint);
    382     }
    383 
    384     /**
    385      * Gets a view (or a descendant of it) that contains the an autofill hint, or {@code null} if
    386      * not found.
    387      */
    388     static View findViewByAutofillHint(View view, String hint) {
    389         if (AUTOFILL_HINT_VIEW_FILTER.matches(view, hint)) return view;
    390         if ((view instanceof ViewGroup)) {
    391             final ViewGroup group = (ViewGroup) view;
    392             for (int i = 0; i < group.getChildCount(); i++) {
    393                 final View child = findViewByAutofillHint(group.getChildAt(i), hint);
    394                 if (child != null) return child;
    395             }
    396         }
    397         return null;
    398     }
    399 
    400     /**
    401      * Gets a view (or a descendant of it) that has the given {@code id}, or {@code null} if
    402      * not found.
    403      */
    404     static ViewNode findNodeByAutofillId(AssistStructure structure, AutofillId id) {
    405         return findNodeByFilter(structure, id, AUTOFILL_ID_FILTER);
    406     }
    407 
    408     /**
    409      * Asserts a text-based node is sanitized.
    410      */
    411     static void assertTextIsSanitized(ViewNode node) {
    412         final CharSequence text = node.getText();
    413         final String resourceId = node.getIdEntry();
    414         if (!TextUtils.isEmpty(text)) {
    415             throw new AssertionError("text on sanitized field " + resourceId + ": " + text);
    416         }
    417 
    418         assertNotFromResources(node);
    419         assertNodeHasNoAutofillValue(node);
    420     }
    421 
    422     private static void assertNotFromResources(ViewNode node) {
    423         assertThat(node.getTextIdEntry()).isNull();
    424     }
    425 
    426     static void assertNodeHasNoAutofillValue(ViewNode node) {
    427         final AutofillValue value = node.getAutofillValue();
    428         if (value != null) {
    429             final String text = value.isText() ? value.getTextValue().toString() : "N/A";
    430             throw new AssertionError("node has value: " + value + " text=" + text);
    431         }
    432     }
    433 
    434     /**
    435      * Asserts the contents of a text-based node that is also auto-fillable.
    436      */
    437     static void assertTextOnly(ViewNode node, String expectedValue) {
    438         assertText(node, expectedValue, false);
    439         assertNotFromResources(node);
    440     }
    441 
    442     /**
    443      * Asserts the contents of a text-based node that is also auto-fillable.
    444      */
    445     static void assertTextOnly(AssistStructure structure, String resourceId, String expectedValue) {
    446         final ViewNode node = findNodeByResourceId(structure, resourceId);
    447         assertText(node, expectedValue, false);
    448         assertNotFromResources(node);
    449     }
    450 
    451     /**
    452      * Asserts the contents of a text-based node that is also auto-fillable.
    453      */
    454     static void assertTextAndValue(ViewNode node, String expectedValue) {
    455         assertText(node, expectedValue, true);
    456         assertNotFromResources(node);
    457     }
    458 
    459     /**
    460      * Asserts a text-based node exists and verify its values.
    461      */
    462     static ViewNode assertTextAndValue(AssistStructure structure, String resourceId,
    463             String expectedValue) {
    464         final ViewNode node = findNodeByResourceId(structure, resourceId);
    465         assertTextAndValue(node, expectedValue);
    466         return node;
    467     }
    468 
    469     /**
    470      * Asserts a text-based node exists and is sanitized.
    471      */
    472     static ViewNode assertValue(AssistStructure structure, String resourceId,
    473             String expectedValue) {
    474         final ViewNode node = findNodeByResourceId(structure, resourceId);
    475         assertTextValue(node, expectedValue);
    476         return node;
    477     }
    478 
    479     /**
    480      * Asserts the values of a text-based node whose string come from resoruces.
    481      */
    482     static ViewNode assertTextFromResouces(AssistStructure structure, String resourceId,
    483             String expectedValue, boolean isAutofillable, String expectedTextIdEntry) {
    484         final ViewNode node = findNodeByResourceId(structure, resourceId);
    485         assertText(node, expectedValue, isAutofillable);
    486         assertThat(node.getTextIdEntry()).isEqualTo(expectedTextIdEntry);
    487         return node;
    488     }
    489 
    490     private static void assertText(ViewNode node, String expectedValue, boolean isAutofillable) {
    491         assertWithMessage("wrong text on %s", node.getAutofillId()).that(node.getText().toString())
    492                 .isEqualTo(expectedValue);
    493         final AutofillValue value = node.getAutofillValue();
    494         final AutofillId id = node.getAutofillId();
    495         if (isAutofillable) {
    496             assertWithMessage("null auto-fill value on %s", id).that(value).isNotNull();
    497             assertWithMessage("wrong auto-fill value on %s", id)
    498                     .that(value.getTextValue().toString()).isEqualTo(expectedValue);
    499         } else {
    500             assertWithMessage("node %s should not have AutofillValue", id).that(value).isNull();
    501         }
    502     }
    503 
    504     /**
    505      * Asserts the auto-fill value of a text-based node.
    506      */
    507     static ViewNode assertTextValue(ViewNode node, String expectedText) {
    508         final AutofillValue value = node.getAutofillValue();
    509         final AutofillId id = node.getAutofillId();
    510         assertWithMessage("null autofill value on %s", id).that(value).isNotNull();
    511         assertWithMessage("wrong autofill type on %s", id).that(value.isText()).isTrue();
    512         assertWithMessage("wrong autofill value on %s", id).that(value.getTextValue().toString())
    513                 .isEqualTo(expectedText);
    514         return node;
    515     }
    516 
    517     /**
    518      * Asserts the auto-fill value of a list-based node.
    519      */
    520     static ViewNode assertListValue(ViewNode node, int expectedIndex) {
    521         final AutofillValue value = node.getAutofillValue();
    522         final AutofillId id = node.getAutofillId();
    523         assertWithMessage("null autofill value on %s", id).that(value).isNotNull();
    524         assertWithMessage("wrong autofill type on %s", id).that(value.isList()).isTrue();
    525         assertWithMessage("wrong autofill value on %s", id).that(value.getListValue())
    526                 .isEqualTo(expectedIndex);
    527         return node;
    528     }
    529 
    530     /**
    531      * Asserts the auto-fill value of a toggle-based node.
    532      */
    533     static void assertToggleValue(ViewNode node, boolean expectedToggle) {
    534         final AutofillValue value = node.getAutofillValue();
    535         final AutofillId id = node.getAutofillId();
    536         assertWithMessage("null autofill value on %s", id).that(value).isNotNull();
    537         assertWithMessage("wrong autofill type on %s", id).that(value.isToggle()).isTrue();
    538         assertWithMessage("wrong autofill value on %s", id).that(value.getToggleValue())
    539                 .isEqualTo(expectedToggle);
    540     }
    541 
    542     /**
    543      * Asserts the auto-fill value of a date-based node.
    544      */
    545     static void assertDateValue(Object object, AutofillValue value, int year, int month, int day) {
    546         assertWithMessage("null autofill value on %s", object).that(value).isNotNull();
    547         assertWithMessage("wrong autofill type on %s", object).that(value.isDate()).isTrue();
    548 
    549         final Calendar cal = Calendar.getInstance();
    550         cal.setTimeInMillis(value.getDateValue());
    551 
    552         assertWithMessage("Wrong year on AutofillValue %s", value)
    553             .that(cal.get(Calendar.YEAR)).isEqualTo(year);
    554         assertWithMessage("Wrong month on AutofillValue %s", value)
    555             .that(cal.get(Calendar.MONTH)).isEqualTo(month);
    556         assertWithMessage("Wrong day on AutofillValue %s", value)
    557              .that(cal.get(Calendar.DAY_OF_MONTH)).isEqualTo(day);
    558     }
    559 
    560     /**
    561      * Asserts the auto-fill value of a date-based node.
    562      */
    563     static void assertDateValue(ViewNode node, int year, int month, int day) {
    564         assertDateValue(node, node.getAutofillValue(), year, month, day);
    565     }
    566 
    567     /**
    568      * Asserts the auto-fill value of a date-based view.
    569      */
    570     static void assertDateValue(View view, int year, int month, int day) {
    571         assertDateValue(view, view.getAutofillValue(), year, month, day);
    572     }
    573 
    574     /**
    575      * Asserts the auto-fill value of a time-based node.
    576      */
    577     private static void assertTimeValue(Object object, AutofillValue value, int hour, int minute) {
    578         assertWithMessage("null autofill value on %s", object).that(value).isNotNull();
    579         assertWithMessage("wrong autofill type on %s", object).that(value.isDate()).isTrue();
    580 
    581         final Calendar cal = Calendar.getInstance();
    582         cal.setTimeInMillis(value.getDateValue());
    583 
    584         assertWithMessage("Wrong hour on AutofillValue %s", value)
    585             .that(cal.get(Calendar.HOUR_OF_DAY)).isEqualTo(hour);
    586         assertWithMessage("Wrong minute on AutofillValue %s", value)
    587             .that(cal.get(Calendar.MINUTE)).isEqualTo(minute);
    588     }
    589 
    590     /**
    591      * Asserts the auto-fill value of a time-based node.
    592      */
    593     static void assertTimeValue(ViewNode node, int hour, int minute) {
    594         assertTimeValue(node, node.getAutofillValue(), hour, minute);
    595     }
    596 
    597     /**
    598      * Asserts the auto-fill value of a time-based view.
    599      */
    600     static void assertTimeValue(View view, int hour, int minute) {
    601         assertTimeValue(view, view.getAutofillValue(), hour, minute);
    602     }
    603 
    604     /**
    605      * Asserts a text-based node exists and is sanitized.
    606      */
    607     static ViewNode assertTextIsSanitized(AssistStructure structure, String resourceId) {
    608         final ViewNode node = findNodeByResourceId(structure, resourceId);
    609         assertWithMessage("no ViewNode with id %s", resourceId).that(node).isNotNull();
    610         assertTextIsSanitized(node);
    611         return node;
    612     }
    613 
    614     /**
    615      * Asserts a list-based node exists and is sanitized.
    616      */
    617     static void assertListValueIsSanitized(AssistStructure structure, String resourceId) {
    618         final ViewNode node = findNodeByResourceId(structure, resourceId);
    619         assertWithMessage("no ViewNode with id %s", resourceId).that(node).isNotNull();
    620         assertTextIsSanitized(node);
    621     }
    622 
    623     /**
    624      * Asserts a toggle node exists and is sanitized.
    625      */
    626     static void assertToggleIsSanitized(AssistStructure structure, String resourceId) {
    627         final ViewNode node = findNodeByResourceId(structure, resourceId);
    628         assertNodeHasNoAutofillValue(node);
    629         assertWithMessage("ViewNode %s should not be checked", resourceId).that(node.isChecked())
    630                 .isFalse();
    631     }
    632 
    633     /**
    634      * Asserts a node exists and has the {@code expected} number of children.
    635      */
    636     static void assertNumberOfChildren(AssistStructure structure, String resourceId, int expected) {
    637         final ViewNode node = findNodeByResourceId(structure, resourceId);
    638         final int actual = node.getChildCount();
    639         if (actual != expected) {
    640             dumpStructure("assertNumberOfChildren()", structure);
    641             throw new AssertionError("assertNumberOfChildren() for " + resourceId
    642                     + " failed: expected " + expected + ", got " + actual);
    643         }
    644     }
    645 
    646     /**
    647      * Asserts the number of children in the Assist structure.
    648      */
    649     static void assertNumberOfChildren(AssistStructure structure, int expected) {
    650         assertWithMessage("wrong number of nodes").that(structure.getWindowNodeCount())
    651                 .isEqualTo(1);
    652         final int actual = getNumberNodes(structure);
    653         if (actual != expected) {
    654             dumpStructure("assertNumberOfChildren()", structure);
    655             throw new AssertionError("assertNumberOfChildren() for structure failed: expected "
    656                     + expected + ", got " + actual);
    657         }
    658     }
    659 
    660     /**
    661      * Gets the total number of nodes in an structure.
    662      */
    663     static int getNumberNodes(AssistStructure structure) {
    664         int count = 0;
    665         final int nodes = structure.getWindowNodeCount();
    666         for (int i = 0; i < nodes; i++) {
    667             final WindowNode windowNode = structure.getWindowNodeAt(i);
    668             final ViewNode rootNode = windowNode.getRootViewNode();
    669             count += getNumberNodes(rootNode);
    670         }
    671         return count;
    672     }
    673 
    674     /**
    675      * Gets the total number of nodes in an node, including all descendants and the node itself.
    676      */
    677     static int getNumberNodes(ViewNode node) {
    678         int count = 1;
    679         final int childrenSize = node.getChildCount();
    680         if (childrenSize > 0) {
    681             for (int i = 0; i < childrenSize; i++) {
    682                 count += getNumberNodes(node.getChildAt(i));
    683             }
    684         }
    685         return count;
    686     }
    687 
    688     /**
    689      * Creates an array of {@link AutofillId} mapped from the {@code structure} nodes with the given
    690      * {@code resourceIds}.
    691      */
    692     static AutofillId[] getAutofillIds(Function<String, ViewNode> nodeResolver,
    693             String[] resourceIds) {
    694         if (resourceIds == null) return null;
    695 
    696         final AutofillId[] requiredIds = new AutofillId[resourceIds.length];
    697         for (int i = 0; i < resourceIds.length; i++) {
    698             final String resourceId = resourceIds[i];
    699             final ViewNode node = nodeResolver.apply(resourceId);
    700             if (node == null) {
    701                 throw new AssertionError("No node with savable resourceId " + resourceId);
    702             }
    703             requiredIds[i] = node.getAutofillId();
    704 
    705         }
    706         return requiredIds;
    707     }
    708 
    709     /**
    710      * Prevents the screen to rotate by itself
    711      */
    712     public static void disableAutoRotation(UiBot uiBot) throws Exception {
    713         runShellCommand(ACCELLEROMETER_CHANGE, 0);
    714         uiBot.setScreenOrientation(PORTRAIT);
    715     }
    716 
    717     /**
    718      * Allows the screen to rotate by itself
    719      */
    720     public static void allowAutoRotation() {
    721         runShellCommand(ACCELLEROMETER_CHANGE, 1);
    722     }
    723 
    724     /**
    725      * Gets the maximum number of partitions per session.
    726      */
    727     public static int getMaxPartitions() {
    728         return Integer.parseInt(runShellCommand("cmd autofill get max_partitions"));
    729     }
    730 
    731     /**
    732      * Sets the maximum number of partitions per session.
    733      */
    734     public static void setMaxPartitions(int value) {
    735         runShellCommand("cmd autofill set max_partitions %d", value);
    736         assertThat(getMaxPartitions()).isEqualTo(value);
    737     }
    738 
    739     /**
    740      * Checks if device supports the Autofill feature.
    741      */
    742     public static boolean hasAutofillFeature() {
    743         return RequiredFeatureRule.hasFeature(PackageManager.FEATURE_AUTOFILL);
    744     }
    745 
    746     /**
    747      * Checks if autofill window is fullscreen, see com.android.server.autofill.ui.FillUi.
    748      */
    749     public static boolean isAutofillWindowFullScreen(Context context) {
    750         return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
    751     }
    752 
    753     /**
    754      * Checks if screen orientation can be changed.
    755      */
    756     public static boolean isRotationSupported(Context context) {
    757         return !context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
    758     }
    759 
    760     /**
    761      * Uses Shell command to get the Autofill logging level.
    762      */
    763     public static String getLoggingLevel() {
    764         return runShellCommand("cmd autofill get log_level");
    765     }
    766 
    767     /**
    768      * Uses Shell command to set the Autofill logging level.
    769      */
    770     public static void setLoggingLevel(String level) {
    771         runShellCommand("cmd autofill set log_level %s", level);
    772     }
    773 
    774     /**
    775      * Uses Settings to enable the given autofill service for the default user, and checks the
    776      * value was properly check, throwing an exception if it was not.
    777      */
    778     public static void enableAutofillService(@NonNull Context context,
    779             @NonNull String serviceName) {
    780         if (isAutofillServiceEnabled(serviceName)) return;
    781 
    782         SettingsHelper.syncSet(context, AUTOFILL_SERVICE, serviceName);
    783     }
    784 
    785     /**
    786      * Uses Settings to disable the given autofill service for the default user, and checks the
    787      * value was properly check, throwing an exception if it was not.
    788      */
    789     public static void disableAutofillService(@NonNull Context context,
    790             @NonNull String serviceName) {
    791         if (!isAutofillServiceEnabled(serviceName)) return;
    792 
    793         SettingsHelper.syncDelete(context, AUTOFILL_SERVICE);
    794     }
    795 
    796     /**
    797      * Checks whether the given service is set as the autofill service for the default user.
    798      */
    799     private static boolean isAutofillServiceEnabled(@NonNull String serviceName) {
    800         final String actualName = SettingsHelper.get(AUTOFILL_SERVICE);
    801         return serviceName.equals(actualName);
    802     }
    803 
    804     /**
    805      * Asserts whether the given service is enabled as the autofill service for the default user.
    806      */
    807     public static void assertAutofillServiceStatus(@NonNull String serviceName, boolean enabled) {
    808         final String actual = SettingsHelper.get(AUTOFILL_SERVICE);
    809         final String expected = enabled ? serviceName : "null";
    810         assertWithMessage("Invalid value for secure setting %s", AUTOFILL_SERVICE)
    811                 .that(actual).isEqualTo(expected);
    812     }
    813 
    814     /**
    815      * Asserts that there is a pending session for the given package.
    816      */
    817     public static void assertHasSessions(String packageName) {
    818         final String result = runShellCommand(CMD_LIST_SESSIONS);
    819         assertThat(result).contains(packageName);
    820     }
    821 
    822     /**
    823      * Gets the instrumentation context.
    824      */
    825     public static Context getContext() {
    826         return InstrumentationRegistry.getInstrumentation().getContext();
    827     }
    828 
    829     /**
    830      * Cleans up the autofill state; should be called before pretty much any test.
    831      */
    832     public static void preTestCleanup() {
    833         if (!hasAutofillFeature()) return;
    834 
    835         Log.d(TAG, "preTestCleanup()");
    836 
    837         disableAutofillService(getContext(), SERVICE_NAME);
    838         InstrumentedAutoFillService.setIgnoreUnexpectedRequests(true);
    839 
    840         InstrumentedAutoFillService.resetStaticState();
    841         AuthenticationActivity.resetStaticState();
    842     }
    843 
    844     /**
    845      * Asserts the node has an {@code HTMLInfo} property, with the given tag.
    846      */
    847     public static HtmlInfo assertHasHtmlTag(ViewNode node, String expectedTag) {
    848         final HtmlInfo info = node.getHtmlInfo();
    849         assertWithMessage("node doesn't have htmlInfo").that(info).isNotNull();
    850         assertWithMessage("wrong tag").that(info.getTag()).isEqualTo(expectedTag);
    851         return info;
    852     }
    853 
    854     /**
    855      * Gets the value of an {@code HTMLInfo} attribute.
    856      */
    857     @Nullable
    858     public static String getAttributeValue(HtmlInfo info, String attribute) {
    859         for (Pair<String, String> pair : info.getAttributes()) {
    860             if (pair.first.equals(attribute)) {
    861                 return pair.second;
    862             }
    863         }
    864         return null;
    865     }
    866 
    867     /**
    868      * Asserts a {@code HTMLInfo} has an attribute with a given value.
    869      */
    870     public static void assertHasAttribute(HtmlInfo info, String attribute, String expectedValue) {
    871         final String actualValue = getAttributeValue(info, attribute);
    872         assertWithMessage("Attribute %s not found", attribute).that(actualValue).isNotNull();
    873         assertWithMessage("Wrong value for Attribute %s", attribute)
    874             .that(actualValue).isEqualTo(expectedValue);
    875     }
    876 
    877     /**
    878      * Finds a {@link WebView} node given its expected form name.
    879      */
    880     public static ViewNode findWebViewNodeByFormName(AssistStructure structure, String formName) {
    881         return findNodeByFilter(structure, formName, WEBVIEW_FORM_FILTER);
    882     }
    883 
    884     private static void assertClientState(Object container, Bundle clientState,
    885             String key, String value) {
    886         assertWithMessage("'%s' should have client state", container)
    887             .that(clientState).isNotNull();
    888         assertWithMessage("Wrong number of client state extras on '%s'", container)
    889             .that(clientState.keySet().size()).isEqualTo(1);
    890         assertWithMessage("Wrong value for client state key (%s) on '%s'", key, container)
    891             .that(clientState.getString(key)).isEqualTo(value);
    892     }
    893 
    894     /**
    895      * Asserts the content of a {@link FillEventHistory#getClientState()}.
    896      *
    897      * @param history event to be asserted
    898      * @param key the only key expected in the client state bundle
    899      * @param value the only value expected in the client state bundle
    900      */
    901     @SuppressWarnings("javadoc")
    902     public static void assertDeprecatedClientState(@NonNull FillEventHistory history,
    903             @NonNull String key, @NonNull String value) {
    904         assertThat(history).isNotNull();
    905         @SuppressWarnings("deprecation")
    906         final Bundle clientState = history.getClientState();
    907         assertClientState(history, clientState, key, value);
    908     }
    909 
    910     /**
    911      * Asserts the {@link FillEventHistory#getClientState()} is not set.
    912      *
    913      * @param history event to be asserted
    914      */
    915     @SuppressWarnings("javadoc")
    916     public static void assertNoDeprecatedClientState(@NonNull FillEventHistory history) {
    917         assertThat(history).isNotNull();
    918         @SuppressWarnings("deprecation")
    919         final Bundle clientState = history.getClientState();
    920         assertWithMessage("History '%s' should not have client state", history)
    921              .that(clientState).isNull();
    922     }
    923 
    924     /**
    925      * Asserts the content of a {@link android.service.autofill.FillEventHistory.Event}.
    926      *
    927      * @param event event to be asserted
    928      * @param eventType expected type
    929      * @param datasetId dataset set id expected in the event
    930      * @param key the only key expected in the client state bundle (or {@code null} if it shouldn't
    931      * have client state)
    932      * @param value the only value expected in the client state bundle (or {@code null} if it
    933      * shouldn't have client state)
    934      * @param fieldClassificationResults expected results when asserting field classification
    935      */
    936     private static void assertFillEvent(@NonNull FillEventHistory.Event event,
    937             int eventType, @Nullable String datasetId,
    938             @Nullable String key, @Nullable String value,
    939             @Nullable FieldClassificationResult[] fieldClassificationResults) {
    940         assertThat(event).isNotNull();
    941         assertWithMessage("Wrong type for %s", event).that(event.getType()).isEqualTo(eventType);
    942         if (datasetId == null) {
    943             assertWithMessage("Event %s should not have dataset id", event)
    944                 .that(event.getDatasetId()).isNull();
    945         } else {
    946             assertWithMessage("Wrong dataset id for %s", event)
    947                 .that(event.getDatasetId()).isEqualTo(datasetId);
    948         }
    949         final Bundle clientState = event.getClientState();
    950         if (key == null) {
    951             assertWithMessage("Event '%s' should not have client state", event)
    952                 .that(clientState).isNull();
    953         } else {
    954             assertClientState(event, clientState, key, value);
    955         }
    956         assertWithMessage("Event '%s' should not have selected datasets", event)
    957                 .that(event.getSelectedDatasetIds()).isEmpty();
    958         assertWithMessage("Event '%s' should not have ignored datasets", event)
    959                 .that(event.getIgnoredDatasetIds()).isEmpty();
    960         assertWithMessage("Event '%s' should not have changed fields", event)
    961                 .that(event.getChangedFields()).isEmpty();
    962         assertWithMessage("Event '%s' should not have manually-entered fields", event)
    963                 .that(event.getManuallyEnteredField()).isEmpty();
    964         final Map<AutofillId, FieldClassification> detectedFields = event.getFieldsClassification();
    965         if (fieldClassificationResults == null) {
    966             assertThat(detectedFields).isEmpty();
    967         } else {
    968             assertThat(detectedFields).hasSize(fieldClassificationResults.length);
    969             int i = 0;
    970             for (Entry<AutofillId, FieldClassification> entry : detectedFields.entrySet()) {
    971                 assertMatches(i, entry, fieldClassificationResults[i]);
    972                 i++;
    973             }
    974         }
    975     }
    976 
    977     private static void assertMatches(int i, Entry<AutofillId, FieldClassification> actualResult,
    978             FieldClassificationResult expectedResult) {
    979         assertWithMessage("Wrong field id at index %s", i).that(actualResult.getKey())
    980                 .isEqualTo(expectedResult.id);
    981         final List<Match> matches = actualResult.getValue().getMatches();
    982         assertWithMessage("Wrong number of matches: " + matches).that(matches.size())
    983                 .isEqualTo(expectedResult.remoteIds.length);
    984         for (int j = 0; j < matches.size(); j++) {
    985             final Match match = matches.get(j);
    986             assertWithMessage("Wrong categoryId at (%s, %s): %s", i, j, match)
    987                 .that(match.getCategoryId()).isEqualTo(expectedResult.remoteIds[j]);
    988             assertWithMessage("Wrong score at (%s, %s): %s", i, j, match)
    989                 .that(match.getScore()).isWithin(0.01f).of(expectedResult.scores[j]);
    990         }
    991     }
    992 
    993     /**
    994      * Asserts the content of a
    995      * {@link android.service.autofill.FillEventHistory.Event#TYPE_DATASET_SELECTED} event.
    996      *
    997      * @param event event to be asserted
    998      * @param datasetId dataset set id expected in the event
    999      */
   1000     public static void assertFillEventForDatasetSelected(@NonNull FillEventHistory.Event event,
   1001             @Nullable String datasetId) {
   1002         assertFillEvent(event, TYPE_DATASET_SELECTED, datasetId, null, null, null);
   1003     }
   1004 
   1005     /**
   1006      * Asserts the content of a
   1007      * {@link android.service.autofill.FillEventHistory.Event#TYPE_DATASET_SELECTED} event.
   1008      *
   1009      * @param event event to be asserted
   1010      * @param datasetId dataset set id expected in the event
   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     public static void assertFillEventForDatasetSelected(@NonNull FillEventHistory.Event event,
   1015             @Nullable String datasetId, @Nullable String key, @Nullable String value) {
   1016         assertFillEvent(event, TYPE_DATASET_SELECTED, datasetId, key, value, null);
   1017     }
   1018 
   1019     /**
   1020      * Asserts the content of a
   1021      * {@link android.service.autofill.FillEventHistory.Event#TYPE_SAVE_SHOWN} event.
   1022      *
   1023      * @param event event to be asserted
   1024      * @param datasetId dataset set id expected in the event
   1025      * @param key the only key expected in the client state bundle
   1026      * @param value the only value expected in the client state bundle
   1027      */
   1028     public static void assertFillEventForSaveShown(@NonNull FillEventHistory.Event event,
   1029             @NonNull String datasetId, @NonNull String key, @NonNull String value) {
   1030         assertFillEvent(event, TYPE_SAVE_SHOWN, datasetId, key, value, null);
   1031     }
   1032 
   1033     /**
   1034      * Asserts the content of a
   1035      * {@link android.service.autofill.FillEventHistory.Event#TYPE_SAVE_SHOWN} event.
   1036      *
   1037      * @param event event to be asserted
   1038      * @param datasetId dataset set id expected in the event
   1039      */
   1040     public static void assertFillEventForSaveShown(@NonNull FillEventHistory.Event event,
   1041             @NonNull String datasetId) {
   1042         assertFillEvent(event, TYPE_SAVE_SHOWN, datasetId, null, null, null);
   1043     }
   1044 
   1045     /**
   1046      * Asserts the content of a
   1047      * {@link android.service.autofill.FillEventHistory.Event#TYPE_DATASET_AUTHENTICATION_SELECTED}
   1048      * event.
   1049      *
   1050      * @param event event to be asserted
   1051      * @param datasetId dataset set id expected in the event
   1052      * @param key the only key expected in the client state bundle
   1053      * @param value the only value expected in the client state bundle
   1054      */
   1055     public static void assertFillEventForDatasetAuthenticationSelected(
   1056             @NonNull FillEventHistory.Event event,
   1057             @Nullable String datasetId, @NonNull String key, @NonNull String value) {
   1058         assertFillEvent(event, TYPE_DATASET_AUTHENTICATION_SELECTED, datasetId, key, value, null);
   1059     }
   1060 
   1061     /**
   1062      * Asserts the content of a
   1063      * {@link android.service.autofill.FillEventHistory.Event#TYPE_AUTHENTICATION_SELECTED} event.
   1064      *
   1065      * @param event event to be asserted
   1066      * @param datasetId dataset set id expected in the event
   1067      * @param key the only key expected in the client state bundle
   1068      * @param value the only value expected in the client state bundle
   1069      */
   1070     public static void assertFillEventForAuthenticationSelected(
   1071             @NonNull FillEventHistory.Event event,
   1072             @Nullable String datasetId, @NonNull String key, @NonNull String value) {
   1073         assertFillEvent(event, TYPE_AUTHENTICATION_SELECTED, datasetId, key, value, null);
   1074     }
   1075 
   1076     public static void assertFillEventForFieldsClassification(@NonNull FillEventHistory.Event event,
   1077             @NonNull AutofillId fieldId, @NonNull String remoteId, float score) {
   1078         assertFillEvent(event, TYPE_CONTEXT_COMMITTED, null, null, null,
   1079                 new FieldClassificationResult[] {
   1080                         new FieldClassificationResult(fieldId, remoteId, score)
   1081                 });
   1082     }
   1083 
   1084     public static void assertFillEventForFieldsClassification(@NonNull FillEventHistory.Event event,
   1085             @NonNull FieldClassificationResult[] results) {
   1086         assertFillEvent(event, TYPE_CONTEXT_COMMITTED, null, null, null, results);
   1087     }
   1088 
   1089     public static void assertFillEventForContextCommitted(@NonNull FillEventHistory.Event event) {
   1090         assertFillEvent(event, TYPE_CONTEXT_COMMITTED, null, null, null, null);
   1091     }
   1092 
   1093     @NonNull
   1094     public static String getActivityName(List<FillContext> contexts) {
   1095         if (contexts == null) return "N/A (null contexts)";
   1096 
   1097         if (contexts.isEmpty()) return "N/A (empty contexts)";
   1098 
   1099         final AssistStructure structure = contexts.get(contexts.size() - 1).getStructure();
   1100         if (structure == null) return "N/A (no AssistStructure)";
   1101 
   1102         final ComponentName componentName = structure.getActivityComponent();
   1103         if (componentName == null) return "N/A (no component name)";
   1104 
   1105         return componentName.flattenToShortString();
   1106     }
   1107 
   1108     public static void assertFloat(float actualValue, float expectedValue) {
   1109         assertThat(actualValue).isWithin(1.0e-10f).of(expectedValue);
   1110     }
   1111 
   1112     public static void assertHasFlags(int actualFlags, int expectedFlags) {
   1113         assertWithMessage("Flags %s not in %s", expectedFlags, actualFlags)
   1114                 .that(actualFlags & expectedFlags).isEqualTo(expectedFlags);
   1115     }
   1116 
   1117     public static String callbackEventAsString(int event) {
   1118         switch (event) {
   1119             case AutofillCallback.EVENT_INPUT_HIDDEN:
   1120                 return "HIDDEN";
   1121             case AutofillCallback.EVENT_INPUT_SHOWN:
   1122                 return "SHOWN";
   1123             case AutofillCallback.EVENT_INPUT_UNAVAILABLE:
   1124                 return "UNAVAILABLE";
   1125             default:
   1126                 return "UNKNOWN:" + event;
   1127         }
   1128     }
   1129 
   1130     public static String importantForAutofillAsString(int mode) {
   1131         switch (mode) {
   1132             case View.IMPORTANT_FOR_AUTOFILL_AUTO:
   1133                 return "IMPORTANT_FOR_AUTOFILL_AUTO";
   1134             case View.IMPORTANT_FOR_AUTOFILL_YES:
   1135                 return "IMPORTANT_FOR_AUTOFILL_YES";
   1136             case View.IMPORTANT_FOR_AUTOFILL_YES_EXCLUDE_DESCENDANTS:
   1137                 return "IMPORTANT_FOR_AUTOFILL_YES_EXCLUDE_DESCENDANTS";
   1138             case View.IMPORTANT_FOR_AUTOFILL_NO:
   1139                 return "IMPORTANT_FOR_AUTOFILL_NO";
   1140             case View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS:
   1141                 return "IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS";
   1142             default:
   1143                 return "UNKNOWN:" + mode;
   1144         }
   1145     }
   1146 
   1147     public static boolean hasHint(@Nullable String[] hints, @Nullable Object expectedHint) {
   1148         if (hints == null || expectedHint == null) return false;
   1149         for (String actualHint : hints) {
   1150             if (expectedHint.equals(actualHint)) return true;
   1151         }
   1152         return false;
   1153     }
   1154 
   1155     /**
   1156      * Asserts that 2 bitmaps have are the same. If they aren't throws an exception and dump them
   1157      * locally so their can be visually inspected.
   1158      *
   1159      * @param filename base name of the files generated in case of error
   1160      * @param bitmap1 first bitmap to be compared
   1161      * @param bitmap2 second bitmap to be compared
   1162      */
   1163     public static void assertBitmapsAreSame(@NonNull String filename, @Nullable Bitmap bitmap1,
   1164             @Nullable Bitmap bitmap2) throws IOException {
   1165         assertWithMessage("1st bitmap is null").that(bitmap1).isNotNull();
   1166         assertWithMessage("2nd bitmap is null").that(bitmap2).isNotNull();
   1167         final boolean same = bitmap1.sameAs(bitmap2);
   1168         if (same) {
   1169             Log.v(TAG, "bitmap comparison passed for " + filename);
   1170             return;
   1171         }
   1172 
   1173         final File dir = new File(LOCAL_DIRECTORY);
   1174         dir.mkdirs();
   1175         if (!dir.exists()) {
   1176             Log.e(TAG, "Could not create directory " + dir);
   1177             throw new AssertionError("bitmap comparison failed for " + filename
   1178                     + ", and bitmaps could not be dumped on " + dir);
   1179         }
   1180         final File dump1 = dumpBitmap(bitmap1, dir, filename + "-1.png");
   1181         final File dump2 = dumpBitmap(bitmap2, dir, filename + "-2.png");
   1182         throw new AssertionError(
   1183                 "bitmap comparison failed; check contents of " + dump1 + " and " + dump2);
   1184     }
   1185 
   1186     @Nullable
   1187     private static File dumpBitmap(@NonNull Bitmap bitmap, @NonNull File dir,
   1188             @NonNull String filename) throws IOException {
   1189         final File file = new File(dir, filename);
   1190         if (file.exists()) {
   1191             file.delete();
   1192         }
   1193         if (!file.createNewFile()) {
   1194             Log.e(TAG, "Could not create file " + file);
   1195             return null;
   1196         }
   1197         Log.d(TAG, "Dumping bitmap at " + file);
   1198         BitmapUtils.saveBitmap(bitmap, file.getParent(), file.getName());
   1199         return file;
   1200     }
   1201 
   1202     private Helper() {
   1203         throw new UnsupportedOperationException("contain static methods only");
   1204     }
   1205 
   1206     static class FieldClassificationResult {
   1207         public final AutofillId id;
   1208         public final String[] remoteIds;
   1209         public final float[] scores;
   1210 
   1211         FieldClassificationResult(@NonNull AutofillId id, @NonNull String remoteId, float score) {
   1212             this(id, new String[] { remoteId }, new float[] { score });
   1213         }
   1214 
   1215         FieldClassificationResult(@NonNull AutofillId id, @NonNull String[] remoteIds,
   1216                 float[] scores) {
   1217             this.id = id;
   1218             this.remoteIds = remoteIds;
   1219             this.scores = scores;
   1220         }
   1221     }
   1222 }
   1223