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.Helper.NOT_SHOWING_TIMEOUT_MS;
20 import static android.autofillservice.cts.Helper.SAVE_TIMEOUT_MS;
21 import static android.autofillservice.cts.Helper.UI_TIMEOUT_MS;
22 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_ADDRESS;
23 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD;
24 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_EMAIL_ADDRESS;
25 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC;
26 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
27 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_USERNAME;
28 
29 import static com.google.common.truth.Truth.assertThat;
30 import static com.google.common.truth.Truth.assertWithMessage;
31 
32 import android.app.Instrumentation;
33 import android.app.UiAutomation;
34 import android.content.res.Resources;
35 import android.os.SystemClock;
36 import android.service.autofill.SaveInfo;
37 import android.support.test.InstrumentationRegistry;
38 import android.support.test.uiautomator.By;
39 import android.support.test.uiautomator.BySelector;
40 import android.support.test.uiautomator.UiDevice;
41 import android.support.test.uiautomator.UiObject2;
42 import android.text.Html;
43 import android.util.Log;
44 import android.view.accessibility.AccessibilityWindowInfo;
45 
46 import java.util.ArrayList;
47 import java.util.Arrays;
48 import java.util.List;
49 
50 /**
51  * Helper for UI-related needs.
52  */
53 final class UiBot {
54 
55     private static final String RESOURCE_ID_DATASET_PICKER = "autofill_dataset_picker";
56     private static final String RESOURCE_ID_SAVE_SNACKBAR = "autofill_save";
57     private static final String RESOURCE_ID_SAVE_TITLE = "autofill_save_title";
58     private static final String RESOURCE_ID_CONTEXT_MENUITEM = "floating_toolbar_menu_item_text";
59 
60     private static final String RESOURCE_STRING_SAVE_TITLE = "autofill_save_title";
61     private static final String RESOURCE_STRING_SAVE_TITLE_WITH_TYPE =
62             "autofill_save_title_with_type";
63     private static final String RESOURCE_STRING_SAVE_TYPE_PASSWORD = "autofill_save_type_password";
64     private static final String RESOURCE_STRING_SAVE_TYPE_ADDRESS = "autofill_save_type_address";
65     private static final String RESOURCE_STRING_SAVE_TYPE_CREDIT_CARD =
66             "autofill_save_type_credit_card";
67     private static final String RESOURCE_STRING_SAVE_TYPE_USERNAME = "autofill_save_type_username";
68     private static final String RESOURCE_STRING_SAVE_TYPE_EMAIL_ADDRESS =
69             "autofill_save_type_email_address";
70     private static final String RESOURCE_STRING_AUTOFILL = "autofill";
71     private static final String RESOURCE_STRING_DATASET_PICKER_ACCESSIBILITY_TITLE =
72             "autofill_picker_accessibility_title";
73     private static final String RESOURCE_STRING_SAVE_SNACKBAR_ACCESSIBILITY_TITLE =
74             "autofill_save_accessibility_title";
75 
76     private static final String TAG = "AutoFillCtsUiBot";
77 
78     private final UiDevice mDevice;
79     private final String mPackageName;
80     private final UiAutomation mAutoman;
81 
UiBot(Instrumentation instrumentation)82     UiBot(Instrumentation instrumentation) throws Exception {
83         mDevice = UiDevice.getInstance(instrumentation);
84         mPackageName = instrumentation.getContext().getPackageName();
85         mAutoman = instrumentation.getUiAutomation();
86     }
87 
88     /**
89      * Asserts the dataset chooser is not shown.
90      */
assertNoDatasets()91     void assertNoDatasets() {
92         final UiObject2 picker;
93         try {
94             picker = findDatasetPicker(NOT_SHOWING_TIMEOUT_MS);
95         } catch (Throwable t) {
96             // Use a more elegant check than catching the expection because it's not showing...
97             return;
98         }
99         throw new RetryableException(
100                 "Should not be showing datasets, but got " + getChildrenAsText(picker));
101     }
102 
103     /**
104      * Asserts the dataset chooser is shown and contains the given datasets.
105      *
106      * @return the dataset picker object.
107      */
assertDatasets(String...names)108     UiObject2 assertDatasets(String...names) {
109         final UiObject2 picker = findDatasetPicker();
110         assertWithMessage("wrong dataset names").that(getChildrenAsText(picker))
111                 .containsExactlyElementsIn(Arrays.asList(names));
112         return picker;
113     }
114 
115     /**
116      * Gets the text of this object children.
117      */
getChildrenAsText(UiObject2 object)118     List<String> getChildrenAsText(UiObject2 object) {
119         final List<String> list = new ArrayList<>();
120         getChildrenAsText(object, list);
121         return list;
122     }
123 
getChildrenAsText(UiObject2 object, List<String> children)124     private static void getChildrenAsText(UiObject2 object, List<String> children) {
125         final String text = object.getText();
126         if (text != null) {
127             children.add(text);
128         }
129         for (UiObject2 child : object.getChildren()) {
130             getChildrenAsText(child, children);
131         }
132     }
133 
134     /**
135      * Selects a dataset that should be visible in the floating UI.
136      */
selectDataset(String name)137     void selectDataset(String name) {
138         final UiObject2 picker = findDatasetPicker();
139         selectDataset(picker, name);
140     }
141 
142     /**
143      * Selects a dataset that should be visible in the floating UI.
144      */
selectDataset(UiObject2 picker, String name)145     void selectDataset(UiObject2 picker, String name) {
146         final UiObject2 dataset = picker.findObject(By.text(name));
147         if (dataset == null) {
148             throw new AssertionError("no dataset " + name + " in " + getChildrenAsText(picker));
149         }
150         dataset.click();
151     }
152 
153     /**
154      * Selects a view by text.
155      *
156      * <p><b>NOTE:</b> when selecting an option in dataset picker is shown, prefer
157      * {@link #selectDataset(String)}.
158      */
selectByText(String name)159     void selectByText(String name) {
160         Log.v(TAG, "selectByText(): " + name);
161 
162         final UiObject2 object = waitForObject(By.text(name));
163         object.click();
164     }
165 
166     /**
167      * Asserts a text is shown.
168      *
169      * <p><b>NOTE:</b> when asserting the dataset picker is shown, prefer
170      * {@link #assertDatasets(String...)}.
171      */
assertShownByText(String text)172     public UiObject2 assertShownByText(String text) {
173         final UiObject2 object = waitForObject(By.text(text));
174         assertWithMessage(text).that(object).isNotNull();
175         return object;
176     }
177 
178     /**
179      * Checks if a View with a certain text exists.
180      */
hasViewWithText(String name)181     boolean hasViewWithText(String name) {
182         Log.v(TAG, "hasViewWithText(): " + name);
183 
184         return mDevice.findObject(By.text(name)) != null;
185     }
186 
187     /**
188      * Selects a view by id.
189      */
selectById(String id)190     void selectById(String id) {
191         Log.v(TAG, "selectById(): " + id);
192 
193         final UiObject2 view = waitForObject(By.res(id));
194         view.click();
195     }
196 
197     /**
198      * Asserts the id is shown on the screen.
199      */
assertShownById(String id)200     void assertShownById(String id) {
201         assertThat(waitForObject(By.res(id))).isNotNull();
202     }
203 
204     /**
205      * Gets the text set on a view.
206      */
getTextById(String id)207     String getTextById(String id) {
208         final UiObject2 obj = waitForObject(By.res(id));
209         return obj.getText();
210     }
211 
212     /**
213      * Focus in the view with the given resource id.
214      */
focusByRelativeId(String id)215     void focusByRelativeId(String id) {
216         waitForObject(By.res(mPackageName, id)).click();
217     }
218 
219     /**
220      * Sets a new text on a view.
221      */
setTextById(String id, String newText)222     void setTextById(String id, String newText) {
223         UiObject2 view = waitForObject(By.res(id));
224         view.setText(newText);
225     }
226 
227     /**
228      * Asserts the save snackbar is showing and returns it.
229      */
assertSaveShowing(int type)230     UiObject2 assertSaveShowing(int type) {
231         return assertSaveShowing(SAVE_TIMEOUT_MS, type);
232     }
233 
234     /**
235      * Asserts the save snackbar is showing and returns it.
236      */
assertSaveShowing(long timeout, int type)237     UiObject2 assertSaveShowing(long timeout, int type) {
238         return assertSaveShowing(null, timeout, type);
239     }
240 
241     /**
242      * Presses the back button.
243      */
pressBack()244     void pressBack() {
245         Log.d(TAG, "pressBack()");
246         mDevice.pressBack();
247     }
248 
249     /**
250      * Presses the home button.
251      */
pressHome()252     void pressHome() {
253         Log.d(TAG, "pressHome()");
254         mDevice.pressHome();
255     }
256     /**
257      * Asserts the save snackbar is not showing and returns it.
258      */
assertSaveNotShowing(int type)259     void assertSaveNotShowing(int type) {
260         try {
261             assertSaveShowing(NOT_SHOWING_TIMEOUT_MS, type);
262         } catch (Throwable t) {
263             // TODO: use a more elegant check than catching the expection because it's not showing
264             // (in which case it wouldn't need a type as parameter).
265             return;
266         }
267         throw new RetryableException("snack bar is showing");
268     }
269 
getSaveTypeString(int type)270     private String getSaveTypeString(int type) {
271         final String typeResourceName;
272         switch (type) {
273             case SAVE_DATA_TYPE_PASSWORD:
274                 typeResourceName = RESOURCE_STRING_SAVE_TYPE_PASSWORD;
275                 break;
276             case SAVE_DATA_TYPE_ADDRESS:
277                 typeResourceName = RESOURCE_STRING_SAVE_TYPE_ADDRESS;
278                 break;
279             case SAVE_DATA_TYPE_CREDIT_CARD:
280                 typeResourceName = RESOURCE_STRING_SAVE_TYPE_CREDIT_CARD;
281                 break;
282             case SAVE_DATA_TYPE_USERNAME:
283                 typeResourceName = RESOURCE_STRING_SAVE_TYPE_USERNAME;
284                 break;
285             case SAVE_DATA_TYPE_EMAIL_ADDRESS:
286                 typeResourceName = RESOURCE_STRING_SAVE_TYPE_EMAIL_ADDRESS;
287                 break;
288             default:
289                 throw new IllegalArgumentException("Unsupported type: " + type);
290         }
291         return getString(typeResourceName);
292     }
293 
assertSaveShowing(String description, int... types)294     UiObject2 assertSaveShowing(String description, int... types) {
295         return assertSaveShowing(SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, description,
296                 SAVE_TIMEOUT_MS, types);
297     }
298 
assertSaveShowing(String description, long timeout, int... types)299     UiObject2 assertSaveShowing(String description, long timeout, int... types) {
300         return assertSaveShowing(SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, description, timeout,
301                 types);
302     }
303 
assertSaveShowing(int negativeButtonStyle, String description, int... types)304     UiObject2 assertSaveShowing(int negativeButtonStyle, String description,
305             int... types) {
306         return assertSaveShowing(negativeButtonStyle, description, SAVE_TIMEOUT_MS, types);
307     }
308 
assertSaveShowing(int negativeButtonStyle, String description, long timeout, int... types)309     UiObject2 assertSaveShowing(int negativeButtonStyle, String description, long timeout,
310             int... types) {
311         final UiObject2 snackbar = waitForObject(By.res("android", RESOURCE_ID_SAVE_SNACKBAR),
312                 timeout);
313 
314         final UiObject2 titleView = snackbar.findObject(By.res("android", RESOURCE_ID_SAVE_TITLE));
315         assertWithMessage("save title (%s)", RESOURCE_ID_SAVE_TITLE).that(titleView).isNotNull();
316 
317         final String actualTitle = titleView.getText();
318         Log.d(TAG, "save title: " + actualTitle);
319 
320         final String serviceLabel = InstrumentedAutoFillService.class.getSimpleName();
321         switch (types.length) {
322             case 1:
323                 final String expectedTitle = (types[0] == SAVE_DATA_TYPE_GENERIC)
324                         ? Html.fromHtml(getString(RESOURCE_STRING_SAVE_TITLE,
325                                 serviceLabel), 0).toString()
326                         : Html.fromHtml(getString(RESOURCE_STRING_SAVE_TITLE_WITH_TYPE,
327                                 getSaveTypeString(types[0]), serviceLabel), 0).toString();
328                 assertThat(actualTitle).isEqualTo(expectedTitle);
329                 break;
330             case 2:
331                 // We cannot predict the order...
332                 assertThat(actualTitle).contains(getSaveTypeString(types[0]));
333                 assertThat(actualTitle).contains(getSaveTypeString(types[1]));
334                 break;
335             case 3:
336                 // We cannot predict the order...
337                 assertThat(actualTitle).contains(getSaveTypeString(types[0]));
338                 assertThat(actualTitle).contains(getSaveTypeString(types[1]));
339                 assertThat(actualTitle).contains(getSaveTypeString(types[2]));
340                 break;
341             default:
342                 throw new IllegalArgumentException("Invalid types: " + Arrays.toString(types));
343         }
344 
345         if (description != null) {
346             final UiObject2 saveSubTitle = snackbar.findObject(By.text(description));
347             assertWithMessage("save subtitle(%s)", description).that(saveSubTitle).isNotNull();
348         }
349 
350         final String negativeButtonText = (negativeButtonStyle
351                 == SaveInfo.NEGATIVE_BUTTON_STYLE_REJECT) ? "NOT NOW" : "NO THANKS";
352         UiObject2 negativeButton = snackbar.findObject(By.text(negativeButtonText));
353         assertWithMessage("negative button (%s)", negativeButtonText)
354                 .that(negativeButton).isNotNull();
355 
356         final String expectedAccessibilityTitle =
357                 getString(RESOURCE_STRING_SAVE_SNACKBAR_ACCESSIBILITY_TITLE);
358         assertAccessibilityTitle(snackbar, expectedAccessibilityTitle);
359 
360         return snackbar;
361     }
362 
363     /**
364      * Taps an option in the save snackbar.
365      *
366      * @param yesDoIt {@code true} for 'YES', {@code false} for 'NO THANKS'.
367      * @param types expected types of save info.
368      */
saveForAutofill(boolean yesDoIt, int... types)369     void saveForAutofill(boolean yesDoIt, int... types) {
370         final UiObject2 saveSnackBar = assertSaveShowing(
371                 SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, null, types);
372         saveForAutofill(saveSnackBar, yesDoIt);
373     }
374 
375     /**
376      * Taps an option in the save snackbar.
377      *
378      * @param yesDoIt {@code true} for 'YES', {@code false} for 'NO THANKS'.
379      * @param types expected types of save info.
380      */
saveForAutofill(int negativeButtonStyle, boolean yesDoIt, int... types)381     void saveForAutofill(int negativeButtonStyle, boolean yesDoIt, int... types) {
382         final UiObject2 saveSnackBar = assertSaveShowing(negativeButtonStyle,null, types);
383         saveForAutofill(saveSnackBar, yesDoIt);
384     }
385 
386     /**
387      * Taps an option in the save snackbar.
388      *
389      * @param saveSnackBar Save snackbar, typically obtained through
390      *            {@link #assertSaveShowing(int)}.
391      * @param yesDoIt {@code true} for 'YES', {@code false} for 'NO THANKS'.
392      */
saveForAutofill(UiObject2 saveSnackBar, boolean yesDoIt)393     void saveForAutofill(UiObject2 saveSnackBar, boolean yesDoIt) {
394         final String id = yesDoIt ? "autofill_save_yes" : "autofill_save_no";
395 
396         final UiObject2 button = saveSnackBar.findObject(By.res("android", id));
397         assertWithMessage("save button (%s)", id).that(button).isNotNull();
398         button.click();
399     }
400 
401     /**
402      * Gets the AUTOFILL contextual menu by long pressing a text field.
403      *
404      * <p><b>NOTE:</b> this method should only be called in scenarios where we explicitly want to
405      * test the overflow menu. For all other scenarios where we want to test manual autofill, it's
406      * better to call {@code AFM.requestAutofill()} directly, because it's less error-prone and
407      * faster.
408      *
409      * @param id resource id of the field.
410      */
getAutofillMenuOption(String id)411     UiObject2 getAutofillMenuOption(String id) {
412         final UiObject2 field = waitForObject(By.res(mPackageName, id));
413         // TODO: figure out why obj.longClick() doesn't always work
414         field.click(3000);
415 
416         final List<UiObject2> menuItems = waitForObjects(
417                 By.res("android", RESOURCE_ID_CONTEXT_MENUITEM));
418         final String expectedText = getString(RESOURCE_STRING_AUTOFILL);
419         final StringBuffer menuNames = new StringBuffer();
420         for (UiObject2 menuItem : menuItems) {
421             final String menuName = menuItem.getText();
422             if (menuName.equalsIgnoreCase(expectedText)) {
423                 return menuItem;
424             }
425             menuNames.append("'").append(menuName).append("' ");
426         }
427         throw new RetryableException("no '%s' on '%s'", expectedText, menuNames);
428     }
429 
430     /**
431      * Gets a string from the Android resources.
432      */
getString(String id)433     private String getString(String id) {
434         final Resources resources = InstrumentationRegistry.getContext().getResources();
435         final int stringId = resources.getIdentifier(id, "string", "android");
436         return resources.getString(stringId);
437     }
438 
439     /**
440      * Gets a string from the Android resources.
441      */
getString(String id, Object... formatArgs)442     private String getString(String id, Object... formatArgs) {
443         final Resources resources = InstrumentationRegistry.getContext().getResources();
444         final int stringId = resources.getIdentifier(id, "string", "android");
445         return resources.getString(stringId, formatArgs);
446     }
447 
448     /**
449      * Waits for and returns an object.
450      *
451      * @param selector {@link BySelector} that identifies the object.
452      */
waitForObject(BySelector selector)453     private UiObject2 waitForObject(BySelector selector) {
454         return waitForObject(selector, UI_TIMEOUT_MS);
455     }
456 
457     /**
458      * Waits for and returns an object.
459      *
460      * @param selector {@link BySelector} that identifies the object.
461      * @param timeout timeout in ms
462      */
waitForObject(BySelector selector, long timeout)463     private UiObject2 waitForObject(BySelector selector, long timeout) {
464         // NOTE: mDevice.wait does not work for the save snackbar, so we need a polling approach.
465         final int maxTries = 5;
466         final long napTime = timeout / maxTries;
467         for (int i = 1; i <= maxTries; i++) {
468             final UiObject2 uiObject = mDevice.findObject(selector);
469             if (uiObject != null) {
470                 return uiObject;
471             }
472             SystemClock.sleep(napTime);
473         }
474         throw new RetryableException("Object with selector '%s' not found in %d ms",
475                 selector, UI_TIMEOUT_MS);
476     }
477 
478     /**
479      * Waits for and returns a list of objects.
480      *
481      * @param selector {@link BySelector} that identifies the object.
482      */
waitForObjects(BySelector selector)483     private List<UiObject2> waitForObjects(BySelector selector) {
484         return waitForObjects(selector, UI_TIMEOUT_MS);
485     }
486 
487     /**
488      * Waits for and returns a list of objects.
489      *
490      * @param selector {@link BySelector} that identifies the object.
491      * @param timeout timeout in ms
492      */
waitForObjects(BySelector selector, long timeout)493     private List<UiObject2> waitForObjects(BySelector selector, long timeout) {
494         // NOTE: mDevice.wait does not work for the save snackbar, so we need a polling approach.
495         final int maxTries = 5;
496         final long napTime = timeout / maxTries;
497         for (int i = 1; i <= maxTries; i++) {
498             final List<UiObject2> uiObjects = mDevice.findObjects(selector);
499             if (uiObjects != null && !uiObjects.isEmpty()) {
500                 return uiObjects;
501             }
502             SystemClock.sleep(napTime);
503         }
504         throw new RetryableException("Objects with selector '%s' not found in %d ms",
505                 selector, UI_TIMEOUT_MS);
506     }
507 
findDatasetPicker()508     private UiObject2 findDatasetPicker() {
509         return findDatasetPicker(UI_TIMEOUT_MS);
510     }
511 
findDatasetPicker(long timeout)512     private UiObject2 findDatasetPicker(long timeout) {
513         final UiObject2 picker = waitForObject(By.res("android", RESOURCE_ID_DATASET_PICKER),
514                 timeout);
515 
516         final String expectedTitle = getString(RESOURCE_STRING_DATASET_PICKER_ACCESSIBILITY_TITLE);
517         assertAccessibilityTitle(picker, expectedTitle);
518 
519         return picker;
520     }
521 
522     /**
523      * Asserts a given object has the expected accessibility title.
524      */
assertAccessibilityTitle(UiObject2 object, String expectedTitle)525     private void assertAccessibilityTitle(UiObject2 object, String expectedTitle) {
526         // TODO: ideally it should get the AccessibilityWindowInfo from the object, but UiAutomator
527         // does not expose that.
528         for (AccessibilityWindowInfo window : mAutoman.getWindows()) {
529             final CharSequence title = window.getTitle();
530             if (title != null && title.toString().equals(expectedTitle)) {
531                 return;
532             }
533         }
534         throw new RetryableException("Title '%s' not found for %s", expectedTitle, object);
535     }
536 }
537