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