| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.autofillservice.cts.commontests; |
| |
| import static android.autofillservice.cts.activities.CheckoutActivity.ID_CC_NUMBER; |
| import static android.autofillservice.cts.activities.LoginActivity.BACKDOOR_USERNAME; |
| import static android.autofillservice.cts.activities.LoginActivity.getWelcomeMessage; |
| import static android.autofillservice.cts.testcore.CannedFillResponse.DO_NOT_REPLY_RESPONSE; |
| import static android.autofillservice.cts.testcore.CannedFillResponse.NO_RESPONSE; |
| import static android.autofillservice.cts.testcore.Helper.ID_PASSWORD; |
| import static android.autofillservice.cts.testcore.Helper.ID_USERNAME; |
| import static android.autofillservice.cts.testcore.Helper.NULL_DATASET_ID; |
| import static android.autofillservice.cts.testcore.Helper.assertDeprecatedClientState; |
| import static android.autofillservice.cts.testcore.Helper.assertFillEventForAuthenticationSelected; |
| import static android.autofillservice.cts.testcore.Helper.assertFillEventForDatasetAuthenticationSelected; |
| import static android.autofillservice.cts.testcore.Helper.assertFillEventForDatasetSelected; |
| import static android.autofillservice.cts.testcore.Helper.assertFillEventForDatasetShown; |
| import static android.autofillservice.cts.testcore.Helper.assertFillEventForSaveShown; |
| import static android.autofillservice.cts.testcore.Helper.assertNoDeprecatedClientState; |
| import static android.autofillservice.cts.testcore.InstrumentedAutoFillService.waitUntilConnected; |
| import static android.autofillservice.cts.testcore.InstrumentedAutoFillService.waitUntilDisconnected; |
| import static android.service.autofill.FillEventHistory.Event.NO_SAVE_UI_REASON_DATASET_MATCH; |
| import static android.service.autofill.FillEventHistory.Event.NO_SAVE_UI_REASON_FIELD_VALIDATION_FAILED; |
| import static android.service.autofill.FillEventHistory.Event.NO_SAVE_UI_REASON_HAS_EMPTY_REQUIRED; |
| import static android.service.autofill.FillEventHistory.Event.NO_SAVE_UI_REASON_NO_SAVE_INFO; |
| import static android.service.autofill.FillEventHistory.Event.NO_SAVE_UI_REASON_NO_VALUE_CHANGED; |
| import static android.service.autofill.FillEventHistory.Event.NO_SAVE_UI_REASON_WITH_DELAY_SAVE_FLAG; |
| import static android.service.autofill.FillEventHistory.Event.UI_TYPE_INLINE; |
| import static android.service.autofill.FillEventHistory.Event.UI_TYPE_MENU; |
| import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC; |
| import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| import static com.google.common.truth.Truth.assertWithMessage; |
| |
| import android.autofillservice.cts.activities.AuthenticationActivity; |
| import android.autofillservice.cts.activities.CheckoutActivity; |
| import android.autofillservice.cts.inline.InlineFillEventHistoryTest; |
| import android.autofillservice.cts.testcore.CannedFillResponse; |
| import android.autofillservice.cts.testcore.CannedFillResponse.CannedDataset; |
| import android.autofillservice.cts.testcore.InstrumentedAutoFillService; |
| import android.autofillservice.cts.testcore.UiBot; |
| import android.content.Intent; |
| import android.content.IntentSender; |
| import android.os.Bundle; |
| import android.platform.test.annotations.AppModeFull; |
| import android.platform.test.annotations.Presubmit; |
| import android.service.autofill.FillEventHistory; |
| import android.service.autofill.FillEventHistory.Event; |
| import android.service.autofill.FillResponse; |
| import android.service.autofill.RegexValidator; |
| import android.service.autofill.SaveInfo; |
| import android.service.autofill.Validator; |
| import android.view.View; |
| import android.view.autofill.AutofillId; |
| |
| import org.junit.Test; |
| |
| import java.util.List; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| import java.util.regex.Pattern; |
| |
| /** |
| * This is the common test cases with {@link FillEventHistoryTest} and |
| * {@link InlineFillEventHistoryTest}. |
| */ |
| @Presubmit |
| @AppModeFull(reason = "Service-specific test") |
| public abstract class FillEventHistoryCommonTestCase extends AbstractLoginActivityTestCase { |
| |
| protected FillEventHistoryCommonTestCase() {} |
| |
| protected FillEventHistoryCommonTestCase(UiBot inlineUiBot) { |
| super(inlineUiBot); |
| } |
| |
| protected Bundle getBundle(String key, String value) { |
| final Bundle bundle = new Bundle(); |
| bundle.putString(key, value); |
| return bundle; |
| } |
| |
| @Test |
| public void testDatasetAuthenticationSelected() throws Exception { |
| enableService(); |
| |
| // Set up FillResponse with dataset authentication |
| Bundle clientState = new Bundle(); |
| clientState.putCharSequence("clientStateKey", "clientStateValue"); |
| |
| // Prepare the authenticated response |
| final IntentSender authentication = AuthenticationActivity.createSender(mContext, 1, |
| new CannedDataset.Builder() |
| .setField(ID_USERNAME, "dude") |
| .setField(ID_PASSWORD, "sweet") |
| .setPresentation("Dataset", isInlineMode()) |
| .build()); |
| |
| sReplier.addResponse(new CannedFillResponse.Builder().addDataset( |
| new CannedDataset.Builder() |
| .setField(ID_USERNAME, "username") |
| .setId("name") |
| .setPresentation("authentication", isInlineMode()) |
| .setAuthentication(authentication) |
| .build()) |
| .setExtras(clientState).build()); |
| mActivity.expectAutoFill("dude", "sweet"); |
| |
| // Trigger autofill and IME. |
| mUiBot.focusByRelativeId(ID_USERNAME); |
| mUiBot.waitForIdle(); |
| |
| // Authenticate |
| sReplier.getNextFillRequest(); |
| mUiBot.selectDataset("authentication"); |
| mActivity.assertAutoFilled(); |
| |
| // Verify fill selection |
| final List<Event> events = InstrumentedAutoFillService.getFillEvents(2); |
| int presentationType = isInlineMode() ? UI_TYPE_INLINE : UI_TYPE_MENU; |
| assertFillEventForDatasetShown(events.get(0), "clientStateKey", |
| "clientStateValue", presentationType); |
| assertFillEventForDatasetAuthenticationSelected(events.get(1), "name", |
| "clientStateKey", "clientStateValue"); |
| } |
| |
| @Test |
| public void testAuthenticationSelected() throws Exception { |
| enableService(); |
| |
| // Set up FillResponse with response wide authentication |
| Bundle clientState = new Bundle(); |
| clientState.putCharSequence("clientStateKey", "clientStateValue"); |
| |
| // Prepare the authenticated response |
| final IntentSender authentication = AuthenticationActivity.createSender(mContext, 1, |
| new CannedFillResponse.Builder().addDataset( |
| new CannedDataset.Builder() |
| .setField(ID_USERNAME, "username") |
| .setId("name") |
| .setPresentation("dataset", isInlineMode()) |
| .build()) |
| .setExtras(clientState).build()); |
| |
| sReplier.addResponse(new CannedFillResponse.Builder().setExtras(clientState) |
| .setPresentation("authentication", isInlineMode()) |
| .setAuthentication(authentication, ID_USERNAME) |
| .build()); |
| |
| // Trigger autofill and IME. |
| mUiBot.focusByRelativeId(ID_USERNAME); |
| mUiBot.waitForIdle(); |
| |
| // Authenticate |
| sReplier.getNextFillRequest(); |
| mUiBot.selectDataset("authentication"); |
| mUiBot.waitForIdle(); |
| mUiBot.selectDataset("dataset"); |
| mUiBot.waitForIdle(); |
| |
| // Verify fill selection |
| final FillEventHistory selection = InstrumentedAutoFillService.getFillEventHistory(4); |
| assertDeprecatedClientState(selection, "clientStateKey", "clientStateValue"); |
| List<Event> events = selection.getEvents(); |
| int presentationType = isInlineMode() ? UI_TYPE_INLINE : UI_TYPE_MENU; |
| assertFillEventForDatasetShown(events.get(0), "clientStateKey", |
| "clientStateValue", presentationType); |
| assertFillEventForAuthenticationSelected(events.get(1), NULL_DATASET_ID, |
| "clientStateKey", "clientStateValue"); |
| assertFillEventForDatasetShown(events.get(2), "clientStateKey", |
| "clientStateValue", presentationType); |
| assertFillEventForDatasetSelected(events.get(3), "name", |
| "clientStateKey", "clientStateValue"); |
| } |
| |
| @Test |
| public void testDatasetSelected_twoResponses() throws Exception { |
| enableService(); |
| |
| // Set up first partition with an anonymous dataset |
| Bundle clientState1 = new Bundle(); |
| clientState1.putCharSequence("clientStateKey", "Value1"); |
| |
| sReplier.addResponse(new CannedFillResponse.Builder().addDataset( |
| new CannedDataset.Builder() |
| .setField(ID_USERNAME, "username") |
| .setPresentation("dataset1", isInlineMode()) |
| .build()) |
| .setExtras(clientState1) |
| .build()); |
| mActivity.expectAutoFill("username"); |
| |
| // Trigger autofill and IME. |
| mUiBot.focusByRelativeId(ID_USERNAME); |
| waitUntilConnected(); |
| sReplier.getNextFillRequest(); |
| mUiBot.selectDataset("dataset1"); |
| mUiBot.waitForIdle(); |
| mActivity.assertAutoFilled(); |
| |
| int presentationType = isInlineMode() ? UI_TYPE_INLINE : UI_TYPE_MENU; |
| { |
| // Verify fill selection |
| final FillEventHistory selection = InstrumentedAutoFillService.getFillEventHistory(2); |
| assertDeprecatedClientState(selection, "clientStateKey", "Value1"); |
| final List<Event> events = selection.getEvents(); |
| assertFillEventForDatasetShown(events.get(0), "clientStateKey", |
| "Value1", presentationType); |
| assertFillEventForDatasetSelected(events.get(1), NULL_DATASET_ID, |
| "clientStateKey", "Value1"); |
| } |
| |
| // Set up second partition with a named dataset |
| Bundle clientState2 = new Bundle(); |
| clientState2.putCharSequence("clientStateKey", "Value2"); |
| |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .addDataset( |
| new CannedDataset.Builder() |
| .setField(ID_PASSWORD, "password2") |
| .setPresentation("dataset2", isInlineMode()) |
| .setId("name2") |
| .build()) |
| .addDataset( |
| new CannedDataset.Builder() |
| .setField(ID_PASSWORD, "password3") |
| .setPresentation("dataset3", isInlineMode()) |
| .setId("name3") |
| .build()) |
| .setExtras(clientState2) |
| .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_PASSWORD).build()); |
| mActivity.expectPasswordAutoFill("password3"); |
| |
| // Trigger autofill on password |
| mActivity.onPassword(View::requestFocus); |
| sReplier.getNextFillRequest(); |
| mUiBot.selectDataset("dataset3"); |
| mUiBot.waitForIdle(); |
| mActivity.assertAutoFilled(); |
| |
| { |
| // Verify fill selection |
| final FillEventHistory selection = InstrumentedAutoFillService.getFillEventHistory(2); |
| assertDeprecatedClientState(selection, "clientStateKey", "Value2"); |
| final List<Event> events = selection.getEvents(); |
| assertFillEventForDatasetShown(events.get(0), "clientStateKey", |
| "Value2", presentationType); |
| assertFillEventForDatasetSelected(events.get(1), "name3", |
| "clientStateKey", "Value2"); |
| } |
| |
| mActivity.onPassword((v) -> v.setText("new password")); |
| mActivity.syncRunOnUiThread(() -> mActivity.finish()); |
| waitUntilDisconnected(); |
| |
| { |
| // Verify fill selection |
| final FillEventHistory selection = InstrumentedAutoFillService.getFillEventHistory(4); |
| assertDeprecatedClientState(selection, "clientStateKey", "Value2"); |
| |
| final List<Event> events = selection.getEvents(); |
| assertFillEventForDatasetShown(events.get(0), "clientStateKey", |
| "Value2", presentationType); |
| assertFillEventForDatasetSelected(events.get(1), "name3", |
| "clientStateKey", "Value2"); |
| assertFillEventForDatasetShown(events.get(2), "clientStateKey", |
| "Value2", presentationType); |
| assertFillEventForSaveShown(events.get(3), NULL_DATASET_ID, |
| "clientStateKey", "Value2"); |
| } |
| } |
| |
| @Test |
| public void testNoEvents_whenServiceReturnsNullResponse() throws Exception { |
| enableService(); |
| |
| // First reset |
| sReplier.addResponse(new CannedFillResponse.Builder().addDataset( |
| new CannedDataset.Builder() |
| .setField(ID_USERNAME, "username") |
| .setPresentation("dataset1", isInlineMode()) |
| .build()) |
| .build()); |
| mActivity.expectAutoFill("username"); |
| |
| // Trigger autofill and IME. |
| mUiBot.focusByRelativeId(ID_USERNAME); |
| waitUntilConnected(); |
| sReplier.getNextFillRequest(); |
| mUiBot.selectDataset("dataset1"); |
| mUiBot.waitForIdleSync(); |
| mActivity.assertAutoFilled(); |
| |
| { |
| // Verify fill selection |
| int presentationType = |
| isInlineMode() ? UI_TYPE_INLINE : UI_TYPE_MENU; |
| final FillEventHistory selection = InstrumentedAutoFillService.getFillEventHistory(2); |
| assertNoDeprecatedClientState(selection); |
| final List<Event> events = selection.getEvents(); |
| assertFillEventForDatasetShown(events.get(0), presentationType); |
| assertFillEventForDatasetSelected(events.get(1), NULL_DATASET_ID); |
| } |
| |
| // Second request |
| sReplier.addResponse(NO_RESPONSE); |
| mActivity.onPassword(View::requestFocus); |
| mUiBot.waitForIdleSync(); |
| sReplier.getNextFillRequest(); |
| mUiBot.assertNoDatasets(); |
| waitUntilDisconnected(); |
| |
| InstrumentedAutoFillService.assertNoFillEventHistory(); |
| } |
| |
| @Test |
| public void testNoEvents_whenServiceReturnsFailure() throws Exception { |
| enableService(); |
| |
| // First reset |
| sReplier.addResponse(new CannedFillResponse.Builder().addDataset( |
| new CannedDataset.Builder() |
| .setField(ID_USERNAME, "username") |
| .setPresentation("dataset1", isInlineMode()) |
| .build()) |
| .build()); |
| mActivity.expectAutoFill("username"); |
| |
| // Trigger autofill and IME. |
| mUiBot.focusByRelativeId(ID_USERNAME); |
| mUiBot.waitForIdle(); |
| waitUntilConnected(); |
| sReplier.getNextFillRequest(); |
| mUiBot.selectDataset("dataset1"); |
| mUiBot.waitForIdleSync(); |
| mActivity.assertAutoFilled(); |
| |
| { |
| // Verify fill selection |
| int presentationType = |
| isInlineMode() ? UI_TYPE_INLINE : UI_TYPE_MENU; |
| final FillEventHistory selection = InstrumentedAutoFillService.getFillEventHistory(2); |
| assertNoDeprecatedClientState(selection); |
| final List<Event> events = selection.getEvents(); |
| assertFillEventForDatasetShown(events.get(0), presentationType); |
| assertFillEventForDatasetSelected(events.get(1), NULL_DATASET_ID); |
| } |
| |
| // Second request |
| sReplier.addResponse(new CannedFillResponse.Builder().returnFailure("D'OH!").build()); |
| mActivity.onPassword(View::requestFocus); |
| mUiBot.waitForIdleSync(); |
| sReplier.getNextFillRequest(); |
| mUiBot.assertNoDatasets(); |
| waitUntilDisconnected(); |
| |
| InstrumentedAutoFillService.assertNoFillEventHistory(); |
| } |
| |
| @Test |
| public void testNoEvents_whenServiceTimesout() throws Exception { |
| enableService(); |
| |
| // First reset |
| sReplier.addResponse(new CannedFillResponse.Builder().addDataset( |
| new CannedDataset.Builder() |
| .setField(ID_USERNAME, "username") |
| .setPresentation("dataset1", isInlineMode()) |
| .build()) |
| .build()); |
| mActivity.expectAutoFill("username"); |
| |
| // Trigger autofill and IME. |
| mUiBot.focusByRelativeId(ID_USERNAME); |
| waitUntilConnected(); |
| sReplier.getNextFillRequest(); |
| mUiBot.selectDataset("dataset1"); |
| mActivity.assertAutoFilled(); |
| |
| { |
| // Verify fill selection |
| int presentationType = |
| isInlineMode() ? UI_TYPE_INLINE : UI_TYPE_MENU; |
| final FillEventHistory selection = InstrumentedAutoFillService.getFillEventHistory(2); |
| assertNoDeprecatedClientState(selection); |
| final List<Event> events = selection.getEvents(); |
| assertFillEventForDatasetShown(events.get(0), presentationType); |
| assertFillEventForDatasetSelected(events.get(1), NULL_DATASET_ID); |
| } |
| |
| // Second request |
| sReplier.addResponse(DO_NOT_REPLY_RESPONSE); |
| mActivity.onPassword(View::requestFocus); |
| sReplier.getNextFillRequest(); |
| waitUntilDisconnected(); |
| |
| InstrumentedAutoFillService.assertNoFillEventHistory(); |
| } |
| |
| /** |
| * Tests the following scenario: |
| * |
| * <ol> |
| * <li>Activity A is launched. |
| * <li>Activity A triggers autofill. |
| * <li>Activity B is launched. |
| * <li>Activity B triggers autofill. |
| * <li>User goes back to Activity A. |
| * <li>Activity A triggers autofill. |
| * <li>User triggers save on Activity A - at this point, service should have stats of |
| * activity A. |
| * </ol> |
| */ |
| @Test |
| public void testEventsFromPreviousSessionIsDiscarded() throws Exception { |
| enableService(); |
| |
| // Launch activity A |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setExtras(getBundle("activity", "A")) |
| .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD) |
| .build()); |
| |
| // Trigger autofill and IME on activity A. |
| mUiBot.focusByRelativeId(ID_USERNAME); |
| waitUntilConnected(); |
| sReplier.getNextFillRequest(); |
| |
| // Verify fill selection for Activity A |
| final FillEventHistory selectionA = InstrumentedAutoFillService.getFillEventHistory(0); |
| assertDeprecatedClientState(selectionA, "activity", "A"); |
| |
| // Launch activity B |
| mActivity.startActivity(new Intent(mActivity, CheckoutActivity.class)); |
| mUiBot.assertShownByRelativeId(ID_CC_NUMBER); |
| |
| // Trigger autofill on activity B |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setExtras(getBundle("activity", "B")) |
| .addDataset(new CannedDataset.Builder() |
| .setField(ID_CC_NUMBER, "4815162342") |
| .setPresentation("datasetB", isInlineMode()) |
| .build()) |
| .build()); |
| mUiBot.focusByRelativeId(ID_CC_NUMBER); |
| sReplier.getNextFillRequest(); |
| |
| // Verify fill selection for Activity B |
| int presentationType = isInlineMode() ? UI_TYPE_INLINE : UI_TYPE_MENU; |
| final FillEventHistory selectionB = InstrumentedAutoFillService.getFillEventHistory(1); |
| assertDeprecatedClientState(selectionB, "activity", "B"); |
| assertFillEventForDatasetShown(selectionB.getEvents().get(0), "activity", |
| "B", presentationType); |
| |
| // Set response for back to activity A |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setExtras(getBundle("activity", "A")) |
| .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD) |
| .build()); |
| |
| // Now switch back to A... |
| mUiBot.pressBack(); // dismiss autofill |
| mUiBot.pressBack(); // dismiss keyboard (or task, if there was no keyboard) |
| final AtomicBoolean focusOnA = new AtomicBoolean(); |
| mActivity.syncRunOnUiThread(() -> focusOnA.set(mActivity.hasWindowFocus())); |
| if (!focusOnA.get()) { |
| mUiBot.pressBack(); // dismiss task, if the last pressBack dismissed only the keyboard |
| } |
| mUiBot.assertShownByRelativeId(ID_USERNAME); |
| assertWithMessage("root window has no focus") |
| .that(mActivity.getWindow().getDecorView().hasWindowFocus()).isTrue(); |
| |
| sReplier.getNextFillRequest(); |
| |
| // ...and trigger save |
| // Set credentials... |
| mActivity.onUsername((v) -> v.setText("malkovich")); |
| mActivity.onPassword((v) -> v.setText("malkovich")); |
| final String expectedMessage = getWelcomeMessage("malkovich"); |
| final String actualMessage = mActivity.tapLogin(); |
| assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage); |
| mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD); |
| sReplier.getNextSaveRequest(); |
| |
| // Finally, make sure history is right |
| final FillEventHistory finalSelection = InstrumentedAutoFillService.getFillEventHistory(1); |
| assertDeprecatedClientState(finalSelection, "activity", "A"); |
| assertFillEventForSaveShown(finalSelection.getEvents().get(0), NULL_DATASET_ID, "activity", |
| "A"); |
| } |
| |
| @Test |
| public void testContextCommitted_withoutFlagOnLastResponse() throws Exception { |
| enableService(); |
| // Trigger 1st autofill request |
| sReplier.addResponse(new CannedFillResponse.Builder().addDataset( |
| new CannedDataset.Builder() |
| .setId("id1") |
| .setField(ID_USERNAME, BACKDOOR_USERNAME) |
| .setPresentation("dataset1", isInlineMode()) |
| .build()) |
| .setFillResponseFlags(FillResponse.FLAG_TRACK_CONTEXT_COMMITED) |
| .build()); |
| mActivity.expectAutoFill(BACKDOOR_USERNAME); |
| // Trigger autofill and IME on username. |
| mUiBot.focusByRelativeId(ID_USERNAME); |
| sReplier.getNextFillRequest(); |
| mUiBot.selectDataset("dataset1"); |
| mActivity.assertAutoFilled(); |
| // Verify fill history |
| { |
| int presentationType = |
| isInlineMode() ? UI_TYPE_INLINE : UI_TYPE_MENU; |
| final List<Event> events = InstrumentedAutoFillService.getFillEvents(2); |
| assertFillEventForDatasetShown(events.get(0), presentationType); |
| assertFillEventForDatasetSelected(events.get(1), "id1"); |
| } |
| |
| // Trigger 2nd autofill request (which will clear the fill event history) |
| sReplier.addResponse(new CannedFillResponse.Builder().addDataset( |
| new CannedDataset.Builder() |
| .setId("id2") |
| .setField(ID_PASSWORD, "whatever") |
| .setPresentation("dataset2", isInlineMode()) |
| .build()) |
| // don't set flags |
| .build()); |
| mActivity.expectPasswordAutoFill("whatever"); |
| mActivity.onPassword(View::requestFocus); |
| sReplier.getNextFillRequest(); |
| mUiBot.selectDataset("dataset2"); |
| mActivity.assertAutoFilled(); |
| // Verify fill history |
| { |
| int presentationType = |
| isInlineMode() ? UI_TYPE_INLINE : UI_TYPE_MENU; |
| final List<Event> events = InstrumentedAutoFillService.getFillEvents(2); |
| assertFillEventForDatasetShown(events.get(0), presentationType); |
| assertFillEventForDatasetSelected(events.get(1), "id2"); |
| } |
| |
| // Finish the context by login in |
| final String expectedMessage = getWelcomeMessage(BACKDOOR_USERNAME); |
| final String actualMessage = mActivity.tapLogin(); |
| assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage); |
| mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD); |
| |
| { |
| // Verify fill history |
| int presentationType = |
| isInlineMode() ? UI_TYPE_INLINE : UI_TYPE_MENU; |
| final List<Event> events = InstrumentedAutoFillService.getFillEvents(2); |
| assertFillEventForDatasetShown(events.get(0), presentationType); |
| assertFillEventForDatasetSelected(events.get(1), "id2"); |
| } |
| } |
| |
| /** |
| * Tests scenario where the context was committed, the save dialog was not shown because the |
| * SaveInfo associated with the FillResponse is null. |
| */ |
| @Test |
| public void testContextCommitted_noSaveUi_whileNoSaveInfo() throws Exception { |
| enableService(); |
| |
| // Set expectations. |
| final CannedFillResponse.Builder builder = createTestResponseBuilder(); |
| sReplier.addResponse(builder.build()); |
| |
| // Trigger autofill and set the save UI not show reason with |
| // NO_SAVE_UI_REASON_NO_SAVE_INFO. |
| triggerAutofillForSaveUiCondition(NO_SAVE_UI_REASON_NO_SAVE_INFO); |
| |
| // Finish the context by login in and it will trigger to check if the save UI should be |
| // shown. |
| tapLogin(); |
| |
| // Verify that the save UI should not be shown and the history should include the reason. |
| mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD); |
| |
| final List<Event> verifyEvents = InstrumentedAutoFillService.getFillEvents(2); |
| final Event event = verifyEvents.get(1); |
| |
| assertThat(event.getNoSaveUiReason()).isEqualTo(NO_SAVE_UI_REASON_NO_SAVE_INFO); |
| } |
| |
| /** |
| * Tests scenario where the context was committed, the save dialog was not shown because the |
| * service asked to delay save. |
| */ |
| @Test |
| public void testContextCommitted_noSaveUi_whileDelaySave() throws Exception { |
| enableService(); |
| |
| // Set expectations. |
| final CannedFillResponse.Builder builder = createTestResponseBuilder(); |
| builder.setSaveInfoFlags(SaveInfo.FLAG_DELAY_SAVE); |
| sReplier.addResponse(builder.build()); |
| |
| // Trigger autofill and set the save UI not show reason with |
| // NO_SAVE_UI_REASON_WITH_DELAY_SAVE_FLAG. |
| triggerAutofillForSaveUiCondition(NO_SAVE_UI_REASON_WITH_DELAY_SAVE_FLAG); |
| |
| // Finish the context by login in and it will trigger to check if the save UI should be |
| // shown. |
| tapLogin(); |
| |
| // Verify that the save UI should not be shown and the history should include the reason. |
| mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD); |
| |
| final List<Event> verifyEvents = InstrumentedAutoFillService.getFillEvents(2); |
| final Event event = verifyEvents.get(1); |
| |
| assertThat(event.getNoSaveUiReason()).isEqualTo(NO_SAVE_UI_REASON_WITH_DELAY_SAVE_FLAG); |
| } |
| |
| /** |
| * Tests scenario where the context was committed, the save dialog was not shown because there |
| * was empty value for required ids. |
| */ |
| @Test |
| public void testContextCommitted_noSaveUi_whileEmptyValueForRequiredIds() throws Exception { |
| enableService(); |
| |
| // Set expectations. |
| final CannedFillResponse.Builder builder = createTestResponseBuilder(); |
| builder.setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD); |
| sReplier.addResponse(builder.build()); |
| |
| // Trigger autofill and set the save UI not show reason with |
| // NO_SAVE_UI_REASON_HAS_EMPTY_REQUIRED. |
| triggerAutofillForSaveUiCondition(NO_SAVE_UI_REASON_HAS_EMPTY_REQUIRED); |
| |
| // Finish the context by login in and it will trigger to check if the save UI should be |
| // shown. |
| tapLogin(); |
| |
| // Verify that the save UI should not be shown and the history should include the reason. |
| mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD); |
| |
| final List<Event> verifyEvents = InstrumentedAutoFillService.getFillEvents(2); |
| final Event event = verifyEvents.get(1); |
| |
| assertThat(event.getNoSaveUiReason()).isEqualTo(NO_SAVE_UI_REASON_HAS_EMPTY_REQUIRED); |
| } |
| |
| /** |
| * Tests scenario where the context was committed, the save dialog was not shown because no |
| * value has been changed. |
| */ |
| @Test |
| public void testContextCommitted_noSaveUi_whileNoValueChanged() throws Exception { |
| enableService(); |
| |
| // Set expectations. |
| final CannedFillResponse.Builder builder = createTestResponseBuilder(); |
| builder.setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD); |
| sReplier.addResponse(builder.build()); |
| |
| // Trigger autofill and set the save UI not show reason with |
| // NO_SAVE_UI_REASON_HAS_EMPTY_REQUIRED. |
| triggerAutofillForSaveUiCondition(NO_SAVE_UI_REASON_NO_VALUE_CHANGED); |
| |
| // Finish the context by login in and it will trigger to check if the save UI should be |
| // shown. |
| tapLogin(); |
| |
| // Verify that the save UI should not be shown and the history should include the reason. |
| mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD); |
| |
| final List<Event> verifyEvents = InstrumentedAutoFillService.getFillEvents(3); |
| final Event event = verifyEvents.get(2); |
| |
| assertThat(event.getNoSaveUiReason()).isEqualTo(NO_SAVE_UI_REASON_NO_VALUE_CHANGED); |
| } |
| |
| /** |
| * Tests scenario where the context was committed, the save dialog was not shown because fields |
| * failed validation. |
| */ |
| @Test |
| public void testContextCommitted_noSaveUi_whileFieldsFailedValidation() throws Exception { |
| enableService(); |
| |
| // Set expectations. |
| final CannedFillResponse.Builder builder = createTestResponseBuilder(); |
| builder.setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD) |
| .setSaveInfoVisitor((contexts, saveInfoBuilder) -> { |
| final Validator validator = |
| new RegexValidator(new AutofillId(1), Pattern.compile(".*")); |
| saveInfoBuilder.setValidator(validator); |
| }); |
| sReplier.addResponse(builder.build()); |
| |
| // Trigger autofill and set the save UI not show reason with |
| // NO_SAVE_UI_REASON_FIELD_VALIDATION_FAILED. |
| triggerAutofillForSaveUiCondition(NO_SAVE_UI_REASON_FIELD_VALIDATION_FAILED); |
| |
| // Finish the context by login in and it will trigger to check if the save UI should be |
| // shown. |
| tapLogin(); |
| |
| // Verify that the save UI should not be shown and the history should include the reason. |
| mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD); |
| |
| final List<Event> verifyEvents = InstrumentedAutoFillService.getFillEvents(2); |
| final Event event = verifyEvents.get(1); |
| |
| assertThat(event.getNoSaveUiReason()).isEqualTo(NO_SAVE_UI_REASON_FIELD_VALIDATION_FAILED); |
| } |
| |
| /** |
| * Tests scenario where the context was committed, the save dialog was not shown because all |
| * fields matched contents of datasets. |
| */ |
| @Test |
| public void testContextCommitted_noSaveUi_whileFieldsMatchedDatasets() throws Exception { |
| enableService(); |
| |
| // Set expectations. |
| final CannedFillResponse.Builder builder = createTestResponseBuilder(); |
| builder.setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD); |
| sReplier.addResponse(builder.build()); |
| |
| // Trigger autofill and set the save UI not show reason with |
| // NO_SAVE_UI_REASON_DATASET_MATCH. |
| triggerAutofillForSaveUiCondition(NO_SAVE_UI_REASON_DATASET_MATCH); |
| |
| // Finish the context by login in and it will trigger to check if the save UI should be |
| // shown. |
| tapLogin(); |
| |
| // Verify that the save UI should not be shown and the history should include the reason. |
| mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD); |
| |
| final List<Event> verifyEvents = InstrumentedAutoFillService.getFillEvents(2); |
| final Event event = verifyEvents.get(1); |
| |
| assertThat(event.getNoSaveUiReason()).isEqualTo(NO_SAVE_UI_REASON_DATASET_MATCH); |
| } |
| |
| private CannedFillResponse.Builder createTestResponseBuilder() { |
| return new CannedFillResponse.Builder() |
| .addDataset(new CannedDataset.Builder() |
| .setId("id1") |
| .setField(ID_USERNAME, BACKDOOR_USERNAME) |
| .setField(ID_PASSWORD, "whatever") |
| .setPresentation("dataset1", isInlineMode()) |
| .build()) |
| .setFillResponseFlags(FillResponse.FLAG_TRACK_CONTEXT_COMMITED); |
| } |
| |
| /** |
| * Triggers autofill on username first and set the behavior of the different conditions so that |
| * the save UI should not be shown. |
| */ |
| private void triggerAutofillForSaveUiCondition(int reason) throws Exception { |
| // Trigger autofill on username and check the suggestion is shown. |
| mUiBot.focusByRelativeId(ID_USERNAME); |
| mUiBot.waitForIdle(); |
| sReplier.getNextFillRequest(); |
| |
| mUiBot.assertDatasets("dataset1"); |
| |
| if (reason == NO_SAVE_UI_REASON_HAS_EMPTY_REQUIRED) { |
| // Set empty value on password to meet that there was empty value for required ids. |
| mActivity.onUsername((v) -> v.setText(BACKDOOR_USERNAME)); |
| mActivity.onPassword((v) -> v.setText("")); |
| } else if (reason == NO_SAVE_UI_REASON_NO_VALUE_CHANGED) { |
| // Select the suggestion to fill the data into username and password, then it will be |
| // able to get the data from ViewState.getCurrentValue() and |
| // ViewState.getAutofilledValue(). |
| mActivity.expectAutoFill(BACKDOOR_USERNAME, "whatever"); |
| mUiBot.selectDataset("dataset1"); |
| mActivity.assertAutoFilled(); |
| } else if (reason == NO_SAVE_UI_REASON_NO_SAVE_INFO |
| || reason == NO_SAVE_UI_REASON_WITH_DELAY_SAVE_FLAG |
| || reason == NO_SAVE_UI_REASON_FIELD_VALIDATION_FAILED |
| || reason == NO_SAVE_UI_REASON_DATASET_MATCH) { |
| // Use the setText to fill the data into username and password, then it will only be |
| // able to get the data from ViewState.getCurrentValue(), but get empty value from |
| // ViewState.getAutofilledValue(). |
| mActivity.onUsername((v) -> v.setText(BACKDOOR_USERNAME)); |
| mActivity.onPassword((v) -> v.setText("whatever")); |
| } else { |
| throw new AssertionError("Can not identify the reason"); |
| } |
| } |
| |
| private void tapLogin() throws Exception { |
| final String expectedMessage = getWelcomeMessage(BACKDOOR_USERNAME); |
| final String actualMessage = mActivity.tapLogin(); |
| assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage); |
| } |
| } |