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