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