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 package android.autofillservice.cts.dropdown;
17 
18 import static android.autofillservice.cts.activities.VirtualContainerView.ID_URL_BAR;
19 import static android.autofillservice.cts.activities.VirtualContainerView.LABEL_CLASS;
20 import static android.autofillservice.cts.activities.VirtualContainerView.TEXT_CLASS;
21 import static android.autofillservice.cts.testcore.CannedFillResponse.NO_RESPONSE;
22 import static android.autofillservice.cts.testcore.Helper.ID_PASSWORD;
23 import static android.autofillservice.cts.testcore.Helper.ID_PASSWORD_LABEL;
24 import static android.autofillservice.cts.testcore.Helper.ID_USERNAME;
25 import static android.autofillservice.cts.testcore.Helper.ID_USERNAME_LABEL;
26 import static android.autofillservice.cts.testcore.Helper.assertTextAndValue;
27 import static android.autofillservice.cts.testcore.Helper.assertTextIsSanitized;
28 import static android.autofillservice.cts.testcore.Helper.assertTextOnly;
29 import static android.autofillservice.cts.testcore.Helper.dumpStructure;
30 import static android.autofillservice.cts.testcore.Helper.findNodeByResourceId;
31 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
32 
33 import static com.google.common.truth.Truth.assertThat;
34 import static com.google.common.truth.Truth.assertWithMessage;
35 
36 import static org.junit.Assume.assumeTrue;
37 
38 import android.app.assist.AssistStructure.ViewNode;
39 import android.autofillservice.cts.activities.VirtualContainerActivity;
40 import android.autofillservice.cts.activities.VirtualContainerView;
41 import android.autofillservice.cts.activities.VirtualContainerView.Line;
42 import android.autofillservice.cts.activities.VirtualContainerView.VisibilityIntegrationMode;
43 import android.autofillservice.cts.commontests.AutoFillServiceTestCase;
44 import android.autofillservice.cts.testcore.AutofillActivityTestRule;
45 import android.autofillservice.cts.testcore.CannedFillResponse;
46 import android.autofillservice.cts.testcore.CannedFillResponse.CannedDataset;
47 import android.autofillservice.cts.testcore.Helper;
48 import android.autofillservice.cts.testcore.InstrumentedAutoFillService.FillRequest;
49 import android.autofillservice.cts.testcore.InstrumentedAutoFillService.SaveRequest;
50 import android.autofillservice.cts.testcore.MyAutofillCallback;
51 import android.content.AutofillOptions;
52 import android.graphics.Rect;
53 import android.platform.test.annotations.AppModeFull;
54 import android.platform.test.annotations.Presubmit;
55 import android.platform.test.annotations.RequiresFlagsDisabled;
56 import android.platform.test.annotations.RequiresFlagsEnabled;
57 import android.service.autofill.SaveInfo;
58 import android.text.InputType;
59 import android.view.ViewGroup;
60 import android.view.autofill.AutofillManager;
61 import android.view.flags.Flags;
62 
63 import androidx.test.filters.FlakyTest;
64 import androidx.test.uiautomator.UiObject2;
65 
66 import org.junit.Ignore;
67 import org.junit.Test;
68 
69 import java.util.concurrent.TimeoutException;
70 
71 /**
72  * Test case for an activity containing virtual children, either using the explicit Autofill APIs
73  * or Compat mode.
74  */
75 public class VirtualContainerActivityTest
76         extends AutoFillServiceTestCase.AutoActivityLaunch<VirtualContainerActivity> {
77 
78     // TODO(b/74256300): remove when fixed it :-)
79     private static final boolean BUG_74256300_FIXED = false;
80 
81     private final boolean mCompatMode;
82     private AutofillActivityTestRule<VirtualContainerActivity> mActivityRule;
83     protected VirtualContainerActivity mActivity;
84 
VirtualContainerActivityTest()85     public VirtualContainerActivityTest() {
86         this(false);
87     }
88 
VirtualContainerActivityTest(boolean compatMode)89     protected VirtualContainerActivityTest(boolean compatMode) {
90         mCompatMode = compatMode;
91     }
92 
93     /**
94      * Hook for subclass to customize test before activity is created.
95      */
preActivityCreated()96     protected void preActivityCreated() {}
97 
98     /**
99      * Hook for subclass to customize activity after it's launched.
100      */
postActivityLaunched()101     protected void postActivityLaunched() {}
102 
103     @Override
getActivityRule()104     protected AutofillActivityTestRule<VirtualContainerActivity> getActivityRule() {
105         if (mActivityRule == null) {
106             mActivityRule =
107                     new AutofillActivityTestRule<VirtualContainerActivity>(
108                             VirtualContainerActivity.class) {
109                         @Override
110                         protected void beforeActivityLaunched() {
111                             preActivityCreated();
112                         }
113 
114                         @Override
115                         protected void afterActivityLaunched() {
116                             mActivity = getActivity();
117                             mActivity.setAutofillOptions(
118                                     new AutofillOptions(
119                                             /*logginLevel=AutofillManager.NO_LOGGING*/
120                                             0, mCompatMode));
121                             postActivityLaunched();
122                         }
123                     };
124         }
125         return mActivityRule;
126     }
127 
128     @Presubmit
129     @Test
testAutofillSync()130     public void testAutofillSync() throws Exception {
131         autofillTest(true);
132     }
133 
134     @Test
135     @AppModeFull(reason = "testAutofillSync() is enough")
testAutofillAsync()136     public void testAutofillAsync() throws Exception {
137         skipTestOnCompatMode();
138 
139         autofillTest(false);
140     }
141 
142     @FlakyTest(bugId = 276895614)
143     @Test
testAutofill_appContext()144     public void testAutofill_appContext() throws Exception {
145         mActivity.mCustomView.setAutofillManager(mActivity.getApplicationContext());
146         autofillTest(true);
147         // Validation check to make sure autofill is enabled in the application context
148         assertThat(mActivity.getApplicationContext().getSystemService(AutofillManager.class)
149                 .isEnabled()).isTrue();
150     }
151 
152     @Test
153     @RequiresFlagsEnabled(Flags.FLAG_CALCULATE_BOUNDS_IN_PARENT_FROM_BOUNDS_IN_SCREEN)
testAutofill_calculateBoundsInParentFromBoundsInScreenFlagOn_BoundsSet()154     public void testAutofill_calculateBoundsInParentFromBoundsInScreenFlagOn_BoundsSet()
155                 throws Exception {
156         autofillTest(false);
157     }
158 
159     @Test
160     @RequiresFlagsDisabled(Flags.FLAG_CALCULATE_BOUNDS_IN_PARENT_FROM_BOUNDS_IN_SCREEN)
testAutofill_calculateBoundsInParentFromBoundsInScreenFlagOff_BoundsSet()161     public void testAutofill_calculateBoundsInParentFromBoundsInScreenFlagOff_BoundsSet()
162                 throws Exception {
163         autofillTest(false);
164     }
165 
166     /**
167      * Focus to username and expect window event
168      */
focusToUsername()169     void focusToUsername() throws TimeoutException {
170         mUiBot.waitForWindowChange(() -> mActivity.mUsername.changeFocus(true));
171     }
172 
173     /**
174      * Focus to username and expect no autofill window event
175      */
focusToUsernameExpectNoWindowEvent()176     void focusToUsernameExpectNoWindowEvent() throws Throwable {
177         // TODO: should use waitForWindowChange() if we can filter out event of app Activity itself.
178         mActivityRule.runOnUiThread(() -> mActivity.mUsername.changeFocus(true));
179     }
180 
181     /**
182      * Focus to password and expect window event
183      */
focusToPassword()184     void focusToPassword() throws TimeoutException {
185         mUiBot.waitForWindowChange(() -> mActivity.mPassword.changeFocus(true));
186     }
187 
188     /**
189      * Focus to password and expect no autofill window event
190      */
focusToPasswordExpectNoWindowEvent()191     void focusToPasswordExpectNoWindowEvent() throws Throwable {
192         // TODO should use waitForWindowChange() if we can filter out event of app Activity itself.
193         mActivityRule.runOnUiThread(() -> mActivity.mPassword.changeFocus(true));
194     }
195 
196     /**
197      * Tests autofilling the virtual views, using the sync / async version of ViewStructure.addChild
198      */
autofillTest(boolean sync)199     private void autofillTest(boolean sync) throws Exception {
200         // Set service.
201         enableService();
202 
203         // Set expectations.
204         sReplier.addResponse(new CannedDataset.Builder()
205                 .setField(ID_USERNAME, "dude", createPresentation("DUDE"))
206                 .setField(ID_PASSWORD, "sweet", createPresentation("SWEET"))
207                 .build());
208         mActivity.expectAutoFill("dude", "sweet");
209         mActivity.mCustomView.setSync(sync);
210 
211         // Trigger auto-fill.
212         focusToUsername();
213         assertDatasetShown(mActivity.mUsername, "DUDE");
214 
215         // Play around with focus to make sure picker is properly drawn.
216         if (BUG_74256300_FIXED || !mCompatMode) {
217             focusToPassword();
218             assertDatasetShown(mActivity.mPassword, "SWEET");
219 
220             focusToUsername();
221             assertDatasetShown(mActivity.mUsername, "DUDE");
222         }
223 
224         // Make sure input was sanitized.
225         final FillRequest request = sReplier.getNextFillRequest();
226         final ViewNode urlBar = findNodeByResourceId(request.structure, ID_URL_BAR);
227         final ViewNode usernameLabel = findNodeByResourceId(request.structure, ID_USERNAME_LABEL);
228         final ViewNode username = findNodeByResourceId(request.structure, ID_USERNAME);
229         final ViewNode passwordLabel = findNodeByResourceId(request.structure, ID_PASSWORD_LABEL);
230         final ViewNode password = findNodeByResourceId(request.structure, ID_PASSWORD);
231 
232         // Check bounds are set correctly
233         assertThat(request.structure.getWindowNodeCount()).isEqualTo(1);
234         assertBoundsSet(request.structure.getWindowNodeAt(0).getRootViewNode());
235 
236         assertUrlBarIsSanitized(urlBar);
237         assertTextIsSanitized(username);
238         assertTextIsSanitized(password);
239         assertLabel(usernameLabel, "Username");
240         assertLabel(passwordLabel, "Password");
241 
242         assertThat(usernameLabel.getClassName()).isEqualTo(LABEL_CLASS);
243         assertThat(username.getClassName()).isEqualTo(TEXT_CLASS);
244         assertThat(passwordLabel.getClassName()).isEqualTo(LABEL_CLASS);
245         assertThat(password.getClassName()).isEqualTo(TEXT_CLASS);
246 
247         assertThat(username.getIdEntry()).isEqualTo(ID_USERNAME);
248         assertThat(password.getIdEntry()).isEqualTo(ID_PASSWORD);
249 
250         assertThat(username.getInputType())
251                 .isEqualTo(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_NORMAL);
252         assertThat(usernameLabel.getInputType()).isEqualTo(0);
253         assertThat(password.getInputType())
254                 .isEqualTo(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
255         assertThat(passwordLabel.getInputType()).isEqualTo(0);
256 
257         final String[] autofillHints = username.getAutofillHints();
258         final boolean hasCompatModeFlag = (request.flags
259                 & android.service.autofill.FillRequest.FLAG_COMPATIBILITY_MODE_REQUEST) != 0;
260         if (mCompatMode) {
261             assertThat(hasCompatModeFlag).isTrue();
262             assertThat(autofillHints).isNull();
263             assertThat(username.getHtmlInfo()).isNull();
264             assertThat(password.getHtmlInfo()).isNull();
265         } else {
266             assertThat(hasCompatModeFlag).isFalse();
267             // Make sure order is preserved and dupes not removed.
268             assertThat(autofillHints).asList()
269                     .containsExactly("c", "a", "a", "b", "a", "a")
270                     .inOrder();
271             try {
272                 VirtualContainerView.assertHtmlInfo(username);
273                 VirtualContainerView.assertHtmlInfo(password);
274             } catch (AssertionError | RuntimeException e) {
275                 dumpStructure("HtmlInfo failed", request.structure);
276                 throw e;
277             }
278         }
279 
280         // Make sure initial focus was properly set.
281         assertWithMessage("Username node is not focused").that(username.isFocused()).isTrue();
282         assertWithMessage("Password node is focused").that(password.isFocused()).isFalse();
283 
284         // Auto-fill it.
285         mUiBot.selectDataset("DUDE");
286 
287         // Check the results.
288         mActivity.assertAutoFilled();
289     }
290 
assertBoundsSet(ViewNode viewNode)291     private void assertBoundsSet(ViewNode viewNode) {
292         String idEntry = viewNode.getIdEntry();
293         // Values come from
294         // tests/autofillservice/res/layout/virtual_container_activity.xml
295         // The left and top values come from the margin
296         if (idEntry != null && idEntry.equals("text_view_child")) {
297             assertThat(viewNode.getLeft()).isEqualTo(10);
298             assertThat(viewNode.getTop()).isEqualTo(10);
299             assertThat(viewNode.getWidth()).isEqualTo(50);
300             assertThat(viewNode.getHeight()).isEqualTo(60);
301         }
302         // Recursively search for the ViewNode
303         for (int i = 0; i < viewNode.getChildCount(); ++i) {
304             assertBoundsSet(viewNode.getChildAt(i));
305         }
306     }
307 
308     @Test
309     @AppModeFull(reason = "testAutofillSync() is enough")
testAutofillTwoDatasets()310     public void testAutofillTwoDatasets() throws Exception {
311         // Set service.
312         enableService();
313 
314         // Set expectations.
315         sReplier.addResponse(new CannedFillResponse.Builder()
316                 .addDataset(new CannedDataset.Builder()
317                         .setField(ID_USERNAME, "dude")
318                         .setField(ID_PASSWORD, "sweet")
319                         .setPresentation(createPresentation("The Dude"))
320                         .build())
321                 .addDataset(new CannedDataset.Builder()
322                         .setField(ID_USERNAME, "DUDE")
323                         .setField(ID_PASSWORD, "SWEET")
324                         .setPresentation(createPresentation("THE DUDE"))
325                         .build())
326                 .build());
327         mActivity.expectAutoFill("DUDE", "SWEET");
328 
329         // Trigger auto-fill.
330         focusToUsername();
331         sReplier.getNextFillRequest();
332         assertDatasetShown(mActivity.mUsername, "The Dude", "THE DUDE");
333 
334         // Play around with focus to make sure picker is properly drawn.
335         if (BUG_74256300_FIXED || !mCompatMode) {
336             focusToPassword();
337             assertDatasetShown(mActivity.mPassword, "The Dude", "THE DUDE");
338             focusToUsername();
339             assertDatasetShown(mActivity.mUsername, "The Dude", "THE DUDE");
340         }
341 
342         // Auto-fill it.
343         mUiBot.selectDataset("THE DUDE");
344 
345         // Check the results.
346         mActivity.assertAutoFilled();
347     }
348 
349     @Presubmit
350     @Test
testAutofillOverrideDispatchProvideAutofillStructure()351     public void testAutofillOverrideDispatchProvideAutofillStructure() throws Exception {
352         mActivity.mCustomView.setOverrideDispatchProvideAutofillStructure(true);
353         autofillTest(true);
354     }
355 
356     @Presubmit
357     @Test
testAutofillManuallyOneDataset()358     public void testAutofillManuallyOneDataset() throws Exception {
359         skipTestOnCompatMode(); // TODO(b/73557072): not supported yet
360 
361         // Set service.
362         enableService();
363 
364         // Set expectations.
365         sReplier.addResponse(new CannedDataset.Builder()
366                 .setField(ID_USERNAME, "dude")
367                 .setField(ID_PASSWORD, "sweet")
368                 .setPresentation(createPresentation("The Dude"))
369                 .build());
370         mActivity.expectAutoFill("dude", "sweet");
371 
372         // Trigger auto-fill.
373         mActivity.requestAutofill(mActivity.mUsername);
374         sReplier.getNextFillRequest();
375 
376         // Select datatest.
377         mUiBot.selectDataset("The Dude");
378 
379         // Check the results.
380         mActivity.assertAutoFilled();
381     }
382 
383     @Test
384     @AppModeFull(reason = "testAutofillManuallyOneDataset() is enough")
testAutofillManuallyTwoDatasetsPickFirst()385     public void testAutofillManuallyTwoDatasetsPickFirst() throws Exception {
386         autofillManuallyTwoDatasets(true);
387     }
388 
389     @Test
390     @AppModeFull(reason = "testAutofillManuallyOneDataset() is enough")
testAutofillManuallyTwoDatasetsPickSecond()391     public void testAutofillManuallyTwoDatasetsPickSecond() throws Exception {
392         autofillManuallyTwoDatasets(false);
393     }
394 
autofillManuallyTwoDatasets(boolean pickFirst)395     private void autofillManuallyTwoDatasets(boolean pickFirst) throws Exception {
396         skipTestOnCompatMode(); // TODO(b/73557072): not supported yet
397 
398         // Set service.
399         enableService();
400 
401         // Set expectations.
402         sReplier.addResponse(new CannedFillResponse.Builder()
403                 .addDataset(new CannedDataset.Builder()
404                         .setField(ID_USERNAME, "dude")
405                         .setField(ID_PASSWORD, "sweet")
406                         .setPresentation(createPresentation("The Dude"))
407                         .build())
408                 .addDataset(new CannedDataset.Builder()
409                         .setField(ID_USERNAME, "jenny")
410                         .setField(ID_PASSWORD, "8675309")
411                         .setPresentation(createPresentation("Jenny"))
412                         .build())
413                 .build());
414         if (pickFirst) {
415             mActivity.expectAutoFill("dude", "sweet");
416         } else {
417             mActivity.expectAutoFill("jenny", "8675309");
418 
419         }
420 
421         // Trigger auto-fill.
422         mActivity.getSystemService(AutofillManager.class).requestAutofill(
423                 mActivity.mCustomView, mActivity.mUsername.text.id,
424                 mActivity.mUsername.getAbsCoordinates());
425         sReplier.getNextFillRequest();
426 
427         // Auto-fill it.
428         final UiObject2 picker = assertDatasetShown(mActivity.mUsername, "The Dude", "Jenny");
429         mUiBot.selectDataset(picker, pickFirst ? "The Dude" : "Jenny");
430 
431         // Check the results.
432         mActivity.assertAutoFilled();
433     }
434 
435     @Presubmit
436     @Test
testAutofillCallbacks()437     public void testAutofillCallbacks() throws Exception {
438         // Set service.
439         enableService();
440         final MyAutofillCallback callback = mActivity.registerCallback();
441 
442         // Set expectations.
443         sReplier.addResponse(new CannedDataset.Builder()
444                 .setField(ID_USERNAME, "dude")
445                 .setField(ID_PASSWORD, "sweet")
446                 .setPresentation(createPresentation("The Dude"))
447                 .build());
448         mActivity.expectAutoFill("dude", "sweet");
449 
450         // Trigger auto-fill.
451         focusToUsername();
452         sReplier.getNextFillRequest();
453 
454         callback.assertUiShownEvent(mActivity.mCustomView, mActivity.mUsername.text.id);
455 
456         // Change focus
457         focusToPassword();
458         callback.assertUiHiddenEvent(mActivity.mCustomView, mActivity.mUsername.text.id);
459         callback.assertUiShownEvent(mActivity.mCustomView, mActivity.mPassword.text.id);
460     }
461 
462     @Test
463     @AppModeFull(reason = "testAutofillCallbacks() is enough")
testAutofillCallbackDisabled()464     public void testAutofillCallbackDisabled() throws Throwable {
465         // Set service.
466         disableService();
467         final MyAutofillCallback callback = mActivity.registerCallback();
468 
469         // Trigger auto-fill.
470         focusToUsernameExpectNoWindowEvent();
471 
472         // Assert callback was called
473         callback.assertUiUnavailableEvent(mActivity.mCustomView, mActivity.mUsername.text.id);
474     }
475 
476     @Test
477     @AppModeFull(reason = "testAutofillCallbacks() is enough")
testAutofillCallbackNoDatasets()478     public void testAutofillCallbackNoDatasets() throws Throwable {
479         // Set service.
480         enableService();
481         final MyAutofillCallback callback = mActivity.registerCallback();
482 
483         // Set expectations.
484         sReplier.addResponse(NO_RESPONSE);
485 
486         // Trigger autofill.
487         focusToUsernameExpectNoWindowEvent();
488         sReplier.getNextFillRequest();
489 
490         // Auto-fill it.
491         mUiBot.assertNoDatasetsEver();
492 
493         // Assert callback was called
494         callback.assertUiUnavailableEvent(mActivity.mCustomView, mActivity.mUsername.text.id);
495     }
496 
497     @Test
498     @AppModeFull(reason = "testAutofillCallbacks() is enough")
testAutofillCallbackNoDatasetsButSaveInfo()499     public void testAutofillCallbackNoDatasetsButSaveInfo() throws Throwable {
500         // Set service.
501         enableService();
502         final MyAutofillCallback callback = mActivity.registerCallback();
503 
504         // Set expectations.
505         sReplier.addResponse(new CannedFillResponse.Builder()
506                 .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
507                 .build());
508 
509         // Trigger autofill.
510         focusToUsernameExpectNoWindowEvent();
511         sReplier.getNextFillRequest();
512 
513         // Autofill it.
514         mUiBot.assertNoDatasetsEver();
515 
516         // Assert callback was called
517         callback.assertUiUnavailableEvent(mActivity.mCustomView, mActivity.mUsername.text.id);
518 
519         // Make sure save is not triggered
520         mActivity.getAutofillManager().commit();
521         mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
522     }
523 
524     @Presubmit
525     @Test
testSaveDialogNotShownWhenBackIsPressed()526     public void testSaveDialogNotShownWhenBackIsPressed() throws Exception {
527         // Set service.
528         enableService();
529 
530         // Set expectations.
531         sReplier.addResponse(new CannedFillResponse.Builder()
532                 .addDataset(new CannedDataset.Builder()
533                         .setField(ID_USERNAME, "dude")
534                         .setField(ID_PASSWORD, "sweet")
535                         .setPresentation(createPresentation("The Dude"))
536                         .build())
537                 .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
538                 .build());
539         mActivity.expectAutoFill("dude", "sweet");
540 
541         // Trigger auto-fill.
542         focusToUsername();
543         sReplier.getNextFillRequest();
544         assertDatasetShown(mActivity.mUsername, "The Dude");
545 
546         mUiBot.pressBack();
547 
548         mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
549     }
550 
551     @Presubmit
552     @Test
testSave_childViewsGone_notifyAfm()553     public void testSave_childViewsGone_notifyAfm() throws Throwable {
554         saveTest(CommitType.CHILDREN_VIEWS_GONE_NOTIFY_CALLBACK_API);
555     }
556 
557     @Presubmit
558     @Test
testSave_childViewsGone_updateView()559     public void testSave_childViewsGone_updateView() throws Throwable {
560         saveTest(CommitType.CHILDREN_VIEWS_GONE_NOTIFY_CALLBACK_API);
561     }
562 
563     @Test
564     @Ignore("Disabled until b/73493342 is fixed")
testSave_parentViewGone()565     public void testSave_parentViewGone() throws Throwable {
566         saveTest(CommitType.PARENT_VIEW_GONE);
567     }
568 
569     @Presubmit
570     @Test
testSave_appCallsCommit()571     public void testSave_appCallsCommit() throws Throwable {
572         saveTest(CommitType.EXPLICIT_COMMIT);
573     }
574 
575     @Presubmit
576     @Test
testSave_submitButtonClicked()577     public void testSave_submitButtonClicked() throws Throwable {
578         saveTest(CommitType.SUBMIT_BUTTON_CLICKED);
579     }
580 
581     enum CommitType {
582         CHILDREN_VIEWS_GONE_NOTIFY_CALLBACK_API,
583         CHILDREN_VIEWS_GONE_IS_VISIBLE_API,
584         PARENT_VIEW_GONE,
585         EXPLICIT_COMMIT,
586         SUBMIT_BUTTON_CLICKED
587     }
588 
saveTest(CommitType commitType)589     private void saveTest(CommitType commitType) throws Throwable {
590         // Set service.
591         enableService();
592 
593         // Set expectations.
594         final CannedFillResponse.Builder response = new CannedFillResponse.Builder()
595                 .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD);
596 
597         switch (commitType) {
598             case CHILDREN_VIEWS_GONE_NOTIFY_CALLBACK_API:
599             case CHILDREN_VIEWS_GONE_IS_VISIBLE_API:
600             case PARENT_VIEW_GONE:
601                 response.setSaveInfoFlags(SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE);
602                 break;
603             case EXPLICIT_COMMIT:
604                 // does nothing
605                 break;
606             case SUBMIT_BUTTON_CLICKED:
607                 response
608                     .setSaveInfoFlags(SaveInfo.FLAG_DONT_SAVE_ON_FINISH)
609                     .setSaveTriggerId(mActivity.mCustomView.mLoginButtonId);
610                 break;
611             default:
612                 throw new IllegalArgumentException("invalid type: " + commitType);
613         }
614         sReplier.addResponse(response.build());
615 
616         // Trigger auto-fill.
617         focusToUsernameExpectNoWindowEvent();
618         sReplier.getNextFillRequest();
619         // Fill in some stuff
620         mActivity.mUsername.setText("foo");
621 
622         // Add a delay to prevent fill request triggers when focusing on
623         // password field
624         mUiBot.waitForIdleSync();
625 
626         focusToPasswordExpectNoWindowEvent();
627         mActivity.mPassword.setText("bar");
628 
629         // Add a delay to prevent save trigger happens too fast
630         mUiBot.waitForIdleSync();
631 
632         // Trigger save.
633         switch (commitType) {
634             case CHILDREN_VIEWS_GONE_NOTIFY_CALLBACK_API:
635                 setViewsInvisible(VisibilityIntegrationMode.NOTIFY_AFM);
636                 break;
637             case CHILDREN_VIEWS_GONE_IS_VISIBLE_API:
638                 setViewsInvisible(VisibilityIntegrationMode.OVERRIDE_IS_VISIBLE_TO_USER);
639                 break;
640             case PARENT_VIEW_GONE:
641                 mActivity.runOnUiThread(() -> {
642                     final ViewGroup parent = (ViewGroup) mActivity.mCustomView.getParent();
643                     parent.removeView(mActivity.mCustomView);
644                 });
645                 break;
646             case EXPLICIT_COMMIT:
647                 mActivity.getAutofillManager().commit();
648                 break;
649             case SUBMIT_BUTTON_CLICKED:
650                 mActivity.mCustomView.clickLogin();
651                 break;
652             default:
653                 throw new IllegalArgumentException("unknown type: " + commitType);
654         }
655 
656         // Assert UI is showing.
657         mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
658 
659         // Assert results
660         final SaveRequest saveRequest = sReplier.getNextSaveRequest();
661         final ViewNode username = findNodeByResourceId(saveRequest.structure, ID_USERNAME);
662         final ViewNode password = findNodeByResourceId(saveRequest.structure, ID_PASSWORD);
663 
664         assertTextAndValue(username, "foo");
665         assertTextAndValue(password, "bar");
666     }
667 
setViewsInvisible(VisibilityIntegrationMode mode)668     protected void setViewsInvisible(VisibilityIntegrationMode mode) {
669         mActivity.mUsername.setVisibilityIntegrationMode(mode);
670         mActivity.mPassword.setVisibilityIntegrationMode(mode);
671         mActivity.mUsername.changeVisibility(false);
672         mActivity.mPassword.changeVisibility(false);
673     }
674 
675     // NOTE: tests where save is not shown only makes sense when calling commit() explicitly,
676     // otherwise the test could pass but the UI is still shown *after* the app is committed.
677     // We could still test them by explicitly committing and then checking that the Save UI is not
678     // shown again, but then we wouldn't be effectively testing that the context was committed
679 
680     @Presubmit
681     @Test
testSaveNotShown_noUserInput()682     public void testSaveNotShown_noUserInput() throws Throwable {
683         // Set service.
684         enableService();
685 
686         // Set expectations.
687         sReplier.addResponse(new CannedFillResponse.Builder()
688                 .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD).build());
689 
690         // Trigger auto-fill.
691         focusToUsernameExpectNoWindowEvent();
692         sReplier.getNextFillRequest();
693 
694         // Trigger save.
695         mActivity.getAutofillManager().commit();
696 
697         // Assert it's not showing.
698         mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
699     }
700 
701     @Test
702     @AppModeFull(reason = "testSaveNotShown_noUserInput() is enough")
testSaveNotShown_initialValues_noUserInput()703     public void testSaveNotShown_initialValues_noUserInput() throws Throwable {
704         // Prepare activitiy.
705         mActivity.mUsername.setText("foo");
706         mActivity.mPassword.setText("bar");
707 
708         // Set service.
709         enableService();
710 
711         // Set expectations.
712         sReplier.addResponse(new CannedFillResponse.Builder()
713                 .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD).build());
714 
715         // Trigger auto-fill.
716         focusToUsernameExpectNoWindowEvent();
717         sReplier.getNextFillRequest();
718 
719         // Trigger save.
720         mActivity.getAutofillManager().commit();
721 
722         // Assert it's not showing.
723         mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
724     }
725 
726     @Test
727     @AppModeFull(reason = "testSaveNotShown_noUserInput() is enough")
testSaveNotShown_initialValues_noUserInput_serviceDatasets()728     public void testSaveNotShown_initialValues_noUserInput_serviceDatasets() throws Throwable {
729         // Prepare activitiy.
730         mActivity.mUsername.setText("foo");
731         mActivity.mPassword.setText("bar");
732 
733         // Set service.
734         enableService();
735 
736         // Set expectations.
737         sReplier.addResponse(new CannedFillResponse.Builder()
738                 .addDataset(new CannedDataset.Builder()
739                         .setField(ID_USERNAME, "dude")
740                         .setField(ID_PASSWORD, "sweet")
741                         .setPresentation(createPresentation("The Dude"))
742                         .build())
743                 .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD).build());
744 
745         // Trigger auto-fill.
746         focusToUsernameExpectNoWindowEvent();
747         sReplier.getNextFillRequest();
748         assertDatasetShown(mActivity.mUsername, "The Dude");
749 
750         // Trigger save.
751         mActivity.getAutofillManager().commit();
752 
753         // Assert it's not showing.
754         mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
755     }
756 
757     @Test
758     @AppModeFull(reason = "testSaveNotShown_noUserInput() is enough")
testSaveNotShown_userInputMatchesDatasets()759     public void testSaveNotShown_userInputMatchesDatasets() throws Throwable {
760         // Prepare activitiy.
761         mActivity.mUsername.setText("foo");
762         mActivity.mPassword.setText("bar");
763 
764         // Set service.
765         enableService();
766 
767         // Set expectations.
768         sReplier.addResponse(new CannedFillResponse.Builder()
769                 .addDataset(new CannedDataset.Builder()
770                         .setField(ID_USERNAME, "foo")
771                         .setField(ID_PASSWORD, "bar")
772                         .setPresentation(createPresentation("The Dude"))
773                         .build())
774                 .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD).build());
775 
776         // Trigger auto-fill.
777         focusToUsernameExpectNoWindowEvent();
778         sReplier.getNextFillRequest();
779         assertDatasetShown(mActivity.mUsername, "The Dude");
780 
781         // Trigger save.
782         mActivity.getAutofillManager().commit();
783 
784         // Assert it's not showing.
785         mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
786     }
787 
788     @Presubmit
789     @Test
testDatasetFiltering()790     public void testDatasetFiltering() throws Throwable {
791         final String aa = "Two A's";
792         final String ab = "A and B";
793         final String b = "Only B";
794 
795         enableService();
796 
797         // Set expectations.
798         sReplier.addResponse(new CannedFillResponse.Builder()
799                 .addDataset(new CannedDataset.Builder()
800                         .setField(ID_USERNAME, "aa")
801                         .setPresentation(createPresentation(aa))
802                         .build())
803                 .addDataset(new CannedDataset.Builder()
804                         .setField(ID_USERNAME, "ab")
805                         .setPresentation(createPresentation(ab))
806                         .build())
807                 .addDataset(new CannedDataset.Builder()
808                         .setField(ID_USERNAME, "b")
809                         .setPresentation(createPresentation(b))
810                         .build())
811                 .build());
812 
813         // Trigger auto-fill.
814         focusToUsernameExpectNoWindowEvent();
815         sReplier.getNextFillRequest();
816 
817         // With no filter text all datasets should be shown
818         assertDatasetShown(mActivity.mUsername, aa, ab, b);
819 
820         // Only two datasets start with 'a'
821         mActivity.mUsername.setText("a");
822         assertDatasetShown(mActivity.mUsername, aa, ab);
823 
824         // Only one dataset start with 'aa'
825         mActivity.mUsername.setText("aa");
826         assertDatasetShown(mActivity.mUsername, aa);
827 
828         // Only two datasets start with 'a'
829         mActivity.mUsername.setText("a");
830         assertDatasetShown(mActivity.mUsername, aa, ab);
831 
832         // With no filter text all datasets should be shown
833         mActivity.mUsername.setText("");
834         assertDatasetShown(mActivity.mUsername, aa, ab, b);
835 
836         // No dataset start with 'aaa'
837         final MyAutofillCallback callback = mActivity.registerCallback();
838         mActivity.mUsername.setText("aaa");
839         callback.assertUiHiddenEvent(mActivity.mCustomView, mActivity.mUsername.text.id);
840         mUiBot.assertNoDatasets();
841     }
842 
843     /**
844      * Asserts the dataset picker is properly displayed in a give line.
845      */
assertDatasetShown(Line line, String... expectedDatasets)846     protected UiObject2 assertDatasetShown(Line line, String... expectedDatasets)
847             throws Exception {
848         boolean autofillViewBoundsMatches = !Helper.isAutofillWindowFullScreen(mContext);
849         final UiObject2 datasetPicker = mUiBot.assertDatasets(expectedDatasets);
850         final Rect pickerBounds = datasetPicker.getVisibleBounds();
851         final Rect fieldBounds = line.getAbsCoordinates();
852         if (autofillViewBoundsMatches) {
853             assertWithMessage("vertical coordinates don't match; picker=%s, field=%s", pickerBounds,
854                     fieldBounds).that(pickerBounds.top).isEqualTo(fieldBounds.bottom);
855             assertWithMessage("horizontal coordinates don't match; picker=%s, field=%s",
856                     pickerBounds, fieldBounds).that(pickerBounds.left).isEqualTo(fieldBounds.left);
857         }
858         return datasetPicker;
859     }
860 
assertLabel(ViewNode node, String expectedValue)861     protected void assertLabel(ViewNode node, String expectedValue) {
862         if (mCompatMode) {
863             // Compat mode doesn't set AutofillValue of non-editable fields
864             assertTextOnly(node, expectedValue);
865         } else {
866             assertTextAndValue(node, expectedValue);
867         }
868     }
869 
assertUrlBarIsSanitized(ViewNode urlBar)870     protected void assertUrlBarIsSanitized(ViewNode urlBar) {
871         assertTextIsSanitized(urlBar);
872         assertThat(urlBar.getWebDomain()).isNull();
873         assertThat(urlBar.getWebScheme()).isNull();
874     }
875 
876 
skipTestOnCompatMode()877     private void skipTestOnCompatMode() {
878         assumeTrue("test not applicable when on compat mode", !mCompatMode);
879     }
880 }
881