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