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