• 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.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     private 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 (true) {
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 (true) {
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         Log.v(TAG, "selectByRelativeId(): " + id);
518         UiObject2 object = waitForObject(By.res(mPackageName, id));
519         object.click();
520         return object;
521     }
522 
523     /**
524      * Asserts the id is shown on the screen.
525      */
assertShownById(String id)526     public UiObject2 assertShownById(String id) throws Exception {
527         final UiObject2 object = waitForObject(By.res(id));
528         assertThat(object).isNotNull();
529         return object;
530     }
531 
532     /**
533      * Asserts the id is shown on the screen, using a resource id from the test package.
534      */
assertShownByRelativeId(String id)535     public UiObject2 assertShownByRelativeId(String id) throws Exception {
536         return assertShownByRelativeId(id, mDefaultTimeout);
537     }
538 
assertShownByRelativeId(String id, Timeout timeout)539     public UiObject2 assertShownByRelativeId(String id, Timeout timeout) throws Exception {
540         final UiObject2 obj = waitForObject(By.res(mPackageName, id), timeout);
541         assertThat(obj).isNotNull();
542         return obj;
543     }
544 
545     /**
546      * Asserts the id is not shown on the screen anymore, using a resource id from the test package.
547      *
548      * <p><b>Note:</b> this method should only called AFTER the id was previously shown, otherwise
549      * it might pass without really asserting anything.
550      */
assertGoneByRelativeId(@onNull String id, @NonNull Timeout timeout)551     public void assertGoneByRelativeId(@NonNull String id, @NonNull Timeout timeout) {
552         assertGoneByRelativeId(/* parent = */ null, id, timeout);
553     }
554 
assertGoneByRelativeId(int resId, @NonNull Timeout timeout)555     public void assertGoneByRelativeId(int resId, @NonNull Timeout timeout) {
556         assertGoneByRelativeId(/* parent = */ null, getIdName(resId), timeout);
557     }
558 
getIdName(int resId)559     private String getIdName(int resId) {
560         return mContext.getResources().getResourceEntryName(resId);
561     }
562 
563     /**
564      * Asserts the id is not shown on the parent anymore, using a resource id from the test package.
565      *
566      * <p><b>Note:</b> this method should only called AFTER the id was previously shown, otherwise
567      * it might pass without really asserting anything.
568      */
assertGoneByRelativeId(@ullable UiObject2 parent, @NonNull String id, @NonNull Timeout timeout)569     public void assertGoneByRelativeId(@Nullable UiObject2 parent, @NonNull String id,
570             @NonNull Timeout timeout) {
571         final SearchCondition<Boolean> condition = Until.gone(By.res(mPackageName, id));
572         final boolean gone = parent != null
573                 ? parent.wait(condition, timeout.ms())
574                 : mDevice.wait(condition, timeout.ms());
575         if (!gone) {
576             final String message = "Object with id '" + id + "' should be gone after "
577                     + timeout + " ms";
578             dumpScreen(message);
579             throw new RetryableException(message);
580         }
581     }
582 
assertShownByRelativeId(int resId)583     public UiObject2 assertShownByRelativeId(int resId) throws Exception {
584         return assertShownByRelativeId(getIdName(resId));
585     }
586 
assertNeverShownByRelativeId(@onNull String description, int resId, long timeout)587     public void assertNeverShownByRelativeId(@NonNull String description, int resId, long timeout)
588             throws Exception {
589         final BySelector selector = By.res(Helper.MY_PACKAGE, getIdName(resId));
590         assertNeverShown(description, selector, timeout);
591     }
592 
593     /**
594      * Asserts that a {@code selector} is not showing after {@code timeout} milliseconds.
595      */
assertNeverShown(String description, BySelector selector, long timeout)596     protected void assertNeverShown(String description, BySelector selector, long timeout)
597             throws Exception {
598         SystemClock.sleep(timeout);
599         final UiObject2 object = mDevice.findObject(selector);
600         if (object != null) {
601             throw new AssertionError(
602                     String.format("Should not be showing %s after %dms, but got %s",
603                             description, timeout, getChildrenAsText(object)));
604         }
605     }
606 
607     /**
608      * Gets the text set on a view.
609      */
getTextByRelativeId(String id)610     public String getTextByRelativeId(String id) throws Exception {
611         return waitForObject(By.res(mPackageName, id)).getText();
612     }
613 
614     /**
615      * Focus in the view with the given resource id.
616      */
focusByRelativeId(String id)617     public void focusByRelativeId(String id) throws Exception {
618         waitForObject(By.res(mPackageName, id)).click();
619     }
620 
621     /**
622      * Sets a new text on a view.
623      */
setTextByRelativeId(String id, String newText)624     public void setTextByRelativeId(String id, String newText) throws Exception {
625         waitForObject(By.res(mPackageName, id)).setText(newText);
626     }
627 
628     /**
629      * Asserts the save snackbar is showing and returns it.
630      */
assertSaveShowing(int type)631     public UiObject2 assertSaveShowing(int type) throws Exception {
632         return assertSaveShowing(SAVE_TIMEOUT, type);
633     }
634 
635     /**
636      * Asserts the save snackbar is showing with a custom service name and returns it.
637      */
assertSaveShowingWithCustomServiceName(int type, String customServiceName)638     public UiObject2 assertSaveShowingWithCustomServiceName(int type, String customServiceName)
639             throws Exception {
640         return assertSaveOrUpdateShowing(/* update= */ false, SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL,
641                 SaveInfo.POSITIVE_BUTTON_STYLE_SAVE, null, SAVE_TIMEOUT, customServiceName, type);
642     }
643 
644     /**
645      * Asserts the save snackbar is showing and returns it.
646      */
assertSaveShowing(Timeout timeout, int type)647     public UiObject2 assertSaveShowing(Timeout timeout, int type) throws Exception {
648         return assertSaveShowing(null, timeout, type);
649     }
650 
651     /**
652      * Asserts the save snackbar is showing with the Update message and returns it.
653      */
assertUpdateShowing(int... types)654     public UiObject2 assertUpdateShowing(int... types) throws Exception {
655         return assertSaveOrUpdateShowing(/* update= */ true, SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL,
656                 null, SAVE_TIMEOUT, types);
657     }
658 
659     /**
660      * Presses the Back button.
661      */
pressBack()662     public void pressBack() {
663         Log.d(TAG, "pressBack()");
664         mDevice.pressBack();
665     }
666 
667     /**
668      * Presses the Home button.
669      */
pressHome()670     public void pressHome() {
671         Log.d(TAG, "pressHome()");
672         mDevice.pressHome();
673     }
674 
675     /**
676      * Asserts the save snackbar is not showing.
677      */
assertSaveNotShowing(int type)678     public void assertSaveNotShowing(int type) throws Exception {
679         assertNeverShown("save UI for type " + saveTypeToString(type), SAVE_UI_SELECTOR,
680                 SAVE_NOT_SHOWN_NAPTIME_MS);
681     }
682 
683     /**
684      * Asserts the save snackbar is not showing, explaining when.
685      */
assertSaveNotShowing(int type, @Nullable String when)686     public void assertSaveNotShowing(int type, @Nullable String when) throws Exception {
687         String suffix = when == null ? "" : " when " + when;
688         assertNeverShown("save UI for type " + saveTypeToString(type) + suffix, SAVE_UI_SELECTOR,
689                 SAVE_NOT_SHOWN_NAPTIME_MS);
690     }
691 
assertSaveNotShowing()692     public void assertSaveNotShowing() throws Exception {
693         assertNeverShown("save UI", SAVE_UI_SELECTOR, SAVE_NOT_SHOWN_NAPTIME_MS);
694     }
695 
getSaveTypeString(int type)696     private String getSaveTypeString(int type) {
697         final String typeResourceName;
698         switch (type) {
699             case SAVE_DATA_TYPE_PASSWORD:
700                 typeResourceName = RESOURCE_STRING_SAVE_TYPE_PASSWORD;
701                 break;
702             case SAVE_DATA_TYPE_ADDRESS:
703                 typeResourceName = RESOURCE_STRING_SAVE_TYPE_ADDRESS;
704                 break;
705             case SAVE_DATA_TYPE_CREDIT_CARD:
706                 typeResourceName = RESOURCE_STRING_SAVE_TYPE_CREDIT_CARD;
707                 break;
708             case SAVE_DATA_TYPE_USERNAME:
709                 typeResourceName = RESOURCE_STRING_SAVE_TYPE_USERNAME;
710                 break;
711             case SAVE_DATA_TYPE_EMAIL_ADDRESS:
712                 typeResourceName = RESOURCE_STRING_SAVE_TYPE_EMAIL_ADDRESS;
713                 break;
714             case SAVE_DATA_TYPE_DEBIT_CARD:
715                 typeResourceName = RESOURCE_STRING_SAVE_TYPE_DEBIT_CARD;
716                 break;
717             case SAVE_DATA_TYPE_PAYMENT_CARD:
718                 typeResourceName = RESOURCE_STRING_SAVE_TYPE_PAYMENT_CARD;
719                 break;
720             case SAVE_DATA_TYPE_GENERIC_CARD:
721                 typeResourceName = RESOURCE_STRING_SAVE_TYPE_GENERIC_CARD;
722                 break;
723             default:
724                 throw new IllegalArgumentException("Unsupported type: " + type);
725         }
726         return getString(typeResourceName);
727     }
728 
saveTypeToString(int type)729     private String saveTypeToString(int type) {
730         // Cannot use DebugUtils, it's @hide
731         switch (type) {
732             case SAVE_DATA_TYPE_PASSWORD:
733                 return "PASSWORD";
734             case SAVE_DATA_TYPE_ADDRESS:
735                 return "ADDRESS";
736             case SAVE_DATA_TYPE_CREDIT_CARD:
737                 return "CREDIT_CARD";
738             case SAVE_DATA_TYPE_USERNAME:
739                 return "USERNAME";
740             case SAVE_DATA_TYPE_EMAIL_ADDRESS:
741                 return "EMAIL_ADDRESS";
742             case SAVE_DATA_TYPE_DEBIT_CARD:
743                 return "DEBIT_CARD";
744             case SAVE_DATA_TYPE_PAYMENT_CARD:
745                 return "PAYMENT_CARD";
746             case SAVE_DATA_TYPE_GENERIC_CARD:
747                 return "GENERIC_CARD";
748             default:
749                 return "UNSUPPORT_TYPE_" + type;
750         }
751     }
752 
assertSaveShowing(String description, int... types)753     public UiObject2 assertSaveShowing(String description, int... types) throws Exception {
754         return assertSaveOrUpdateShowing(/* update= */ false, SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL,
755                 description, SAVE_TIMEOUT, types);
756     }
757 
assertSaveShowing(String description, Timeout timeout, int... types)758     public UiObject2 assertSaveShowing(String description, Timeout timeout, int... types)
759             throws Exception {
760         return assertSaveOrUpdateShowing(/* update= */ false, SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL,
761                 description, timeout, types);
762     }
763 
assertSaveShowing(int negativeButtonStyle, String description, int... types)764     public UiObject2 assertSaveShowing(int negativeButtonStyle, String description,
765             int... types) throws Exception {
766         return assertSaveOrUpdateShowing(/* update= */ false, negativeButtonStyle, description,
767                 SAVE_TIMEOUT, types);
768     }
769 
assertSaveShowing(int positiveButtonStyle, int... types)770     public UiObject2 assertSaveShowing(int positiveButtonStyle, int... types) throws Exception {
771         return assertSaveOrUpdateShowing(/* update= */ false, SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL,
772                 positiveButtonStyle, /* description= */ null, SAVE_TIMEOUT, types);
773     }
774 
assertSaveOrUpdateShowing(boolean update, int negativeButtonStyle, String description, Timeout timeout, int... types)775     public UiObject2 assertSaveOrUpdateShowing(boolean update, int negativeButtonStyle,
776             String description, Timeout timeout, int... types) throws Exception {
777         return assertSaveOrUpdateShowing(update, negativeButtonStyle,
778                 SaveInfo.POSITIVE_BUTTON_STYLE_SAVE, description, timeout, types);
779     }
780 
assertSaveOrUpdateShowing(boolean update, int negativeButtonStyle, int positiveButtonStyle, String description, Timeout timeout, int... types)781     public UiObject2 assertSaveOrUpdateShowing(boolean update, int negativeButtonStyle,
782             int positiveButtonStyle, String description, Timeout timeout, int... types)
783             throws Exception {
784         return assertSaveOrUpdateShowing(update, negativeButtonStyle, positiveButtonStyle,
785             description, timeout, InstrumentedAutoFillService.getServiceLabel(), types);
786     }
787 
assertSaveOrUpdateShowing(boolean update, int negativeButtonStyle, int positiveButtonStyle, String description, Timeout timeout, String serviceLabel, int... types)788     public UiObject2 assertSaveOrUpdateShowing(boolean update, int negativeButtonStyle,
789             int positiveButtonStyle, String description, Timeout timeout, String serviceLabel,
790             int... types) throws Exception {
791 
792         final UiObject2 snackbar = waitForObject(SAVE_UI_SELECTOR, timeout);
793 
794         final UiObject2 titleView =
795                 waitForObject(snackbar, By.res("android", RESOURCE_ID_SAVE_TITLE), timeout);
796         assertWithMessage("save title (%s) is not shown", RESOURCE_ID_SAVE_TITLE).that(titleView)
797                 .isNotNull();
798 
799         final UiObject2 iconView =
800                 waitForObject(snackbar, By.res("android", RESOURCE_ID_SAVE_ICON), timeout);
801         assertWithMessage("save icon (%s) is not shown", RESOURCE_ID_SAVE_ICON).that(iconView)
802                 .isNotNull();
803 
804         final String actualTitle = titleView.getText();
805         Log.d(TAG, "save title: " + actualTitle);
806 
807         final String titleId, titleWithTypeId;
808         if (update) {
809             titleId = RESOURCE_STRING_UPDATE_TITLE;
810             titleWithTypeId = RESOURCE_STRING_UPDATE_TITLE_WITH_TYPE;
811         } else {
812             titleId = RESOURCE_STRING_SAVE_TITLE;
813             titleWithTypeId = RESOURCE_STRING_SAVE_TITLE_WITH_TYPE;
814         }
815 
816         switch (types.length) {
817             case 1:
818                 final String expectedTitle = (types[0] == SAVE_DATA_TYPE_GENERIC)
819                         ? Html.fromHtml(getString(titleId, serviceLabel), 0).toString()
820                         : Html.fromHtml(getString(titleWithTypeId,
821                                 getSaveTypeString(types[0]), serviceLabel), 0).toString();
822                 assertThat(actualTitle).isEqualTo(expectedTitle);
823                 break;
824             case 2:
825                 // We cannot predict the order...
826                 assertThat(actualTitle).contains(getSaveTypeString(types[0]));
827                 assertThat(actualTitle).contains(getSaveTypeString(types[1]));
828                 break;
829             case 3:
830                 // We cannot predict the order...
831                 assertThat(actualTitle).contains(getSaveTypeString(types[0]));
832                 assertThat(actualTitle).contains(getSaveTypeString(types[1]));
833                 assertThat(actualTitle).contains(getSaveTypeString(types[2]));
834                 break;
835             default:
836                 throw new IllegalArgumentException("Invalid types: " + Arrays.toString(types));
837         }
838 
839         if (description != null) {
840             final UiObject2 saveSubTitle = snackbar.findObject(By.text(description));
841             assertWithMessage("save subtitle(%s)", description).that(saveSubTitle).isNotNull();
842         }
843 
844         final String positiveButtonStringId;
845         switch (positiveButtonStyle) {
846             case SaveInfo.POSITIVE_BUTTON_STYLE_CONTINUE:
847                 positiveButtonStringId = RESOURCE_STRING_CONTINUE_BUTTON_YES;
848                 break;
849             default:
850                 positiveButtonStringId = update ? RESOURCE_STRING_UPDATE_BUTTON_YES
851                         : RESOURCE_STRING_SAVE_BUTTON_YES;
852         }
853         final String expectedPositiveButtonText = getString(positiveButtonStringId).toUpperCase();
854         final UiObject2 positiveButton = waitForObject(snackbar,
855                 By.res("android", RESOURCE_ID_SAVE_BUTTON_YES), timeout);
856         assertWithMessage("wrong text on positive button")
857                 .that(positiveButton.getText().toUpperCase()).isEqualTo(expectedPositiveButtonText);
858 
859         final String negativeButtonStringId;
860         if (negativeButtonStyle == SaveInfo.NEGATIVE_BUTTON_STYLE_REJECT) {
861             negativeButtonStringId = RESOURCE_STRING_SAVE_BUTTON_NOT_NOW;
862         } else if (negativeButtonStyle == SaveInfo.NEGATIVE_BUTTON_STYLE_NEVER) {
863             negativeButtonStringId = RESOURCE_STRING_SAVE_BUTTON_NEVER;
864         } else {
865             negativeButtonStringId = RESOURCE_STRING_SAVE_BUTTON_NO_THANKS;
866         }
867         final String expectedNegativeButtonText = getString(negativeButtonStringId).toUpperCase();
868         final UiObject2 negativeButton = waitForObject(snackbar,
869                 By.res("android", RESOURCE_ID_SAVE_BUTTON_NO), timeout);
870         assertWithMessage("wrong text on negative button")
871                 .that(negativeButton.getText().toUpperCase()).isEqualTo(expectedNegativeButtonText);
872 
873         final String expectedAccessibilityTitle =
874                 getString(RESOURCE_STRING_SAVE_SNACKBAR_ACCESSIBILITY_TITLE);
875         timeout.run(
876                 String.format(
877                         "assertAccessibilityTitle(%s, %s)",
878                         snackbar,
879                         expectedAccessibilityTitle),
880                 () -> {
881                     try {
882                         assertAccessibilityTitle(snackbar, expectedAccessibilityTitle);
883                     } catch (RetryableException e) {
884                         return null;
885                     }
886                     return true;
887                 });
888         return snackbar;
889     }
890 
891     /**
892      * Taps an option in the save snackbar.
893      *
894      * @param yesDoIt {@code true} for 'YES', {@code false} for 'NO THANKS'.
895      * @param types expected types of save info.
896      */
saveForAutofill(boolean yesDoIt, int... types)897     public void saveForAutofill(boolean yesDoIt, int... types) throws Exception {
898         final UiObject2 saveSnackBar = assertSaveShowing(
899                 SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, null, types);
900         saveForAutofill(saveSnackBar, yesDoIt);
901     }
902 
updateForAutofill(boolean yesDoIt, int... types)903     public void updateForAutofill(boolean yesDoIt, int... types) throws Exception {
904         final UiObject2 saveUi = assertUpdateShowing(types);
905         saveForAutofill(saveUi, yesDoIt);
906     }
907 
908     /**
909      * Taps an option in the save snackbar.
910      *
911      * @param yesDoIt {@code true} for 'YES', {@code false} for 'NO THANKS'.
912      * @param types expected types of save info.
913      */
saveForAutofill(int negativeButtonStyle, boolean yesDoIt, int... types)914     public void saveForAutofill(int negativeButtonStyle, boolean yesDoIt, int... types)
915             throws Exception {
916         final UiObject2 saveSnackBar = assertSaveShowing(negativeButtonStyle, null, types);
917         saveForAutofill(saveSnackBar, yesDoIt);
918     }
919 
920     /**
921      * Taps the positive button in the save snackbar.
922      *
923      * @param types expected types of save info.
924      */
saveForAutofill(int positiveButtonStyle, int... types)925     public void saveForAutofill(int positiveButtonStyle, int... types) throws Exception {
926         final UiObject2 saveSnackBar = assertSaveShowing(positiveButtonStyle, types);
927         saveForAutofill(saveSnackBar, /* yesDoIt= */ true);
928     }
929 
930     /**
931      * Taps an option in the save snackbar.
932      *
933      * @param saveSnackBar Save snackbar, typically obtained through
934      *            {@link #assertSaveShowing(int)}.
935      * @param yesDoIt {@code true} for 'YES', {@code false} for 'NO THANKS'.
936      */
saveForAutofill(UiObject2 saveSnackBar, boolean yesDoIt)937     public void saveForAutofill(UiObject2 saveSnackBar, boolean yesDoIt) {
938         final String id = yesDoIt ? "autofill_save_yes" : "autofill_save_no";
939 
940         final UiObject2 button = saveSnackBar.findObject(By.res("android", id));
941         assertWithMessage("save button (%s)", id).that(button).isNotNull();
942         button.click();
943     }
944 
945     /**
946      * Gets the AUTOFILL contextual menu by long pressing a text field.
947      *
948      * <p><b>NOTE:</b> this method should only be called in scenarios where we explicitly want to
949      * test the overflow menu. For all other scenarios where we want to test manual autofill, it's
950      * better to call {@code AFM.requestAutofill()} directly, because it's less error-prone and
951      * faster.
952      *
953      * @param id resource id of the field.
954      */
getAutofillMenuOption(String id)955     public UiObject2 getAutofillMenuOption(String id) throws Exception {
956         final UiObject2 field = waitForObject(By.res(mPackageName, id));
957         // TODO: figure out why obj.longClick() doesn't always work
958         field.click(LONG_PRESS_MS);
959 
960         List<UiObject2> menuItems = waitForObjects(
961                 By.res("android", RESOURCE_ID_CONTEXT_MENUITEM), mDefaultTimeout);
962         final String expectedText = getAutofillContextualMenuTitle();
963 
964         final StringBuffer menuNames = new StringBuffer();
965 
966         // Check first menu for AUTOFILL
967         for (UiObject2 menuItem : menuItems) {
968             final String menuName = menuItem.getText();
969             if (menuName.equalsIgnoreCase(expectedText)) {
970                 Log.v(TAG, "AUTOFILL found in first menu");
971                 return menuItem;
972             }
973             menuNames.append("'").append(menuName).append("' ");
974         }
975 
976         menuNames.append(";");
977 
978         // First menu does not have AUTOFILL, check overflow
979         final BySelector overflowSelector = By.res("android", RESOURCE_ID_OVERFLOW);
980 
981         // Click overflow menu button.
982         final UiObject2 overflowMenu = waitForObject(overflowSelector, mDefaultTimeout);
983         overflowMenu.click();
984 
985         // Wait for overflow menu to show.
986         mDevice.wait(Until.gone(overflowSelector), 1000);
987 
988         menuItems = waitForObjects(
989                 By.res("android", RESOURCE_ID_CONTEXT_MENUITEM), mDefaultTimeout);
990         for (UiObject2 menuItem : menuItems) {
991             final String menuName = menuItem.getText();
992             if (menuName.equalsIgnoreCase(expectedText)) {
993                 Log.v(TAG, "AUTOFILL found in overflow menu");
994                 return menuItem;
995             }
996             menuNames.append("'").append(menuName).append("' ");
997         }
998         throw new RetryableException("no '%s' on '%s'", expectedText, menuNames);
999     }
1000 
getAutofillContextualMenuTitle()1001     String getAutofillContextualMenuTitle() {
1002         return getString(RESOURCE_STRING_AUTOFILL);
1003     }
1004 
1005     /**
1006      * Gets a string from the Android resources.
1007      */
getString(String id)1008     private String getString(String id) {
1009         final Resources resources = mContext.getResources();
1010         final int stringId = resources.getIdentifier(id, "string", "android");
1011         try {
1012             return resources.getString(stringId);
1013         } catch (Resources.NotFoundException e) {
1014             throw new IllegalStateException("no internal string for '" + id + "' / res=" + stringId
1015                     + ": ", e);
1016         }
1017     }
1018 
1019     /**
1020      * Gets a string from the Android resources.
1021      */
getString(String id, Object... formatArgs)1022     private String getString(String id, Object... formatArgs) {
1023         final Resources resources = mContext.getResources();
1024         final int stringId = resources.getIdentifier(id, "string", "android");
1025         try {
1026             return resources.getString(stringId, formatArgs);
1027         } catch (Resources.NotFoundException e) {
1028             throw new IllegalStateException("no internal string for '" + id + "' / res=" + stringId
1029                     + ": ", e);
1030         }
1031     }
1032 
1033     /**
1034      * Waits for and returns an object.
1035      *
1036      * @param selector {@link BySelector} that identifies the object.
1037      */
waitForObject(BySelector selector)1038     private UiObject2 waitForObject(BySelector selector) throws Exception {
1039         return waitForObject(selector, mDefaultTimeout);
1040     }
1041 
1042     /**
1043      * Waits for and returns an object.
1044      *
1045      * @param parent where to find the object (or {@code null} to use device's root).
1046      * @param selector {@link BySelector} that identifies the object.
1047      * @param timeout timeout in ms.
1048      * @param dumpOnError whether the window hierarchy should be dumped if the object is not found.
1049      */
waitForObject(UiObject2 parent, BySelector selector, Timeout timeout, boolean dumpOnError)1050     private UiObject2 waitForObject(UiObject2 parent, BySelector selector, Timeout timeout,
1051             boolean dumpOnError) throws Exception {
1052         // NOTE: mDevice.wait does not work for the save snackbar, so we need a polling approach.
1053         try {
1054             return timeout.run("waitForObject(" + selector + ")", () -> {
1055                 return parent != null
1056                         ? parent.findObject(selector)
1057                         : mDevice.findObject(selector);
1058 
1059             });
1060         } catch (RetryableException e) {
1061             if (dumpOnError) {
1062                 dumpScreen("waitForObject() for " + selector + "on "
1063                         + (parent == null ? "mDevice" : parent) + " failed");
1064             }
1065             throw e;
1066         }
1067     }
1068 
waitForObject(@ullable UiObject2 parent, @NonNull BySelector selector, @NonNull Timeout timeout)1069     public UiObject2 waitForObject(@Nullable UiObject2 parent, @NonNull BySelector selector,
1070             @NonNull Timeout timeout)
1071             throws Exception {
1072         return waitForObject(parent, selector, timeout, DUMP_ON_ERROR);
1073     }
1074 
1075     /**
1076      * Waits for and returns an object.
1077      *
1078      * @param selector {@link BySelector} that identifies the object.
1079      * @param timeout timeout in ms
1080      */
waitForObject(@onNull BySelector selector, @NonNull Timeout timeout)1081     protected UiObject2 waitForObject(@NonNull BySelector selector, @NonNull Timeout timeout)
1082             throws Exception {
1083         return waitForObject(/* parent= */ null, selector, timeout);
1084     }
1085 
1086     /**
1087      * Waits for and returns a child from a parent {@link UiObject2}.
1088      */
assertChildText(UiObject2 parent, String resourceId, String expectedText)1089     public UiObject2 assertChildText(UiObject2 parent, String resourceId, String expectedText)
1090             throws Exception {
1091         final UiObject2 child = waitForObject(parent, By.res(mPackageName, resourceId),
1092                 Timeouts.UI_TIMEOUT);
1093         assertWithMessage("wrong text for view '%s'", resourceId).that(child.getText())
1094                 .isEqualTo(expectedText);
1095         return child;
1096     }
1097 
1098     /**
1099      * Execute a Runnable and wait for {@link AccessibilityEvent#TYPE_WINDOWS_CHANGED} or
1100      * {@link AccessibilityEvent#TYPE_WINDOW_STATE_CHANGED}.
1101      */
waitForWindowChange(Runnable runnable, long timeoutMillis)1102     public AccessibilityEvent waitForWindowChange(Runnable runnable, long timeoutMillis) {
1103         try {
1104             return mAutoman.executeAndWaitForEvent(runnable, (AccessibilityEvent event) -> {
1105                 switch (event.getEventType()) {
1106                     case AccessibilityEvent.TYPE_WINDOWS_CHANGED:
1107                     case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
1108                         return true;
1109                     default:
1110                         Log.v(TAG, "waitForWindowChange(): ignoring event " + event);
1111                 }
1112                 return false;
1113             }, timeoutMillis);
1114         } catch (TimeoutException e) {
1115             throw new WindowChangeTimeoutException(e, timeoutMillis);
1116         }
1117     }
1118 
1119     public AccessibilityEvent waitForWindowChange(Runnable runnable) {
1120         return waitForWindowChange(runnable, Timeouts.WINDOW_CHANGE_TIMEOUT_MS);
1121     }
1122 
1123     /**
1124      * Waits for and returns a list of objects.
1125      *
1126      * @param selector {@link BySelector} that identifies the object.
1127      * @param timeout timeout in ms
1128      */
1129     private List<UiObject2> waitForObjects(BySelector selector, Timeout timeout) throws Exception {
1130         // NOTE: mDevice.wait does not work for the save snackbar, so we need a polling approach.
1131         try {
1132             return timeout.run("waitForObject(" + selector + ")", () -> {
1133                 final List<UiObject2> uiObjects = mDevice.findObjects(selector);
1134                 if (uiObjects != null && !uiObjects.isEmpty()) {
1135                     return uiObjects;
1136                 }
1137                 return null;
1138 
1139             });
1140 
1141         } catch (RetryableException e) {
1142             dumpScreen("waitForObjects() for " + selector + "failed");
1143             throw e;
1144         }
1145     }
1146 
1147     private UiObject2 findDatasetPicker(Timeout timeout) throws Exception {
1148         // The UI element here is flaky. Sometimes the UI automator returns a StateObject.
1149         // Retry is put in place here to make sure that we catch the object.
1150         UiObject2 picker = null;
1151         int retryCount = 0;
1152         final String expectedTitle = getString(RESOURCE_STRING_DATASET_PICKER_ACCESSIBILITY_TITLE);
1153         while (retryCount < MAX_UIOBJECT_RETRY_COUNT) {
1154             try {
1155                 picker = waitForObject(DATASET_PICKER_SELECTOR, timeout);
1156                 assertAccessibilityTitle(picker, expectedTitle);
1157                 break;
1158             } catch (StaleObjectException e) {
1159                 Log.d(TAG, "Retry grabbing view class");
1160             }
1161             retryCount++;
1162         }
1163         assertWithMessage(expectedTitle + " not found").that(retryCount).isLessThan(
1164                 MAX_UIOBJECT_RETRY_COUNT);
1165 
1166         if (picker != null) {
1167             mOkToCallAssertNoDatasets = true;
1168         }
1169 
1170         return picker;
1171     }
1172 
1173     /**
1174      * Asserts a given object has the expected accessibility title.
1175      */
1176     private void assertAccessibilityTitle(UiObject2 object, String expectedTitle) {
1177         // TODO: ideally it should get the AccessibilityWindowInfo from the object, but UiAutomator
1178         // does not expose that.
1179         for (AccessibilityWindowInfo window : mAutoman.getWindows()) {
1180             final CharSequence title = window.getTitle();
1181             Log.d(TAG, "assertAccessibilityTitle(): found title =" + title + ", expected title="
1182                     + expectedTitle);
1183             if (title != null && title.toString().equals(expectedTitle)) {
1184                 return;
1185             }
1186         }
1187         throw new RetryableException("Title '%s' not found for %s", expectedTitle, object);
1188     }
1189 
1190     /**
1191      * Sets the screen orientation.
1192      *
1193      * @param orientation typically {@link #LANDSCAPE} or {@link #PORTRAIT}.
1194      *
1195      * @throws RetryableException if value didn't change.
1196      */
1197     public void setScreenOrientation(int orientation) throws Exception {
1198         // Use the platform API instead of mDevice.getDisplayRotation(), which is slow due to
1199         // waitForIdle(). waitForIdle() is not needed here because in AutoFillServiceTestCase we
1200         // always use UiBot#setScreenOrientation() to change the screen rotation, which blocks until
1201         // new rotation is reflected on the device.
1202         final int currentRotation = InstrumentationRegistry.getInstrumentation().getContext()
1203                 .getSystemService(DisplayManager.class).getDisplay(Display.DEFAULT_DISPLAY)
1204                 .getRotation();
1205         mAutoman.setRotation(orientation);
1206 
1207         if (orientation == currentRotation) {
1208             // Just need to freeze the rotation.
1209             return;
1210         }
1211 
1212         UI_SCREEN_ORIENTATION_TIMEOUT.run("setScreenOrientation(" + orientation + ")", () ->
1213                 mDevice.getDisplayRotation() == orientation ? Boolean.TRUE : null);
1214     }
1215 
1216     /**
1217      * Gets the value of the screen orientation.
1218      *
1219      * @return typically {@link #LANDSCAPE} or {@link #PORTRAIT}.
1220      */
1221     public int getScreenOrientation() {
1222         return mDevice.getDisplayRotation();
1223     }
1224 
1225     /**
1226      * Dumps the current view hierarchy and take a screenshot and save both locally so they can be
1227      * inspected later.
1228      */
1229     public void dumpScreen(@NonNull String cause) {
1230         try {
1231             final File file = Helper.createTestFile("hierarchy.xml");
1232             if (file == null) return;
1233             Log.w(TAG, "Dumping window hierarchy because " + cause + " on " + file);
1234             try (FileInputStream fis = new FileInputStream(file)) {
1235                 mDevice.dumpWindowHierarchy(file);
1236             }
1237         } catch (Exception e) {
1238             Log.e(TAG, "error dumping screen on " + cause, e);
1239         } finally {
1240             takeScreenshotAndSave();
1241         }
1242     }
1243 
1244     private Rect cropScreenshotWithoutScreenDecoration(Activity activity) {
1245         final WindowInsets[] inset = new WindowInsets[1];
1246         final View[] rootView = new View[1];
1247 
1248         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
1249             rootView[0] = activity.getWindow().getDecorView();
1250             inset[0] = rootView[0].getRootWindowInsets();
1251         });
1252         final int navBarHeight = inset[0].getStableInsetBottom();
1253         final int statusBarHeight = inset[0].getStableInsetTop();
1254 
1255         return new Rect(0, statusBarHeight, rootView[0].getWidth(),
1256                 rootView[0].getHeight() - navBarHeight - statusBarHeight);
1257     }
1258 
1259     // TODO(b/74358143): ideally we should take a screenshot limited by the boundaries of the
1260     // activity window, so external elements (such as the clock) are filtered out and don't cause
1261     // test flakiness when the contents are compared.
1262     public Bitmap takeScreenshot() {
1263         return takeScreenshotWithRect(null);
1264     }
1265 
1266     public Bitmap takeScreenshot(@NonNull Activity activity) {
1267         // crop the screenshot without screen decoration to prevent test flakiness.
1268         final Rect rect = cropScreenshotWithoutScreenDecoration(activity);
1269         return takeScreenshotWithRect(rect);
1270     }
1271 
1272     private Bitmap takeScreenshotWithRect(@Nullable Rect r) {
1273         final long before = SystemClock.elapsedRealtime();
1274         final Bitmap bitmap = mAutoman.takeScreenshot();
1275         final long delta = SystemClock.elapsedRealtime() - before;
1276         Log.v(TAG, "Screenshot taken in " + delta + "ms");
1277         if (r == null) {
1278             return bitmap;
1279         }
1280         try {
1281             return Bitmap.createBitmap(bitmap, r.left, r.top, r.right, r.bottom);
1282         } finally {
1283             if (bitmap != null) {
1284                 bitmap.recycle();
1285             }
1286         }
1287     }
1288 
1289     /**
1290      * Takes a screenshot and save it in the file system for post-mortem analysis.
1291      */
1292     public void takeScreenshotAndSave() {
1293         File file = null;
1294         try {
1295             file = Helper.createTestFile("screenshot.png");
1296             if (file != null) {
1297                 Log.i(TAG, "Taking screenshot on " + file);
1298                 final Bitmap screenshot = takeScreenshot();
1299                 Helper.dumpBitmap(screenshot, file);
1300             }
1301         } catch (Exception e) {
1302             Log.e(TAG, "Error taking screenshot and saving on " + file, e);
1303         }
1304     }
1305 
1306     /**
1307      * Asserts the contents of a child element.
1308      *
1309      * @param parent parent object
1310      * @param childId (relative) resource id of the child
1311      * @param assertion if {@code null}, asserts the child does not exist; otherwise, asserts the
1312      * child with it.
1313      */
1314     public void assertChild(@NonNull UiObject2 parent, @NonNull String childId,
1315             @Nullable Visitor<UiObject2> assertion) {
1316         final UiObject2 child = parent.findObject(By.res(mPackageName, childId));
1317         try {
1318             if (assertion != null) {
1319                 assertWithMessage("Didn't find child with id '%s'", childId).that(child)
1320                         .isNotNull();
1321                 try {
1322                     assertion.visit(child);
1323                 } catch (Throwable t) {
1324                     throw new AssertionError("Error on child '" + childId + "'", t);
1325                 }
1326             } else {
1327                 assertWithMessage("Shouldn't find child with id '%s'", childId).that(child)
1328                         .isNull();
1329             }
1330         } catch (RuntimeException | Error e) {
1331             dumpScreen("assertChild(" + childId + ") failed: " + e);
1332             throw e;
1333         }
1334     }
1335 
1336     /**
1337      * Finds the first {@link URLSpan} on the current screen.
1338      */
1339     public URLSpan findFirstUrlSpanWithText(String str) throws Exception {
1340         final List<AccessibilityNodeInfo> list = mAutoman.getRootInActiveWindow()
1341                 .findAccessibilityNodeInfosByText(str);
1342         if (list.isEmpty()) {
1343             throw new AssertionError("Didn't found AccessibilityNodeInfo with " + str);
1344         }
1345 
1346         final AccessibilityNodeInfo text = list.get(0);
1347         final CharSequence accessibilityTextWithSpan = text.getText();
1348         if (!(accessibilityTextWithSpan instanceof Spanned)) {
1349             throw new AssertionError("\"" + text.getViewIdResourceName() + "\" was not a Spanned");
1350         }
1351 
1352         final URLSpan[] spans = ((Spanned) accessibilityTextWithSpan)
1353                 .getSpans(0, accessibilityTextWithSpan.length(), URLSpan.class);
1354         return spans[0];
1355     }
1356 
1357     public boolean scrollToTextObject(String text) {
1358         UiScrollable scroller = new UiScrollable(new UiSelector().scrollable(true));
1359         try {
1360             // Swipe far away from the edges to avoid triggering navigation gestures
1361             scroller.setSwipeDeadZonePercentage(0.25);
1362             return scroller.scrollTextIntoView(text);
1363         } catch (UiObjectNotFoundException e) {
1364             return false;
1365         }
1366     }
1367 
1368     /**
1369      * Asserts the header in the fill dialog.
1370      */
1371     public void assertFillDialogHeader(String expectedHeader) throws Exception {
1372         final UiObject2 header = findFillDialogHeaderPicker();
1373 
1374         assertWithMessage("wrong header for fill dialog")
1375                 .that(getChildrenAsText(header))
1376                 .containsExactlyElementsIn(Arrays.asList(expectedHeader)).inOrder();
1377     }
1378 
1379     /**
1380      * Asserts reject button in the fill dialog.
1381      */
1382     public void assertFillDialogRejectButton() throws Exception {
1383         final UiObject2 picker = findFillDialogPicker();
1384 
1385         // "No thanks" button shown
1386         final UiObject2 rejectButton = picker.findObject(
1387                 By.res("android", RESOURCE_ID_FILL_DIALOG_BUTTON_NO));
1388         assertWithMessage("No reject button in fill dialog")
1389                 .that(rejectButton).isNotNull();
1390         assertWithMessage("wrong text on reject button")
1391                 .that(rejectButton.getText().toUpperCase()).isEqualTo(
1392                         getString(RESOURCE_STRING_SAVE_BUTTON_NO_THANKS).toUpperCase());
1393     }
1394 
1395     /**
1396      * Asserts accept button in the fill dialog.
1397      */
1398     public void assertFillDialogAcceptButton() throws Exception {
1399         final UiObject2 picker = findFillDialogPicker();
1400 
1401         // "Continue" button shown
1402         final UiObject2 acceptButton = picker.findObject(
1403                 By.res("android", RESOURCE_ID_FILL_DIALOG_BUTTON_YES));
1404         assertWithMessage("No accept button in fill dialog")
1405                 .that(acceptButton).isNotNull();
1406         assertWithMessage("wrong text on accept button")
1407                 .that(acceptButton.getText().toUpperCase()).isEqualTo(
1408                         getString(RESOURCE_STRING_CONTINUE_BUTTON_YES).toUpperCase());
1409     }
1410 
1411     /**
1412      * Asserts there is no accept button in the fill dialog.
1413      */
1414     public void assertFillDialogNoAcceptButton() throws Exception {
1415         final UiObject2 picker = findFillDialogPicker();
1416 
1417         // "Continue" button not shown
1418         final UiObject2 acceptButton = picker.findObject(
1419                 By.res("android", RESOURCE_ID_FILL_DIALOG_BUTTON_YES));
1420         assertWithMessage("wrong accept button in fill dialog")
1421                 .that(acceptButton).isNull();
1422     }
1423 
1424     /**
1425      * Asserts the fill dialog is shown and contains the given datasets.
1426      *
1427      * @return the dataset picker object.
1428      */
1429     public UiObject2 assertFillDialogDatasets(String... datasets) throws Exception {
1430         final UiObject2 picker = findFillDialogDatasetPicker();
1431 
1432         assertWithMessage("wrong elements in fill dialog")
1433                 .that(getChildrenAsText(picker))
1434                 .containsExactlyElementsIn(datasets).inOrder();
1435         return picker;
1436     }
1437 
1438     /**
1439      * Asserts the fill dialog is shown and contains the given dataset. And then select the dataset
1440      */
1441     public void selectFillDialogDataset(String dataset) throws Exception {
1442         final UiObject2 picker = assertFillDialogDatasets(dataset);
1443         selectDataset(picker, dataset);
1444     }
1445 
1446     /**
1447      * Touch outside the fill dialog.
1448      */
1449     public void touchOutsideDialog() throws Exception {
1450         Log.v(TAG, "touchOutsideDialog()");
1451         final UiObject2 picker = findFillDialogPicker();
1452         assertThat(injectClick(new Point(1, picker.getVisibleBounds().top / 2))).isTrue();
1453     }
1454 
1455     /**
1456      * Touch outside the fill dialog.
1457      */
1458     public void touchOutsideSaveDialog() throws Exception {
1459         Log.v(TAG, "touchOutsideSaveDialog()");
1460         final UiObject2 picker = waitForObject(SAVE_UI_SELECTOR, SAVE_TIMEOUT);
1461         Log.v(TAG, "got picker: " + picker);
1462         assertThat(injectClick(new Point(1, picker.getVisibleBounds().top / 2))).isTrue();
1463     }
1464 
1465     /**
1466      * click dismiss button the fill dialog.
1467      */
1468     public void clickFillDialogDismiss() throws Exception {
1469         Log.v(TAG, "dismissedFillDialog()");
1470         final UiObject2 picker = findFillDialogPicker();
1471         final UiObject2 noButton =
1472                 picker.findObject(By.res("android", RESOURCE_ID_FILL_DIALOG_BUTTON_NO));
1473         noButton.click();
1474     }
1475 
1476     private UiObject2 findFillDialogPicker() throws Exception {
1477         return waitForObject(FILL_DIALOG_SELECTOR, UI_DATASET_PICKER_TIMEOUT);
1478     }
1479 
1480     public UiObject2 findFillDialogDatasetPicker() throws Exception {
1481         return waitForObject(FILL_DIALOG_DATASET_SELECTOR, UI_DATASET_PICKER_TIMEOUT);
1482     }
1483 
1484     public UiObject2 findFillDialogHeaderPicker() throws Exception {
1485         return waitForObject(FILL_DIALOG_HEADER_SELECTOR, UI_DATASET_PICKER_TIMEOUT);
1486     }
1487 
1488     /**
1489      * Asserts the fill dialog is not shown.
1490      */
1491     public void assertNoFillDialog() throws Exception {
1492         assertNeverShown("Fill dialog", FILL_DIALOG_SELECTOR, DATASET_PICKER_NOT_SHOWN_NAPTIME_MS);
1493     }
1494 
1495     /**
1496      * Injects a click input event at the given point in the default display.
1497      * We have this method because {@link UiObject2#click) cannot touch outside the object, and
1498      * {@link UiDevice#click} is broken in multi windowing mode (b/238254060).
1499      */
1500     private boolean injectClick(Point p) {
1501         final long downTime = SystemClock.uptimeMillis();
1502         final MotionEvent downEvent = getMotionEvent(downTime, downTime, MotionEvent.ACTION_DOWN,
1503                 p);
1504         if (!mAutoman.injectInputEvent(downEvent, true)) {
1505             Log.e(TAG, "Failed to inject down event.");
1506             return false;
1507         }
1508 
1509         try {
1510             Thread.sleep(100);
1511         } catch (InterruptedException e) {
1512             Log.e(TAG, "Interrupted while sleep between click", e);
1513         }
1514 
1515         final MotionEvent upEvent = getMotionEvent(downTime, SystemClock.uptimeMillis(),
1516                 MotionEvent.ACTION_UP, p);
1517         return mAutoman.injectInputEvent(upEvent, true);
1518     }
1519 
1520     private MotionEvent getMotionEvent(long downTime, long eventTime, int action, Point p) {
1521         final MotionEvent.PointerProperties properties = new MotionEvent.PointerProperties();
1522         properties.id = 0;
1523         properties.toolType = Configurator.getInstance().getToolType();
1524         final MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
1525         coords.pressure = 1.0F;
1526         coords.size = 1.0F;
1527         coords.x = p.x;
1528         coords.y = p.y;
1529         MotionEvent event = MotionEvent.obtain(downTime, eventTime, action, 1,
1530                 new MotionEvent.PointerProperties[]{properties},
1531                 new MotionEvent.PointerCoords[]{coords}, 0, 0, 1.0F, 1.0F, 0, 0,
1532                 InputDevice.SOURCE_TOUCHSCREEN, 0);
1533         mUserHelper.injectDisplayIdIfNeeded(event);
1534         return event;
1535     }
1536 }
1537