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