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.Timeouts.DATASET_PICKER_NOT_SHOWN_NAPTIME_MS;
20 import static android.autofillservice.cts.testcore.Timeouts.LONG_PRESS_MS;
21 import static android.autofillservice.cts.testcore.Timeouts.SAVE_NOT_SHOWN_NAPTIME_MS;
22 import static android.autofillservice.cts.testcore.Timeouts.SAVE_TIMEOUT;
23 import static android.autofillservice.cts.testcore.Timeouts.UI_DATASET_PICKER_TIMEOUT;
24 import static android.autofillservice.cts.testcore.Timeouts.UI_SCREEN_ORIENTATION_TIMEOUT;
25 import static android.autofillservice.cts.testcore.Timeouts.UI_TIMEOUT;
26 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_ADDRESS;
27 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD;
28 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_DEBIT_CARD;
29 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_EMAIL_ADDRESS;
30 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC;
31 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC_CARD;
32 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
33 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PAYMENT_CARD;
34 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_USERNAME;
35 
36 import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
37 
38 import static com.google.common.truth.Truth.assertThat;
39 import static com.google.common.truth.Truth.assertWithMessage;
40 
41 import static org.junit.Assume.assumeTrue;
42 
43 import android.app.Activity;
44 import android.app.Instrumentation;
45 import android.app.UiAutomation;
46 import android.content.Context;
47 import android.content.res.Resources;
48 import android.graphics.Bitmap;
49 import android.graphics.Point;
50 import android.graphics.Rect;
51 import android.hardware.display.DisplayManager;
52 import android.os.SystemClock;
53 import android.service.autofill.SaveInfo;
54 import android.text.Html;
55 import android.text.Spanned;
56 import android.text.style.URLSpan;
57 import android.util.Log;
58 import android.view.Display;
59 import android.view.InputDevice;
60 import android.view.MotionEvent;
61 import android.view.Surface;
62 import android.view.View;
63 import android.view.WindowInsets;
64 import android.view.accessibility.AccessibilityEvent;
65 import android.view.accessibility.AccessibilityNodeInfo;
66 import android.view.accessibility.AccessibilityWindowInfo;
67 
68 import androidx.annotation.NonNull;
69 import androidx.annotation.Nullable;
70 import androidx.test.platform.app.InstrumentationRegistry;
71 import androidx.test.uiautomator.By;
72 import androidx.test.uiautomator.BySelector;
73 import androidx.test.uiautomator.Configurator;
74 import androidx.test.uiautomator.Direction;
75 import androidx.test.uiautomator.SearchCondition;
76 import androidx.test.uiautomator.StaleObjectException;
77 import androidx.test.uiautomator.UiDevice;
78 import androidx.test.uiautomator.UiObject2;
79 import androidx.test.uiautomator.UiObjectNotFoundException;
80 import androidx.test.uiautomator.UiScrollable;
81 import androidx.test.uiautomator.UiSelector;
82 import androidx.test.uiautomator.Until;
83 
84 import com.android.compatibility.common.util.RetryableException;
85 import com.android.compatibility.common.util.Timeout;
86 import com.android.compatibility.common.util.UserHelper;
87 
88 import java.io.File;
89 import java.io.FileInputStream;
90 import java.util.ArrayList;
91 import java.util.Arrays;
92 import java.util.List;
93 import java.util.concurrent.TimeoutException;
94 
95 /**
96  * Helper for UI-related needs.
97  */
98 public class UiBot {
99 
100     private static final String TAG = "AutoFillCtsUiBot";
101 
102     private static final String RESOURCE_ID_DATASET_PICKER = "autofill_dataset_picker";
103     private static final String RESOURCE_ID_DATASET_HEADER = "autofill_dataset_header";
104     private static final String RESOURCE_ID_SAVE_SNACKBAR = "autofill_save";
105     private static final String RESOURCE_ID_SAVE_ICON = "autofill_save_icon";
106     private static final String RESOURCE_ID_SAVE_TITLE = "autofill_save_title";
107     private static final String RESOURCE_ID_CONTEXT_MENUITEM = "floating_toolbar_menu_item_text";
108     private static final String RESOURCE_ID_SAVE_BUTTON_NO = "autofill_save_no";
109     private static final String RESOURCE_ID_SAVE_BUTTON_YES = "autofill_save_yes";
110     private static final String RESOURCE_ID_OVERFLOW = "overflow";
111 
112     private static final String RESOURCE_STRING_SAVE_TITLE = "autofill_save_title";
113     private static final String RESOURCE_STRING_SAVE_TITLE_WITH_TYPE =
114             "autofill_save_title_with_type";
115     private static final String RESOURCE_STRING_SAVE_TYPE_PASSWORD = "autofill_save_type_password";
116     private static final String RESOURCE_STRING_SAVE_TYPE_ADDRESS = "autofill_save_type_address";
117     private static final String RESOURCE_STRING_SAVE_TYPE_CREDIT_CARD =
118             "autofill_save_type_credit_card";
119     private static final String RESOURCE_STRING_SAVE_TYPE_USERNAME = "autofill_save_type_username";
120     private static final String RESOURCE_STRING_SAVE_TYPE_EMAIL_ADDRESS =
121             "autofill_save_type_email_address";
122     private static final String RESOURCE_STRING_SAVE_TYPE_DEBIT_CARD =
123             "autofill_save_type_debit_card";
124     private static final String RESOURCE_STRING_SAVE_TYPE_PAYMENT_CARD =
125             "autofill_save_type_payment_card";
126     private static final String RESOURCE_STRING_SAVE_TYPE_GENERIC_CARD =
127             "autofill_save_type_generic_card";
128     private static final String RESOURCE_STRING_SAVE_BUTTON_NEVER = "autofill_save_never";
129     private static final String RESOURCE_STRING_SAVE_BUTTON_NOT_NOW = "autofill_save_notnow";
130     private static final String RESOURCE_STRING_SAVE_BUTTON_NO_THANKS = "autofill_save_no";
131     private static final String RESOURCE_STRING_SAVE_BUTTON_YES = "autofill_save_yes";
132     private static final String RESOURCE_STRING_UPDATE_BUTTON_YES = "autofill_update_yes";
133     private static final String RESOURCE_STRING_CONTINUE_BUTTON_YES = "autofill_continue_yes";
134     private static final String RESOURCE_STRING_UPDATE_TITLE = "autofill_update_title";
135     private static final String RESOURCE_STRING_UPDATE_TITLE_WITH_TYPE =
136             "autofill_update_title_with_type";
137 
138     private static final String RESOURCE_STRING_AUTOFILL = "autofill";
139     private static final String RESOURCE_STRING_DATASET_PICKER_ACCESSIBILITY_TITLE =
140             "autofill_picker_accessibility_title";
141     private static final String RESOURCE_STRING_SAVE_SNACKBAR_ACCESSIBILITY_TITLE =
142             "autofill_save_accessibility_title";
143 
144     private static final String RESOURCE_ID_FILL_DIALOG_PICKER = "autofill_dialog_picker";
145     private static final String RESOURCE_ID_FILL_DIALOG_HEADER = "autofill_dialog_header";
146     private static final String RESOURCE_ID_FILL_DIALOG_DATASET = "autofill_dialog_list";
147     private static final String RESOURCE_ID_FILL_DIALOG_BUTTON_NO = "autofill_dialog_no";
148     private static final String RESOURCE_ID_FILL_DIALOG_BUTTON_YES = "autofill_dialog_yes";
149 
150     static final BySelector DATASET_PICKER_SELECTOR = By.res("android", RESOURCE_ID_DATASET_PICKER);
151     private static final BySelector SAVE_UI_SELECTOR = By.res("android", RESOURCE_ID_SAVE_SNACKBAR);
152     private static final BySelector DATASET_HEADER_SELECTOR =
153             By.res("android", RESOURCE_ID_DATASET_HEADER);
154     private static final BySelector FILL_DIALOG_SELECTOR =
155             By.res("android", RESOURCE_ID_FILL_DIALOG_PICKER);
156     private static final BySelector FILL_DIALOG_HEADER_SELECTOR =
157             By.res("android", RESOURCE_ID_FILL_DIALOG_HEADER);
158     private static final BySelector FILL_DIALOG_DATASET_SELECTOR =
159             By.res("android", RESOURCE_ID_FILL_DIALOG_DATASET);
160 
161 
162     // TODO: figure out a more reliable solution that does not depend on SystemUI resources.
163     private static final String SPLIT_WINDOW_DIVIDER_ID =
164             "com.android.systemui:id/docked_divider_background";
165 
166     private static final boolean DUMP_ON_ERROR = true;
167 
168     protected static final int MAX_UIOBJECT_RETRY_COUNT = 3;
169 
170     /**
171      * Pass to {@link #setScreenOrientation(int)} to change the display to portrait mode.
172      * This is an alias of Surface.ROTATION_0 though it's named as PORTRAIT for historical reasons.
173      */
174     public static final int PORTRAIT = Surface.ROTATION_0;
175 
176     /**
177      * Pass to {@link #setScreenOrientation(int)} to change the display to landscape mode.
178      * This is an alias of Surface.ROTATION_90 though it's named as LANDSCAPE for historical
179      * reasons.
180      */
181     public static final int LANDSCAPE = Surface.ROTATION_90;
182 
183     private final UiDevice mDevice;
184     private final Context mContext;
185     private final UserHelper mUserHelper;
186     private final String mPackageName;
187     private final UiAutomation mAutoman;
188     private final Timeout mDefaultTimeout;
189 
190     private boolean mOkToCallAssertNoDatasets;
191 
UiBot()192     public UiBot() {
193         this(UI_TIMEOUT);
194     }
195 
UiBot(Timeout defaultTimeout)196     public UiBot(Timeout defaultTimeout) {
197         mDefaultTimeout = defaultTimeout;
198         final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
199         mDevice = UiDevice.getInstance(instrumentation);
200         mContext = instrumentation.getContext();
201         mUserHelper = new UserHelper(mContext);
202         mPackageName = mContext.getPackageName();
203         mAutoman = instrumentation.getUiAutomation();
204     }
205 
waitForIdle()206     public void waitForIdle() {
207         final long before = SystemClock.elapsedRealtimeNanos();
208         mDevice.waitForIdle();
209         final float delta = ((float) (SystemClock.elapsedRealtimeNanos() - before)) / 1_000_000;
210         Log.v(TAG, "device idle in " + delta + "ms");
211     }
212 
waitForIdleSync()213     public void waitForIdleSync() {
214         final long before = SystemClock.elapsedRealtimeNanos();
215         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
216         final float delta = ((float) (SystemClock.elapsedRealtimeNanos() - before)) / 1_000_000;
217         Log.v(TAG, "device idle sync in " + delta + "ms");
218     }
219 
reset()220     public void reset() {
221         mOkToCallAssertNoDatasets = false;
222     }
223 
224     /**
225      * Assumes the device has a minimum height and width of {@code minSize}, throwing a
226      * {@code AssumptionViolatedException} if it doesn't (so the test is skiped by the JUnit
227      * Runner).
228      */
assumeMinimumResolution(int minSize)229     public void assumeMinimumResolution(int minSize) {
230         final int width = mDevice.getDisplayWidth();
231         final int heigth = mDevice.getDisplayHeight();
232         final int min = Math.min(width, heigth);
233         assumeTrue("Screen size is too small (" + width + "x" + heigth + ")", min >= minSize);
234         Log.d(TAG, "assumeMinimumResolution(" + minSize + ") passed: screen size is "
235                 + width + "x" + heigth);
236     }
237 
238     /**
239      * Sets the screen resolution in a way that the IME doesn't interfere with the Autofill UI
240      * when the device is rotated to landscape.
241      *
242      * When called, test must call <p>{@link #resetScreenResolution()} in a {@code finally} block.
243      *
244      * @deprecated this method should not be necessarily anymore as we're using a MockIme.
245      */
246     @Deprecated
247     // TODO: remove once we're sure no more OEM is getting failure due to screen size
setScreenResolution()248     public void setScreenResolution() {
249         if (false) {
250             Log.w(TAG, "setScreenResolution(): ignored");
251             return;
252         }
253         assumeMinimumResolution(500);
254 
255         runShellCommand("wm size 1080x1920");
256         runShellCommand("wm density 320");
257     }
258 
259     /**
260      * Resets the screen resolution.
261      *
262      * <p>Should always be called after {@link #setScreenResolution()}.
263      *
264      * @deprecated this method should not be necessarily anymore as we're using a MockIme.
265      */
266     @Deprecated
267     // TODO: remove once we're sure no more OEM is getting failure due to screen size
resetScreenResolution()268     public void resetScreenResolution() {
269         if (false) {
270             Log.w(TAG, "resetScreenResolution(): ignored");
271             return;
272         }
273         runShellCommand("wm density reset");
274         runShellCommand("wm size reset");
275     }
276 
277     /**
278      * Asserts the dataset picker is not shown anymore.
279      *
280      * @throws IllegalStateException if called *before* an assertion was made to make sure the
281      * dataset picker is shown - if that's not the case, call
282      * {@link #assertNoDatasetsEver()} instead.
283      */
assertNoDatasets()284     public void assertNoDatasets() throws Exception {
285         if (!mOkToCallAssertNoDatasets) {
286             throw new IllegalStateException(
287                     "Cannot call assertNoDatasets() without calling assertDatasets first");
288         }
289         mDevice.wait(Until.gone(DATASET_PICKER_SELECTOR), UI_DATASET_PICKER_TIMEOUT.ms());
290         mOkToCallAssertNoDatasets = false;
291     }
292 
293     /**
294      * Asserts the dataset picker was never shown.
295      *
296      * <p>This method is slower than {@link #assertNoDatasets()} and should only be called in the
297      * cases where the dataset picker was not previous shown.
298      */
assertNoDatasetsEver()299     public void assertNoDatasetsEver() throws Exception {
300         assertNeverShown("dataset picker", DATASET_PICKER_SELECTOR,
301                 DATASET_PICKER_NOT_SHOWN_NAPTIME_MS);
302     }
303 
304     /**
305      * Asserts the dataset chooser is shown and contains exactly the given datasets.
306      *
307      * @return the dataset picker object.
308      */
assertDatasets(String...names)309     public UiObject2 assertDatasets(String...names) throws Exception {
310         final UiObject2 picker = findDatasetPicker(UI_DATASET_PICKER_TIMEOUT);
311         return assertDatasets(picker, names);
312     }
313 
assertDatasets(UiObject2 picker, String...names)314     protected UiObject2 assertDatasets(UiObject2 picker, String...names) {
315         assertWithMessage("wrong dataset names").that(getChildrenAsText(picker))
316                 .containsExactlyElementsIn(Arrays.asList(names)).inOrder();
317         return picker;
318     }
319 
320     /**
321      * Asserts the dataset chooser is shown and contains the given datasets.
322      *
323      * @return the dataset picker object.
324      */
assertDatasetsContains(String...names)325     public UiObject2 assertDatasetsContains(String...names) throws Exception {
326         final UiObject2 picker = findDatasetPicker(UI_DATASET_PICKER_TIMEOUT);
327         assertWithMessage("wrong dataset names").that(getChildrenAsText(picker))
328                 .containsAtLeastElementsIn(Arrays.asList(names)).inOrder();
329         return picker;
330     }
331 
332     /**
333      * Asserts the dataset chooser is shown and contains the given datasets, header, and footer.
334      * <p>In fullscreen, header view is not under R.id.autofill_dataset_picker.
335      *
336      * @return the dataset picker object.
337      */
assertDatasetsWithBorders(String header, String footer, String...names)338     public UiObject2 assertDatasetsWithBorders(String header, String footer, String...names)
339             throws Exception {
340         final UiObject2 picker = findDatasetPicker(UI_DATASET_PICKER_TIMEOUT);
341         final List<String> expectedChild = new ArrayList<>();
342         if (header != null) {
343             if (Helper.isAutofillWindowFullScreen(mContext)) {
344                 final UiObject2 headerView = waitForObject(DATASET_HEADER_SELECTOR,
345                         UI_DATASET_PICKER_TIMEOUT);
346                 assertWithMessage("fullscreen wrong dataset header")
347                         .that(getChildrenAsText(headerView))
348                         .containsExactlyElementsIn(Arrays.asList(header)).inOrder();
349             } else {
350                 expectedChild.add(header);
351             }
352         }
353         expectedChild.addAll(Arrays.asList(names));
354         if (footer != null) {
355             expectedChild.add(footer);
356         }
357         assertWithMessage("wrong elements on dataset picker").that(getChildrenAsText(picker))
358                 .containsExactlyElementsIn(expectedChild).inOrder();
359         return picker;
360     }
361 
362     /**
363      * Gets the text of this object children.
364      */
getChildrenAsText(UiObject2 object)365     public List<String> getChildrenAsText(UiObject2 object) {
366         final List<String> list = new ArrayList<>();
367         getChildrenAsText(object, list);
368         return list;
369     }
370 
getChildrenAsText(UiObject2 object, List<String> children)371     private static void getChildrenAsText(UiObject2 object, List<String> children) {
372         final String text = object.getText();
373         if (text != null) {
374             children.add(text);
375         }
376         for (UiObject2 child : object.getChildren()) {
377             getChildrenAsText(child, children);
378         }
379     }
380 
381     /**
382      * Selects a dataset that should be visible in the floating UI and does not need to wait for
383      * application become idle.
384      */
selectDataset(String name)385     public void selectDataset(String name) throws Exception {
386         final UiObject2 picker = findDatasetPicker(UI_DATASET_PICKER_TIMEOUT);
387         selectDataset(picker, name);
388     }
389 
390     /**
391      * Selects a dataset that should be visible in the floating UI and waits for application become
392      * idle if needed.
393      */
selectDatasetSync(String name)394     public void selectDatasetSync(String name) throws Exception {
395         final UiObject2 picker = findDatasetPicker(UI_DATASET_PICKER_TIMEOUT);
396         selectDataset(picker, name);
397         mDevice.waitForIdle();
398     }
399 
400     /**
401      * Selects a dataset that should be visible in the floating UI.
402      */
selectDataset(UiObject2 picker, String name)403     public void selectDataset(UiObject2 picker, String name) {
404         final UiObject2 dataset = picker.findObject(By.text(name));
405         if (dataset == null) {
406             throw new AssertionError("no dataset " + name + " in " + getChildrenAsText(picker));
407         }
408         dataset.click();
409     }
410 
411     /**
412      * Finds the suggestion by name and perform long click on suggestion to trigger attribution
413      * intent.
414      */
longPressSuggestion(String name)415     public void longPressSuggestion(String name) throws Exception {
416         throw new UnsupportedOperationException();
417     }
418 
419     /**
420      * Asserts the suggestion chooser is shown in the suggestion view.
421      */
assertSuggestion(String name)422     public void assertSuggestion(String name) throws Exception {
423         throw new UnsupportedOperationException();
424     }
425 
426     /**
427      * Asserts the suggestion chooser is not shown in the suggestion view.
428      */
assertNoSuggestion(String name)429     public void assertNoSuggestion(String name) throws Exception {
430         throw new UnsupportedOperationException();
431     }
432 
433     /**
434      * Scrolls the suggestion view.
435      *
436      * @param direction The direction to scroll.
437      * @param speed The speed to scroll per second.
438      */
scrollSuggestionView(Direction direction, int speed)439     public void scrollSuggestionView(Direction direction, int speed) throws Exception {
440         throw new UnsupportedOperationException();
441     }
442 
443     /**
444      * Selects a view by text.
445      *
446      * <p><b>NOTE:</b> when selecting an option in dataset picker is shown, prefer
447      * {@link #selectDataset(String)}.
448      */
selectByText(String name)449     public void selectByText(String name) throws Exception {
450         Log.v(TAG, "selectByText(): " + name);
451 
452         final UiObject2 object = waitForObject(By.text(name));
453         object.click();
454     }
455 
456     /**
457      * Asserts a text is shown.
458      *
459      * <p><b>NOTE:</b> when asserting the dataset picker is shown, prefer
460      * {@link #assertDatasets(String...)}.
461      */
assertShownByText(String text)462     public UiObject2 assertShownByText(String text) throws Exception {
463         return assertShownByText(text, mDefaultTimeout);
464     }
465 
assertShownByText(String text, Timeout timeout)466     public UiObject2 assertShownByText(String text, Timeout timeout) throws Exception {
467         final UiObject2 object = waitForObject(By.text(text), timeout);
468         assertWithMessage("No node with text '%s'", text).that(object).isNotNull();
469         return object;
470     }
471 
472     /**
473      * Finds a node by text, without waiting for it to be shown (but failing if it isn't).
474      */
475     @NonNull
findRightAwayByText(@onNull String text)476     public UiObject2 findRightAwayByText(@NonNull String text) throws Exception {
477         final UiObject2 object = mDevice.findObject(By.text(text));
478         assertWithMessage("no UIObject for text '%s'", text).that(object).isNotNull();
479         return object;
480     }
481 
482     /**
483      * Asserts that the text is not showing for sure in the screen "as is", i.e., without waiting
484      * for it.
485      *
486      * <p>Typically called after another assertion that waits for a condition to be shown.
487      */
assertNotShowingForSure(String text)488     public void assertNotShowingForSure(String text) throws Exception {
489         final UiObject2 object = mDevice.findObject(By.text(text));
490         assertWithMessage("Found node with text '%s'", text).that(object).isNull();
491     }
492 
493     /**
494      * Asserts a node with the given content description is shown.
495      *
496      */
assertShownByContentDescription(String contentDescription)497     public UiObject2 assertShownByContentDescription(String contentDescription) throws Exception {
498         final UiObject2 object = waitForObject(By.desc(contentDescription));
499         assertWithMessage("No node with content description '%s'", contentDescription).that(object)
500                 .isNotNull();
501         return object;
502     }
503 
504     /**
505      * Checks if a View with a certain text exists.
506      */
hasViewWithText(String name)507     public boolean hasViewWithText(String name) {
508         Log.v(TAG, "hasViewWithText(): " + name);
509 
510         return mDevice.findObject(By.text(name)) != null;
511     }
512 
513     /**
514      * Selects a view by id.
515      */
selectByRelativeId(String id)516     public UiObject2 selectByRelativeId(String id) throws Exception {
517         return selectByRelativeId(mPackageName, id);
518     }
519 
selectByRelativeId(String packageName, String id)520     public UiObject2 selectByRelativeId(String packageName, String id) throws Exception {
521         Log.v(TAG, "selectByRelativeId(): " + packageName + ":/" + id);
522         UiObject2 object = waitForObject(By.res(packageName, id));
523         object.click();
524         return object;
525     }
526 
527     /**
528      * Asserts the id is shown on the screen.
529      */
assertShownById(String id)530     public UiObject2 assertShownById(String id) throws Exception {
531         final UiObject2 object = waitForObject(By.res(id));
532         assertThat(object).isNotNull();
533         return object;
534     }
535 
536     /**
537      * Asserts the id is shown on the screen, using a resource id from the test package.
538      */
assertShownByRelativeId(String id)539     public UiObject2 assertShownByRelativeId(String id) throws Exception {
540         return assertShownByRelativeId(id, mDefaultTimeout);
541     }
542 
assertShownByRelativeId(String id, Timeout timeout)543     public UiObject2 assertShownByRelativeId(String id, Timeout timeout) throws Exception {
544         final UiObject2 obj = waitForObject(By.res(mPackageName, id), timeout);
545         assertThat(obj).isNotNull();
546         return obj;
547     }
548 
549     /**
550      * Asserts the id is not shown on the screen anymore, using a resource id from the test package.
551      *
552      * <p><b>Note:</b> this method should only called AFTER the id was previously shown, otherwise
553      * it might pass without really asserting anything.
554      */
assertGoneByRelativeId(@onNull String id, @NonNull Timeout timeout)555     public void assertGoneByRelativeId(@NonNull String id, @NonNull Timeout timeout) {
556         assertGoneByRelativeId(/* parent = */ null, id, timeout);
557     }
558 
assertGoneByRelativeId(int resId, @NonNull Timeout timeout)559     public void assertGoneByRelativeId(int resId, @NonNull Timeout timeout) {
560         assertGoneByRelativeId(/* parent = */ null, getIdName(resId), timeout);
561     }
562 
getIdName(int resId)563     private String getIdName(int resId) {
564         return mContext.getResources().getResourceEntryName(resId);
565     }
566 
567     /**
568      * Asserts the id is not shown on the parent anymore, using a resource id from the test package.
569      *
570      * <p><b>Note:</b> this method should only called AFTER the id was previously shown, otherwise
571      * it might pass without really asserting anything.
572      */
assertGoneByRelativeId(@ullable UiObject2 parent, @NonNull String id, @NonNull Timeout timeout)573     public void assertGoneByRelativeId(@Nullable UiObject2 parent, @NonNull String id,
574             @NonNull Timeout timeout) {
575         final SearchCondition<Boolean> condition = Until.gone(By.res(mPackageName, id));
576         final boolean gone = parent != null
577                 ? parent.wait(condition, timeout.ms())
578                 : mDevice.wait(condition, timeout.ms());
579         if (!gone) {
580             final String message = "Object with id '" + id + "' should be gone after "
581                     + timeout + " ms";
582             dumpScreen(message);
583             throw new RetryableException(message);
584         }
585     }
586 
assertShownByRelativeId(int resId)587     public UiObject2 assertShownByRelativeId(int resId) throws Exception {
588         return assertShownByRelativeId(getIdName(resId));
589     }
590 
assertNeverShownByRelativeId(@onNull String description, int resId, long timeout)591     public void assertNeverShownByRelativeId(@NonNull String description, int resId, long timeout)
592             throws Exception {
593         final BySelector selector = By.res(Helper.MY_PACKAGE, getIdName(resId));
594         assertNeverShown(description, selector, timeout);
595     }
596 
597     /**
598      * Asserts that a {@code selector} is not showing after {@code timeout} milliseconds.
599      */
assertNeverShown(String description, BySelector selector, long timeout)600     protected void assertNeverShown(String description, BySelector selector, long timeout)
601             throws Exception {
602         SystemClock.sleep(timeout);
603         final UiObject2 object = mDevice.findObject(selector);
604         if (object != null) {
605             throw new AssertionError(
606                     String.format("Should not be showing %s after %dms, but got %s",
607                             description, timeout, getChildrenAsText(object)));
608         }
609     }
610 
611     /**
612      * Gets the text set on a view.
613      */
getTextByRelativeId(String id)614     public String getTextByRelativeId(String id) throws Exception {
615         return waitForObject(By.res(mPackageName, id)).getText();
616     }
617 
618     /**
619      * Focus in the view with the given resource id.
620      */
focusByRelativeId(String id)621     public void focusByRelativeId(String id) throws Exception {
622         waitForObject(By.res(mPackageName, id)).click();
623     }
624 
625     /**
626      * Sets a new text on a view.
627      */
setTextByRelativeId(String id, String newText)628     public void setTextByRelativeId(String id, String newText) throws Exception {
629         waitForObject(By.res(mPackageName, id)).setText(newText);
630     }
631 
632     /**
633      * Sets a new text on a view.
634      *
635      * <p><b>Note:</b> First clear the view to the first character of the old string, then clear to
636      * empty. This is to accommodate the fix for the bug where views are reset to empty, causing
637      * save dialog to not show. The fix for this bug is to ignore sudden resets to empty, therefore
638      * CTS tests simulating field clearing have to progressively clear the field instead of
639      * resetting to empty at once.
640      */
clearTextByRelativeId(String id)641     public void clearTextByRelativeId(String id) throws Exception {
642         final UiObject2 object = waitForObject(By.res(mPackageName, id));
643         String oldText = object.getText();
644         if (!oldText.isEmpty()) {
645             object.setText(String.valueOf(oldText.charAt(0)));
646             object.setText("");
647         }
648     }
649 
650     /**
651      * Asserts the save snackbar is showing and returns it.
652      */
assertSaveShowing(int type)653     public UiObject2 assertSaveShowing(int type) throws Exception {
654         return assertSaveShowing(SAVE_TIMEOUT, type);
655     }
656 
657     /**
658      * Asserts the save snackbar is showing with a custom service name and returns it.
659      */
assertSaveShowingWithCustomServiceName(int type, String customServiceName)660     public UiObject2 assertSaveShowingWithCustomServiceName(int type, String customServiceName)
661             throws Exception {
662         return assertSaveOrUpdateShowing(/* update= */ false, SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL,
663                 SaveInfo.POSITIVE_BUTTON_STYLE_SAVE, null, SAVE_TIMEOUT, customServiceName, type);
664     }
665 
666     /**
667      * Asserts the save snackbar is showing and returns it.
668      */
assertSaveShowing(Timeout timeout, int type)669     public UiObject2 assertSaveShowing(Timeout timeout, int type) throws Exception {
670         return assertSaveShowing(null, timeout, type);
671     }
672 
673     /**
674      * Asserts the save snackbar is showing with the Update message and returns it.
675      */
assertUpdateShowing(int... types)676     public UiObject2 assertUpdateShowing(int... types) throws Exception {
677         return assertSaveOrUpdateShowing(/* update= */ true, SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL,
678                 null, SAVE_TIMEOUT, types);
679     }
680 
681     /**
682      * Presses the Back button.
683      */
pressBack()684     public void pressBack() {
685         Log.d(TAG, "pressBack()");
686         mDevice.pressBack();
687     }
688 
689     /**
690      * Presses the Home button.
691      */
pressHome()692     public void pressHome() {
693         Log.d(TAG, "pressHome()");
694         mDevice.pressHome();
695     }
696 
697     /**
698      * Asserts the save snackbar is not showing.
699      */
assertSaveNotShowing(int type)700     public void assertSaveNotShowing(int type) throws Exception {
701         assertNeverShown("save UI for type " + saveTypeToString(type), SAVE_UI_SELECTOR,
702                 SAVE_NOT_SHOWN_NAPTIME_MS);
703     }
704 
705     /**
706      * Asserts the save snackbar is not showing, explaining when.
707      */
assertSaveNotShowing(int type, @Nullable String when)708     public void assertSaveNotShowing(int type, @Nullable String when) throws Exception {
709         String suffix = when == null ? "" : " when " + when;
710         assertNeverShown("save UI for type " + saveTypeToString(type) + suffix, SAVE_UI_SELECTOR,
711                 SAVE_NOT_SHOWN_NAPTIME_MS);
712     }
713 
assertSaveNotShowing()714     public void assertSaveNotShowing() throws Exception {
715         assertNeverShown("save UI", SAVE_UI_SELECTOR, SAVE_NOT_SHOWN_NAPTIME_MS);
716     }
717 
getSaveTypeString(int type)718     private String getSaveTypeString(int type) {
719         final String typeResourceName;
720         switch (type) {
721             case SAVE_DATA_TYPE_PASSWORD:
722                 typeResourceName = RESOURCE_STRING_SAVE_TYPE_PASSWORD;
723                 break;
724             case SAVE_DATA_TYPE_ADDRESS:
725                 typeResourceName = RESOURCE_STRING_SAVE_TYPE_ADDRESS;
726                 break;
727             case SAVE_DATA_TYPE_CREDIT_CARD:
728                 typeResourceName = RESOURCE_STRING_SAVE_TYPE_CREDIT_CARD;
729                 break;
730             case SAVE_DATA_TYPE_USERNAME:
731                 typeResourceName = RESOURCE_STRING_SAVE_TYPE_USERNAME;
732                 break;
733             case SAVE_DATA_TYPE_EMAIL_ADDRESS:
734                 typeResourceName = RESOURCE_STRING_SAVE_TYPE_EMAIL_ADDRESS;
735                 break;
736             case SAVE_DATA_TYPE_DEBIT_CARD:
737                 typeResourceName = RESOURCE_STRING_SAVE_TYPE_DEBIT_CARD;
738                 break;
739             case SAVE_DATA_TYPE_PAYMENT_CARD:
740                 typeResourceName = RESOURCE_STRING_SAVE_TYPE_PAYMENT_CARD;
741                 break;
742             case SAVE_DATA_TYPE_GENERIC_CARD:
743                 typeResourceName = RESOURCE_STRING_SAVE_TYPE_GENERIC_CARD;
744                 break;
745             default:
746                 throw new IllegalArgumentException("Unsupported type: " + type);
747         }
748         return getString(typeResourceName);
749     }
750 
saveTypeToString(int type)751     private String saveTypeToString(int type) {
752         // Cannot use DebugUtils, it's @hide
753         switch (type) {
754             case SAVE_DATA_TYPE_PASSWORD:
755                 return "PASSWORD";
756             case SAVE_DATA_TYPE_ADDRESS:
757                 return "ADDRESS";
758             case SAVE_DATA_TYPE_CREDIT_CARD:
759                 return "CREDIT_CARD";
760             case SAVE_DATA_TYPE_USERNAME:
761                 return "USERNAME";
762             case SAVE_DATA_TYPE_EMAIL_ADDRESS:
763                 return "EMAIL_ADDRESS";
764             case SAVE_DATA_TYPE_DEBIT_CARD:
765                 return "DEBIT_CARD";
766             case SAVE_DATA_TYPE_PAYMENT_CARD:
767                 return "PAYMENT_CARD";
768             case SAVE_DATA_TYPE_GENERIC_CARD:
769                 return "GENERIC_CARD";
770             default:
771                 return "UNSUPPORT_TYPE_" + type;
772         }
773     }
774 
assertSaveShowing(String description, int... types)775     public UiObject2 assertSaveShowing(String description, int... types) throws Exception {
776         return assertSaveOrUpdateShowing(/* update= */ false, SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL,
777                 description, SAVE_TIMEOUT, types);
778     }
779 
assertSaveShowing(String description, Timeout timeout, int... types)780     public UiObject2 assertSaveShowing(String description, Timeout timeout, int... types)
781             throws Exception {
782         return assertSaveOrUpdateShowing(/* update= */ false, SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL,
783                 description, timeout, types);
784     }
785 
assertSaveShowing(int negativeButtonStyle, String description, int... types)786     public UiObject2 assertSaveShowing(int negativeButtonStyle, String description,
787             int... types) throws Exception {
788         return assertSaveOrUpdateShowing(/* update= */ false, negativeButtonStyle, description,
789                 SAVE_TIMEOUT, types);
790     }
791 
assertSaveShowing(int positiveButtonStyle, int... types)792     public UiObject2 assertSaveShowing(int positiveButtonStyle, int... types) throws Exception {
793         return assertSaveOrUpdateShowing(/* update= */ false, SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL,
794                 positiveButtonStyle, /* description= */ null, SAVE_TIMEOUT, types);
795     }
796 
assertSaveOrUpdateShowing(boolean update, int negativeButtonStyle, String description, Timeout timeout, int... types)797     public UiObject2 assertSaveOrUpdateShowing(boolean update, int negativeButtonStyle,
798             String description, Timeout timeout, int... types) throws Exception {
799         return assertSaveOrUpdateShowing(update, negativeButtonStyle,
800                 SaveInfo.POSITIVE_BUTTON_STYLE_SAVE, description, timeout, types);
801     }
802 
assertSaveOrUpdateShowing(boolean update, int negativeButtonStyle, int positiveButtonStyle, String description, Timeout timeout, int... types)803     public UiObject2 assertSaveOrUpdateShowing(boolean update, int negativeButtonStyle,
804             int positiveButtonStyle, String description, Timeout timeout, int... types)
805             throws Exception {
806         return assertSaveOrUpdateShowing(update, negativeButtonStyle, positiveButtonStyle,
807             description, timeout, InstrumentedAutoFillService.getServiceLabel(), types);
808     }
809 
assertSaveOrUpdateShowing(boolean update, int negativeButtonStyle, int positiveButtonStyle, String description, Timeout timeout, String serviceLabel, int... types)810     public UiObject2 assertSaveOrUpdateShowing(boolean update, int negativeButtonStyle,
811             int positiveButtonStyle, String description, Timeout timeout, String serviceLabel,
812             int... types) throws Exception {
813 
814         final UiObject2 snackbar = waitForObject(SAVE_UI_SELECTOR, timeout);
815 
816         final UiObject2 titleView =
817                 waitForObject(snackbar, By.res("android", RESOURCE_ID_SAVE_TITLE), timeout);
818         assertWithMessage("save title (%s) is not shown", RESOURCE_ID_SAVE_TITLE).that(titleView)
819                 .isNotNull();
820 
821         final UiObject2 iconView =
822                 waitForObject(snackbar, By.res("android", RESOURCE_ID_SAVE_ICON), timeout);
823         assertWithMessage("save icon (%s) is not shown", RESOURCE_ID_SAVE_ICON).that(iconView)
824                 .isNotNull();
825 
826         final String actualTitle = titleView.getText();
827         Log.d(TAG, "save title: " + actualTitle);
828 
829         final String titleId, titleWithTypeId;
830         if (update) {
831             titleId = RESOURCE_STRING_UPDATE_TITLE;
832             titleWithTypeId = RESOURCE_STRING_UPDATE_TITLE_WITH_TYPE;
833         } else {
834             titleId = RESOURCE_STRING_SAVE_TITLE;
835             titleWithTypeId = RESOURCE_STRING_SAVE_TITLE_WITH_TYPE;
836         }
837 
838         switch (types.length) {
839             case 1:
840                 final String expectedTitle = (types[0] == SAVE_DATA_TYPE_GENERIC)
841                         ? Html.fromHtml(getString(titleId, serviceLabel), 0).toString()
842                         : Html.fromHtml(getString(titleWithTypeId,
843                                 getSaveTypeString(types[0]), serviceLabel), 0).toString();
844                 assertThat(actualTitle).isEqualTo(expectedTitle);
845                 break;
846             case 2:
847                 // We cannot predict the order...
848                 assertThat(actualTitle).contains(getSaveTypeString(types[0]));
849                 assertThat(actualTitle).contains(getSaveTypeString(types[1]));
850                 break;
851             case 3:
852                 // We cannot predict the order...
853                 assertThat(actualTitle).contains(getSaveTypeString(types[0]));
854                 assertThat(actualTitle).contains(getSaveTypeString(types[1]));
855                 assertThat(actualTitle).contains(getSaveTypeString(types[2]));
856                 break;
857             default:
858                 throw new IllegalArgumentException("Invalid types: " + Arrays.toString(types));
859         }
860 
861         if (description != null) {
862             final UiObject2 saveSubTitle = snackbar.findObject(By.text(description));
863             assertWithMessage("save subtitle(%s)", description).that(saveSubTitle).isNotNull();
864         }
865 
866         final String positiveButtonStringId;
867         switch (positiveButtonStyle) {
868             case SaveInfo.POSITIVE_BUTTON_STYLE_CONTINUE:
869                 positiveButtonStringId = RESOURCE_STRING_CONTINUE_BUTTON_YES;
870                 break;
871             default:
872                 positiveButtonStringId = update ? RESOURCE_STRING_UPDATE_BUTTON_YES
873                         : RESOURCE_STRING_SAVE_BUTTON_YES;
874         }
875         final String expectedPositiveButtonText = getString(positiveButtonStringId).toUpperCase();
876         final UiObject2 positiveButton = waitForObject(snackbar,
877                 By.res("android", RESOURCE_ID_SAVE_BUTTON_YES), timeout);
878         assertWithMessage("wrong text on positive button")
879                 .that(positiveButton.getText().toUpperCase()).isEqualTo(expectedPositiveButtonText);
880 
881         final String negativeButtonStringId;
882         if (negativeButtonStyle == SaveInfo.NEGATIVE_BUTTON_STYLE_REJECT) {
883             negativeButtonStringId = RESOURCE_STRING_SAVE_BUTTON_NOT_NOW;
884         } else if (negativeButtonStyle == SaveInfo.NEGATIVE_BUTTON_STYLE_NEVER) {
885             negativeButtonStringId = RESOURCE_STRING_SAVE_BUTTON_NEVER;
886         } else {
887             negativeButtonStringId = RESOURCE_STRING_SAVE_BUTTON_NO_THANKS;
888         }
889         final String expectedNegativeButtonText = getString(negativeButtonStringId).toUpperCase();
890         final UiObject2 negativeButton = waitForObject(snackbar,
891                 By.res("android", RESOURCE_ID_SAVE_BUTTON_NO), timeout);
892         assertWithMessage("wrong text on negative button")
893                 .that(negativeButton.getText().toUpperCase()).isEqualTo(expectedNegativeButtonText);
894 
895         final String expectedAccessibilityTitle =
896                 getString(RESOURCE_STRING_SAVE_SNACKBAR_ACCESSIBILITY_TITLE);
897         timeout.run(
898                 String.format(
899                         "assertAccessibilityTitle(%s, %s)",
900                         snackbar,
901                         expectedAccessibilityTitle),
902                 () -> {
903                     try {
904                         assertAccessibilityTitle(snackbar, expectedAccessibilityTitle);
905                     } catch (RetryableException e) {
906                         return null;
907                     }
908                     return true;
909                 });
910         return snackbar;
911     }
912 
913     /**
914      * Taps an option in the save snackbar.
915      *
916      * @param yesDoIt {@code true} for 'YES', {@code false} for 'NO THANKS'.
917      * @param types expected types of save info.
918      */
saveForAutofill(boolean yesDoIt, int... types)919     public void saveForAutofill(boolean yesDoIt, int... types) throws Exception {
920         final UiObject2 saveSnackBar = assertSaveShowing(
921                 SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, null, types);
922         saveForAutofill(saveSnackBar, yesDoIt);
923     }
924 
updateForAutofill(boolean yesDoIt, int... types)925     public void updateForAutofill(boolean yesDoIt, int... types) throws Exception {
926         final UiObject2 saveUi = assertUpdateShowing(types);
927         saveForAutofill(saveUi, yesDoIt);
928     }
929 
930     /**
931      * Taps an option in the save snackbar.
932      *
933      * @param yesDoIt {@code true} for 'YES', {@code false} for 'NO THANKS'.
934      * @param types expected types of save info.
935      */
saveForAutofill(int negativeButtonStyle, boolean yesDoIt, int... types)936     public void saveForAutofill(int negativeButtonStyle, boolean yesDoIt, int... types)
937             throws Exception {
938         final UiObject2 saveSnackBar = assertSaveShowing(negativeButtonStyle, null, types);
939         saveForAutofill(saveSnackBar, yesDoIt);
940     }
941 
942     /**
943      * Taps the positive button in the save snackbar.
944      *
945      * @param types expected types of save info.
946      */
saveForAutofill(int positiveButtonStyle, int... types)947     public void saveForAutofill(int positiveButtonStyle, int... types) throws Exception {
948         final UiObject2 saveSnackBar = assertSaveShowing(positiveButtonStyle, types);
949         saveForAutofill(saveSnackBar, /* yesDoIt= */ true);
950     }
951 
952     /**
953      * Taps an option in the save snackbar.
954      *
955      * @param saveSnackBar Save snackbar, typically obtained through
956      *            {@link #assertSaveShowing(int)}.
957      * @param yesDoIt {@code true} for 'YES', {@code false} for 'NO THANKS'.
958      */
saveForAutofill(UiObject2 saveSnackBar, boolean yesDoIt)959     public void saveForAutofill(UiObject2 saveSnackBar, boolean yesDoIt) {
960         final String id = yesDoIt ? "autofill_save_yes" : "autofill_save_no";
961 
962         final UiObject2 button = saveSnackBar.findObject(By.res("android", id));
963         assertWithMessage("save button (%s)", id).that(button).isNotNull();
964         button.click();
965     }
966 
967     /**
968      * Gets the AUTOFILL contextual menu by long pressing a text field.
969      *
970      * <p><b>NOTE:</b> this method should only be called in scenarios where we explicitly want to
971      * test the overflow menu. For all other scenarios where we want to test manual autofill, it's
972      * better to call {@code AFM.requestAutofill()} directly, because it's less error-prone and
973      * faster.
974      *
975      * @param id resource id of the field.
976      */
getAutofillMenuOption(String id)977     public UiObject2 getAutofillMenuOption(String id) throws Exception {
978         final UiObject2 field = waitForObject(By.res(mPackageName, id));
979         // TODO: figure out why obj.longClick() doesn't always work
980         field.click(LONG_PRESS_MS);
981 
982         List<UiObject2> menuItems = waitForObjects(
983                 By.res("android", RESOURCE_ID_CONTEXT_MENUITEM), mDefaultTimeout);
984         final String expectedText = getAutofillContextualMenuTitle();
985 
986         final StringBuffer menuNames = new StringBuffer();
987 
988         // Check first menu for AUTOFILL
989         for (UiObject2 menuItem : menuItems) {
990             final String menuName = menuItem.getText();
991             if (menuName.equalsIgnoreCase(expectedText)) {
992                 Log.v(TAG, "AUTOFILL found in first menu");
993                 return menuItem;
994             }
995             menuNames.append("'").append(menuName).append("' ");
996         }
997 
998         menuNames.append(";");
999 
1000         // First menu does not have AUTOFILL, check overflow
1001         final BySelector overflowSelector = By.res("android", RESOURCE_ID_OVERFLOW);
1002 
1003         // Click overflow menu button.
1004         final UiObject2 overflowMenu = waitForObject(overflowSelector, mDefaultTimeout);
1005         overflowMenu.click();
1006 
1007         // Wait for overflow menu to show.
1008         mDevice.wait(Until.gone(overflowSelector), 1000);
1009 
1010         menuItems = waitForObjects(
1011                 By.res("android", RESOURCE_ID_CONTEXT_MENUITEM), mDefaultTimeout);
1012         for (UiObject2 menuItem : menuItems) {
1013             final String menuName = menuItem.getText();
1014             if (menuName.equalsIgnoreCase(expectedText)) {
1015                 Log.v(TAG, "AUTOFILL found in overflow menu");
1016                 return menuItem;
1017             }
1018             menuNames.append("'").append(menuName).append("' ");
1019         }
1020         throw new RetryableException("no '%s' on '%s'", expectedText, menuNames);
1021     }
1022 
getAutofillContextualMenuTitle()1023     String getAutofillContextualMenuTitle() {
1024         return getString(RESOURCE_STRING_AUTOFILL);
1025     }
1026 
1027     /**
1028      * Gets a string from the Android resources.
1029      */
getString(String id)1030     private String getString(String id) {
1031         final Resources resources = mContext.getResources();
1032         final int stringId = resources.getIdentifier(id, "string", "android");
1033         try {
1034             return resources.getString(stringId);
1035         } catch (Resources.NotFoundException e) {
1036             throw new IllegalStateException("no internal string for '" + id + "' / res=" + stringId
1037                     + ": ", e);
1038         }
1039     }
1040 
1041     /**
1042      * Gets a string from the Android resources.
1043      */
getString(String id, Object... formatArgs)1044     private String getString(String id, Object... formatArgs) {
1045         final Resources resources = mContext.getResources();
1046         final int stringId = resources.getIdentifier(id, "string", "android");
1047         try {
1048             return resources.getString(stringId, formatArgs);
1049         } catch (Resources.NotFoundException e) {
1050             throw new IllegalStateException("no internal string for '" + id + "' / res=" + stringId
1051                     + ": ", e);
1052         }
1053     }
1054 
1055     /**
1056      * Waits for and returns an object.
1057      *
1058      * @param selector {@link BySelector} that identifies the object.
1059      */
waitForObject(BySelector selector)1060     private UiObject2 waitForObject(BySelector selector) throws Exception {
1061         return waitForObject(selector, mDefaultTimeout);
1062     }
1063 
1064     /**
1065      * Waits for and returns an object.
1066      *
1067      * @param parent where to find the object (or {@code null} to use device's root).
1068      * @param selector {@link BySelector} that identifies the object.
1069      * @param timeout timeout in ms.
1070      * @param dumpOnError whether the window hierarchy should be dumped if the object is not found.
1071      */
waitForObject(UiObject2 parent, BySelector selector, Timeout timeout, boolean dumpOnError)1072     private UiObject2 waitForObject(UiObject2 parent, BySelector selector, Timeout timeout,
1073             boolean dumpOnError) throws Exception {
1074         // NOTE: mDevice.wait does not work for the save snackbar, so we need a polling approach.
1075         try {
1076             return timeout.run("waitForObject(" + selector + ")", () -> {
1077                 return parent != null
1078                         ? parent.findObject(selector)
1079                         : mDevice.findObject(selector);
1080 
1081             });
1082         } catch (RetryableException e) {
1083             if (dumpOnError) {
1084                 dumpScreen("waitForObject() for " + selector + "on "
1085                         + (parent == null ? "mDevice" : parent) + " failed");
1086             }
1087             throw e;
1088         }
1089     }
1090 
waitForObject(@ullable UiObject2 parent, @NonNull BySelector selector, @NonNull Timeout timeout)1091     public UiObject2 waitForObject(@Nullable UiObject2 parent, @NonNull BySelector selector,
1092             @NonNull Timeout timeout)
1093             throws Exception {
1094         return waitForObject(parent, selector, timeout, DUMP_ON_ERROR);
1095     }
1096 
1097     /**
1098      * Waits for and returns an object.
1099      *
1100      * @param selector {@link BySelector} that identifies the object.
1101      * @param timeout timeout in ms
1102      */
waitForObject(@onNull BySelector selector, @NonNull Timeout timeout)1103     protected UiObject2 waitForObject(@NonNull BySelector selector, @NonNull Timeout timeout)
1104             throws Exception {
1105         return waitForObject(/* parent= */ null, selector, timeout);
1106     }
1107 
1108     /**
1109      * Waits for and returns a child from a parent {@link UiObject2}.
1110      */
assertChildText(UiObject2 parent, String resourceId, String expectedText)1111     public UiObject2 assertChildText(UiObject2 parent, String resourceId, String expectedText)
1112             throws Exception {
1113         final UiObject2 child = waitForObject(parent, By.res(mPackageName, resourceId),
1114                 Timeouts.UI_TIMEOUT);
1115         assertWithMessage("wrong text for view '%s'", resourceId).that(child.getText())
1116                 .isEqualTo(expectedText);
1117         return child;
1118     }
1119 
1120     /**
1121      * Execute a Runnable and wait for {@link AccessibilityEvent#TYPE_WINDOWS_CHANGED} or
1122      * {@link AccessibilityEvent#TYPE_WINDOW_STATE_CHANGED}.
1123      */
waitForWindowChange(Runnable runnable, long timeoutMillis)1124     public AccessibilityEvent waitForWindowChange(Runnable runnable, long timeoutMillis) {
1125         try {
1126             return mAutoman.executeAndWaitForEvent(runnable, (AccessibilityEvent event) -> {
1127                 switch (event.getEventType()) {
1128                     case AccessibilityEvent.TYPE_WINDOWS_CHANGED:
1129                     case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
1130                         return true;
1131                     default:
1132                         Log.v(TAG, "waitForWindowChange(): ignoring event " + event);
1133                 }
1134                 return false;
1135             }, timeoutMillis);
1136         } catch (TimeoutException e) {
1137             throw new WindowChangeTimeoutException(e, timeoutMillis);
1138         }
1139     }
1140 
1141     public AccessibilityEvent waitForWindowChange(Runnable runnable) {
1142         return waitForWindowChange(runnable, Timeouts.WINDOW_CHANGE_TIMEOUT_MS);
1143     }
1144 
1145     /**
1146      * Waits for and returns a list of objects.
1147      *
1148      * @param selector {@link BySelector} that identifies the object.
1149      * @param timeout timeout in ms
1150      */
1151     private List<UiObject2> waitForObjects(BySelector selector, Timeout timeout) throws Exception {
1152         // NOTE: mDevice.wait does not work for the save snackbar, so we need a polling approach.
1153         try {
1154             return timeout.run("waitForObject(" + selector + ")", () -> {
1155                 final List<UiObject2> uiObjects = mDevice.findObjects(selector);
1156                 if (uiObjects != null && !uiObjects.isEmpty()) {
1157                     return uiObjects;
1158                 }
1159                 return null;
1160 
1161             });
1162 
1163         } catch (RetryableException e) {
1164             dumpScreen("waitForObjects() for " + selector + "failed");
1165             throw e;
1166         }
1167     }
1168 
1169     private UiObject2 findDatasetPicker(Timeout timeout) throws Exception {
1170         // The UI element here is flaky. Sometimes the UI automator returns a StateObject.
1171         // Retry is put in place here to make sure that we catch the object.
1172         UiObject2 picker = null;
1173         int retryCount = 0;
1174         final String expectedTitle = getString(RESOURCE_STRING_DATASET_PICKER_ACCESSIBILITY_TITLE);
1175         while (retryCount < MAX_UIOBJECT_RETRY_COUNT) {
1176             try {
1177                 picker = waitForObject(DATASET_PICKER_SELECTOR, timeout);
1178                 assertAccessibilityTitle(picker, expectedTitle);
1179                 break;
1180             } catch (StaleObjectException e) {
1181                 Log.d(TAG, "Retry grabbing view class");
1182             }
1183             retryCount++;
1184         }
1185         assertWithMessage(expectedTitle + " not found").that(retryCount).isLessThan(
1186                 MAX_UIOBJECT_RETRY_COUNT);
1187 
1188         if (picker != null) {
1189             mOkToCallAssertNoDatasets = true;
1190         }
1191 
1192         return picker;
1193     }
1194 
1195     /**
1196      * Asserts a given object has the expected accessibility title.
1197      */
1198     private void assertAccessibilityTitle(UiObject2 object, String expectedTitle) {
1199         // TODO: ideally it should get the AccessibilityWindowInfo from the object, but UiAutomator
1200         // does not expose that.
1201         for (AccessibilityWindowInfo window : mAutoman.getWindows()) {
1202             final CharSequence title = window.getTitle();
1203             Log.d(TAG, "assertAccessibilityTitle(): found title =" + title + ", expected title="
1204                     + expectedTitle);
1205             if (title != null && title.toString().equals(expectedTitle)) {
1206                 return;
1207             }
1208         }
1209         throw new RetryableException("Title '%s' not found for %s", expectedTitle, object);
1210     }
1211 
1212     /**
1213      * Trys to set the orientation, if possible. No-op if the device does not support rotation
1214      *
1215      * @return True if the device orientation matches the requested orientation, else false
1216      */
1217     public boolean maybeSetScreenOrientation(int orientation) {
1218         mAutoman.setRotation(orientation);
1219         final int currentRotation =
1220                 InstrumentationRegistry.getInstrumentation()
1221                         .getContext()
1222                         .getSystemService(DisplayManager.class)
1223                         .getDisplay(Display.DEFAULT_DISPLAY)
1224                         .getRotation();
1225         return orientation == currentRotation;
1226     }
1227 
1228     /**
1229      * Sets the screen orientation.
1230      *
1231      * @param orientation typically {@link #LANDSCAPE} or {@link #PORTRAIT}.
1232      * @throws org.junit.AssumptionViolatedException if device does not support rotation.
1233      * @throws RetryableException if value didn't change.
1234      */
1235     public void setScreenOrientation(int orientation) throws Exception {
1236         assumeTrue("Rotation is supported", Helper.isRotationSupported(mContext));
1237 
1238         // Use the platform API instead of mDevice.getDisplayRotation(), which is slow due to
1239         // waitForIdle(). waitForIdle() is not needed here because in AutoFillServiceTestCase we
1240         // always use UiBot#setScreenOrientation() to change the screen rotation, which blocks until
1241         // new rotation is reflected on the device.
1242         final int currentRotation = InstrumentationRegistry.getInstrumentation().getContext()
1243                 .getSystemService(DisplayManager.class).getDisplay(Display.DEFAULT_DISPLAY)
1244                 .getRotation();
1245         mAutoman.setRotation(orientation);
1246 
1247         if (orientation == currentRotation) {
1248             // Just need to freeze the rotation.
1249             return;
1250         }
1251 
1252         UI_SCREEN_ORIENTATION_TIMEOUT.run("setScreenOrientation(" + orientation + ")", () ->
1253                 mDevice.getDisplayRotation() == orientation ? Boolean.TRUE : null);
1254     }
1255 
1256     /**
1257      * Gets the value of the screen orientation.
1258      *
1259      * @return typically {@link #LANDSCAPE} or {@link #PORTRAIT}.
1260      */
1261     public int getScreenOrientation() {
1262         return mDevice.getDisplayRotation();
1263     }
1264 
1265     /**
1266      * Dumps the current view hierarchy and take a screenshot and save both locally so they can be
1267      * inspected later.
1268      */
1269     public void dumpScreen(@NonNull String cause) {
1270         try {
1271             final File file = Helper.createTestFile("hierarchy.xml");
1272             if (file == null) return;
1273             Log.w(TAG, "Dumping window hierarchy because " + cause + " on " + file);
1274             try (FileInputStream fis = new FileInputStream(file)) {
1275                 mDevice.dumpWindowHierarchy(file);
1276             }
1277         } catch (Exception e) {
1278             Log.e(TAG, "error dumping screen on " + cause, e);
1279         } finally {
1280             takeScreenshotAndSave();
1281         }
1282     }
1283 
1284     private Rect cropScreenshotWithoutScreenDecoration(Activity activity) {
1285         final WindowInsets[] inset = new WindowInsets[1];
1286         final View[] rootView = new View[1];
1287 
1288         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
1289             rootView[0] = activity.getWindow().getDecorView();
1290             inset[0] = rootView[0].getRootWindowInsets();
1291         });
1292         final int navBarHeight = inset[0].getStableInsetBottom();
1293         final int statusBarHeight = inset[0].getStableInsetTop();
1294 
1295         return new Rect(0, statusBarHeight, rootView[0].getWidth(),
1296                 rootView[0].getHeight() - navBarHeight - statusBarHeight);
1297     }
1298 
1299     // TODO(b/74358143): ideally we should take a screenshot limited by the boundaries of the
1300     // activity window, so external elements (such as the clock) are filtered out and don't cause
1301     // test flakiness when the contents are compared.
1302     public Bitmap takeScreenshot() {
1303         return takeScreenshotWithRect(null);
1304     }
1305 
1306     public Bitmap takeScreenshot(@NonNull Activity activity) {
1307         // crop the screenshot without screen decoration to prevent test flakiness.
1308         final Rect rect = cropScreenshotWithoutScreenDecoration(activity);
1309         return takeScreenshotWithRect(rect);
1310     }
1311 
1312     private Bitmap takeScreenshotWithRect(@Nullable Rect r) {
1313         final long before = SystemClock.elapsedRealtime();
1314         final Bitmap bitmap = mAutoman.takeScreenshot();
1315         final long delta = SystemClock.elapsedRealtime() - before;
1316         Log.v(TAG, "Screenshot taken in " + delta + "ms");
1317         if (r == null) {
1318             return bitmap;
1319         }
1320         try {
1321             return Bitmap.createBitmap(bitmap, r.left, r.top, r.right, r.bottom);
1322         } finally {
1323             if (bitmap != null) {
1324                 bitmap.recycle();
1325             }
1326         }
1327     }
1328 
1329     /**
1330      * Takes a screenshot and save it in the file system for post-mortem analysis.
1331      */
1332     public void takeScreenshotAndSave() {
1333         File file = null;
1334         try {
1335             file = Helper.createTestFile("screenshot.png");
1336             if (file != null) {
1337                 Log.i(TAG, "Taking screenshot on " + file);
1338                 final Bitmap screenshot = takeScreenshot();
1339                 Helper.dumpBitmap(screenshot, file);
1340             }
1341         } catch (Exception e) {
1342             Log.e(TAG, "Error taking screenshot and saving on " + file, e);
1343         }
1344     }
1345 
1346     /**
1347      * Asserts the contents of a child element.
1348      *
1349      * @param parent parent object
1350      * @param childId (relative) resource id of the child
1351      * @param assertion if {@code null}, asserts the child does not exist; otherwise, asserts the
1352      * child with it.
1353      */
1354     public void assertChild(@NonNull UiObject2 parent, @NonNull String childId,
1355             @Nullable Visitor<UiObject2> assertion) {
1356         final UiObject2 child = parent.findObject(By.res(mPackageName, childId));
1357         try {
1358             if (assertion != null) {
1359                 assertWithMessage("Didn't find child with id '%s'", childId).that(child)
1360                         .isNotNull();
1361                 try {
1362                     assertion.visit(child);
1363                 } catch (Throwable t) {
1364                     throw new AssertionError("Error on child '" + childId + "'", t);
1365                 }
1366             } else {
1367                 assertWithMessage("Shouldn't find child with id '%s'", childId).that(child)
1368                         .isNull();
1369             }
1370         } catch (RuntimeException | Error e) {
1371             dumpScreen("assertChild(" + childId + ") failed: " + e);
1372             throw e;
1373         }
1374     }
1375 
1376     /**
1377      * Finds the first {@link URLSpan} on the current screen.
1378      */
1379     public URLSpan findFirstUrlSpanWithText(String str) throws Exception {
1380         final List<AccessibilityNodeInfo> list = mAutoman.getRootInActiveWindow()
1381                 .findAccessibilityNodeInfosByText(str);
1382         if (list.isEmpty()) {
1383             throw new AssertionError("Didn't found AccessibilityNodeInfo with " + str);
1384         }
1385 
1386         final AccessibilityNodeInfo text = list.get(0);
1387         final CharSequence accessibilityTextWithSpan = text.getText();
1388         if (!(accessibilityTextWithSpan instanceof Spanned)) {
1389             throw new AssertionError("\"" + text.getViewIdResourceName() + "\" was not a Spanned");
1390         }
1391 
1392         final URLSpan[] spans = ((Spanned) accessibilityTextWithSpan)
1393                 .getSpans(0, accessibilityTextWithSpan.length(), URLSpan.class);
1394         return spans[0];
1395     }
1396 
1397     public boolean scrollToTextObject(String text) {
1398         UiScrollable scroller = new UiScrollable(new UiSelector().scrollable(true));
1399         try {
1400             // Swipe far away from the edges to avoid triggering navigation gestures
1401             scroller.setSwipeDeadZonePercentage(0.25);
1402             return scroller.scrollTextIntoView(text);
1403         } catch (UiObjectNotFoundException e) {
1404             return false;
1405         }
1406     }
1407 
1408     /**
1409      * Asserts the header in the fill dialog.
1410      */
1411     public void assertFillDialogHeader(String expectedHeader) throws Exception {
1412         final UiObject2 header = findFillDialogHeaderPicker();
1413 
1414         assertWithMessage("wrong header for fill dialog")
1415                 .that(getChildrenAsText(header))
1416                 .containsExactlyElementsIn(Arrays.asList(expectedHeader)).inOrder();
1417     }
1418 
1419     /**
1420      * Asserts reject button in the fill dialog.
1421      */
1422     public void assertFillDialogRejectButton() throws Exception {
1423         final UiObject2 picker = findFillDialogPicker();
1424 
1425         // "No thanks" button shown
1426         final UiObject2 rejectButton = picker.findObject(
1427                 By.res("android", RESOURCE_ID_FILL_DIALOG_BUTTON_NO));
1428         assertWithMessage("No reject button in fill dialog")
1429                 .that(rejectButton).isNotNull();
1430         assertWithMessage("wrong text on reject button")
1431                 .that(rejectButton.getText().toUpperCase()).isEqualTo(
1432                         getString(RESOURCE_STRING_SAVE_BUTTON_NO_THANKS).toUpperCase());
1433     }
1434 
1435     /**
1436      * Asserts accept button in the fill dialog.
1437      */
1438     public void assertFillDialogAcceptButton() throws Exception {
1439         final UiObject2 picker = findFillDialogPicker();
1440 
1441         // "Continue" button shown
1442         final UiObject2 acceptButton = picker.findObject(
1443                 By.res("android", RESOURCE_ID_FILL_DIALOG_BUTTON_YES));
1444         assertWithMessage("No accept button in fill dialog")
1445                 .that(acceptButton).isNotNull();
1446         assertWithMessage("wrong text on accept button")
1447                 .that(acceptButton.getText().toUpperCase()).isEqualTo(
1448                         getString(RESOURCE_STRING_CONTINUE_BUTTON_YES).toUpperCase());
1449     }
1450 
1451     /**
1452      * Asserts there is no accept button in the fill dialog.
1453      */
1454     public void assertFillDialogNoAcceptButton() throws Exception {
1455         final UiObject2 picker = findFillDialogPicker();
1456 
1457         // "Continue" button not shown
1458         final UiObject2 acceptButton = picker.findObject(
1459                 By.res("android", RESOURCE_ID_FILL_DIALOG_BUTTON_YES));
1460         assertWithMessage("wrong accept button in fill dialog")
1461                 .that(acceptButton).isNull();
1462     }
1463 
1464     /**
1465      * Asserts the fill dialog is shown and contains the given datasets.
1466      *
1467      * @return the dataset picker object.
1468      */
1469     public UiObject2 assertFillDialogDatasets(String... datasets) throws Exception {
1470         final UiObject2 picker = findFillDialogDatasetPicker();
1471 
1472         assertWithMessage("wrong elements in fill dialog")
1473                 .that(getChildrenAsText(picker))
1474                 .containsExactlyElementsIn(datasets).inOrder();
1475         return picker;
1476     }
1477 
1478     /**
1479      * Asserts the fill dialog is shown and contains the given dataset. And then select the dataset
1480      */
1481     public void selectFillDialogDataset(String dataset) throws Exception {
1482         final UiObject2 picker = assertFillDialogDatasets(dataset);
1483         selectDataset(picker, dataset);
1484     }
1485 
1486     /**
1487      * Touch outside the fill dialog.
1488      */
1489     public void touchOutsideDialog() throws Exception {
1490         Log.v(TAG, "touchOutsideDialog()");
1491         final UiObject2 picker = findFillDialogPicker();
1492         final Rect bounds = picker.getVisibleBounds();
1493         assertThat(injectClick(new Point(bounds.left, bounds.top / 2))).isTrue();
1494     }
1495 
1496     /**
1497      * Touch outside the fill dialog.
1498      */
1499     public void touchOutsideSaveDialog() throws Exception {
1500         Log.v(TAG, "touchOutsideSaveDialog()");
1501         final UiObject2 picker = waitForObject(SAVE_UI_SELECTOR, SAVE_TIMEOUT);
1502         Log.v(TAG, "got picker: " + picker);
1503         final Rect bounds = picker.getVisibleBounds();
1504         assertThat(injectClick(new Point(bounds.left, bounds.top / 2))).isTrue();
1505     }
1506 
1507     /**
1508      * click dismiss button the fill dialog.
1509      */
1510     public void clickFillDialogDismiss() throws Exception {
1511         Log.v(TAG, "dismissedFillDialog()");
1512         final UiObject2 picker = findFillDialogPicker();
1513         final UiObject2 noButton =
1514                 picker.findObject(By.res("android", RESOURCE_ID_FILL_DIALOG_BUTTON_NO));
1515         noButton.click();
1516     }
1517 
1518     private UiObject2 findFillDialogPicker() throws Exception {
1519         return waitForObject(FILL_DIALOG_SELECTOR, UI_DATASET_PICKER_TIMEOUT);
1520     }
1521 
1522     public UiObject2 findFillDialogDatasetPicker() throws Exception {
1523         return waitForObject(FILL_DIALOG_DATASET_SELECTOR, UI_DATASET_PICKER_TIMEOUT);
1524     }
1525 
1526     public UiObject2 findFillDialogHeaderPicker() throws Exception {
1527         return waitForObject(FILL_DIALOG_HEADER_SELECTOR, UI_DATASET_PICKER_TIMEOUT);
1528     }
1529 
1530     /**
1531      * Asserts the fill dialog is not shown.
1532      */
1533     public void assertNoFillDialog() throws Exception {
1534         assertNeverShown("Fill dialog", FILL_DIALOG_SELECTOR, DATASET_PICKER_NOT_SHOWN_NAPTIME_MS);
1535     }
1536 
1537     /**
1538      * Injects a click input event at the given point in the default display.
1539      * We have this method because {@link UiObject2#click) cannot touch outside the object, and
1540      * {@link UiDevice#click} is broken in multi windowing mode (b/238254060).
1541      */
1542     private boolean injectClick(Point p) {
1543         final long downTime = SystemClock.uptimeMillis();
1544         final MotionEvent downEvent = getMotionEvent(downTime, downTime, MotionEvent.ACTION_DOWN,
1545                 p);
1546         if (!mAutoman.injectInputEvent(downEvent, true)) {
1547             Log.e(TAG, "Failed to inject down event.");
1548             return false;
1549         }
1550 
1551         try {
1552             Thread.sleep(100);
1553         } catch (InterruptedException e) {
1554             Log.e(TAG, "Interrupted while sleep between click", e);
1555         }
1556 
1557         final MotionEvent upEvent = getMotionEvent(downTime, SystemClock.uptimeMillis(),
1558                 MotionEvent.ACTION_UP, p);
1559         return mAutoman.injectInputEvent(upEvent, true);
1560     }
1561 
1562     private MotionEvent getMotionEvent(long downTime, long eventTime, int action, Point p) {
1563         final MotionEvent.PointerProperties properties = new MotionEvent.PointerProperties();
1564         properties.id = 0;
1565         properties.toolType = Configurator.getInstance().getToolType();
1566         final MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
1567         coords.pressure = 1.0F;
1568         coords.size = 1.0F;
1569         coords.x = p.x;
1570         coords.y = p.y;
1571         MotionEvent event = MotionEvent.obtain(downTime, eventTime, action, 1,
1572                 new MotionEvent.PointerProperties[]{properties},
1573                 new MotionEvent.PointerCoords[]{coords}, 0, 0, 1.0F, 1.0F, 0, 0,
1574                 InputDevice.SOURCE_TOUCHSCREEN, 0);
1575         mUserHelper.injectDisplayIdIfNeeded(event);
1576         return event;
1577     }
1578 }
1579