| /* |
| * Copyright (C) 2017 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.saveui; |
| |
| import static android.autofillservice.cts.activities.LoginActivity.ID_USERNAME_CONTAINER; |
| import static android.autofillservice.cts.activities.SimpleSaveActivity.ID_COMMIT; |
| import static android.autofillservice.cts.activities.SimpleSaveActivity.ID_INPUT; |
| import static android.autofillservice.cts.activities.SimpleSaveActivity.ID_LABEL; |
| import static android.autofillservice.cts.activities.SimpleSaveActivity.ID_PASSWORD; |
| import static android.autofillservice.cts.activities.SimpleSaveActivity.TEXT_LABEL; |
| import static android.autofillservice.cts.testcore.AntiTrimmerTextWatcher.TRIMMER_PATTERN; |
| import static android.autofillservice.cts.testcore.Helper.ID_STATIC_TEXT; |
| import static android.autofillservice.cts.testcore.Helper.ID_USERNAME; |
| import static android.autofillservice.cts.testcore.Helper.LARGE_STRING; |
| import static android.autofillservice.cts.testcore.Helper.assertTextAndValue; |
| import static android.autofillservice.cts.testcore.Helper.assertTextValue; |
| import static android.autofillservice.cts.testcore.Helper.findAutofillIdByResourceId; |
| import static android.autofillservice.cts.testcore.Helper.findNodeByResourceId; |
| import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC; |
| import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD; |
| import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_USERNAME; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| import static com.google.common.truth.Truth.assertWithMessage; |
| |
| import static org.junit.Assume.assumeTrue; |
| |
| import android.app.assist.AssistStructure; |
| import android.app.assist.AssistStructure.ViewNode; |
| import android.autofillservice.cts.R; |
| import android.autofillservice.cts.activities.LoginActivity; |
| import android.autofillservice.cts.activities.SecondActivity; |
| import android.autofillservice.cts.activities.SimpleSaveActivity; |
| import android.autofillservice.cts.activities.SimpleSaveActivity.FillExpectation; |
| import android.autofillservice.cts.activities.TrampolineWelcomeActivity; |
| import android.autofillservice.cts.activities.ViewActionActivity; |
| import android.autofillservice.cts.activities.WelcomeActivity; |
| import android.autofillservice.cts.commontests.CustomDescriptionWithLinkTestCase; |
| import android.autofillservice.cts.testcore.AntiTrimmerTextWatcher; |
| import android.autofillservice.cts.testcore.AutofillActivityTestRule; |
| import android.autofillservice.cts.testcore.CannedFillResponse; |
| import android.autofillservice.cts.testcore.CannedFillResponse.CannedDataset; |
| import android.autofillservice.cts.testcore.DismissType; |
| import android.autofillservice.cts.testcore.Helper; |
| import android.autofillservice.cts.testcore.InstrumentedAutoFillService; |
| import android.autofillservice.cts.testcore.InstrumentedAutoFillService.FillRequest; |
| import android.autofillservice.cts.testcore.InstrumentedAutoFillService.SaveRequest; |
| import android.autofillservice.cts.testcore.MyAutofillCallback; |
| import android.autofillservice.cts.testcore.MyAutofillId; |
| import android.autofillservice.cts.testcore.Timeouts; |
| import android.autofillservice.cts.testcore.UiBot; |
| import android.content.Intent; |
| import android.graphics.Bitmap; |
| import android.os.Bundle; |
| import android.platform.test.annotations.AppModeFull; |
| import android.platform.test.annotations.Presubmit; |
| import android.service.autofill.BatchUpdates; |
| import android.service.autofill.CustomDescription; |
| import android.service.autofill.FillContext; |
| import android.service.autofill.FillEventHistory; |
| import android.service.autofill.RegexValidator; |
| import android.service.autofill.SaveInfo; |
| import android.service.autofill.TextValueSanitizer; |
| import android.service.autofill.Validator; |
| import android.support.test.uiautomator.By; |
| import android.support.test.uiautomator.UiObject2; |
| import android.text.Spannable; |
| import android.text.SpannableString; |
| import android.text.style.URLSpan; |
| import android.view.View; |
| import android.view.autofill.AutofillId; |
| import android.widget.RemoteViews; |
| |
| import org.junit.Test; |
| import org.junit.rules.RuleChain; |
| import org.junit.rules.TestRule; |
| |
| import java.util.regex.Pattern; |
| |
| public class SimpleSaveActivityTest extends CustomDescriptionWithLinkTestCase<SimpleSaveActivity> { |
| |
| private static final AutofillActivityTestRule<SimpleSaveActivity> sActivityRule = |
| new AutofillActivityTestRule<SimpleSaveActivity>(SimpleSaveActivity.class, false); |
| |
| private static final AutofillActivityTestRule<WelcomeActivity> sWelcomeActivityRule = |
| new AutofillActivityTestRule<WelcomeActivity>(WelcomeActivity.class, false); |
| |
| public SimpleSaveActivityTest() { |
| super(SimpleSaveActivity.class); |
| } |
| |
| @Override |
| protected AutofillActivityTestRule<SimpleSaveActivity> getActivityRule() { |
| return sActivityRule; |
| } |
| |
| @Override |
| protected TestRule getMainTestRule() { |
| return RuleChain.outerRule(sActivityRule).around(sWelcomeActivityRule); |
| } |
| |
| private void restartActivity() { |
| final Intent intent = new Intent(mContext.getApplicationContext(), |
| SimpleSaveActivity.class); |
| intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); |
| mActivity.startActivity(intent); |
| } |
| |
| @Presubmit |
| @Test |
| public void testAutoFillOneDatasetAndSave() throws Exception { |
| startActivity(); |
| |
| // Set service. |
| enableService(); |
| |
| // Set expectations. |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT, ID_PASSWORD) |
| .addDataset(new CannedDataset.Builder() |
| .setField(ID_INPUT, "id") |
| .setField(ID_PASSWORD, "pass") |
| .setPresentation(createPresentation("YO")) |
| .build()) |
| .build()); |
| |
| // Trigger autofill. |
| mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus()); |
| sReplier.getNextFillRequest(); |
| |
| // Select dataset. |
| final FillExpectation autofillExpecation = mActivity.expectAutoFill("id", "pass"); |
| mUiBot.selectDataset("YO"); |
| autofillExpecation.assertAutoFilled(); |
| |
| mActivity.syncRunOnUiThread(() -> { |
| mActivity.mInput.setText("ID"); |
| mActivity.mPassword.setText("PASS"); |
| mActivity.mCommit.performClick(); |
| }); |
| final UiObject2 saveUi = mUiBot.assertUpdateShowing(SAVE_DATA_TYPE_GENERIC); |
| |
| // Save it... |
| mUiBot.saveForAutofill(saveUi, true); |
| |
| // ... and assert results |
| final SaveRequest saveRequest = sReplier.getNextSaveRequest(); |
| assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "ID"); |
| assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_PASSWORD), "PASS"); |
| } |
| |
| @Test |
| @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough") |
| public void testAutoFillOneDatasetAndSave_largeAssistStructure() throws Exception { |
| startActivity(); |
| |
| mActivity.syncRunOnUiThread( |
| () -> mActivity.mInput.setAutofillHints(LARGE_STRING, LARGE_STRING, LARGE_STRING)); |
| |
| // Set service. |
| enableService(); |
| |
| // Set expectations. |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT, ID_PASSWORD) |
| .addDataset(new CannedDataset.Builder() |
| .setField(ID_INPUT, "id") |
| .setField(ID_PASSWORD, "pass") |
| .setPresentation(createPresentation("YO")) |
| .build()) |
| .build()); |
| |
| // Trigger autofill. |
| mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus()); |
| final FillRequest fillRequest = sReplier.getNextFillRequest(); |
| final ViewNode inputOnFill = findNodeByResourceId(fillRequest.structure, ID_INPUT); |
| final String[] hintsOnFill = inputOnFill.getAutofillHints(); |
| // Cannot compare these large strings directly becauise it could cause ANR |
| assertThat(hintsOnFill).hasLength(3); |
| Helper.assertEqualsToLargeString(hintsOnFill[0]); |
| Helper.assertEqualsToLargeString(hintsOnFill[1]); |
| Helper.assertEqualsToLargeString(hintsOnFill[2]); |
| |
| // Select dataset. |
| final FillExpectation autofillExpecation = mActivity.expectAutoFill("id", "pass"); |
| mUiBot.selectDataset("YO"); |
| autofillExpecation.assertAutoFilled(); |
| |
| mActivity.syncRunOnUiThread(() -> { |
| mActivity.mInput.setText("ID"); |
| mActivity.mPassword.setText("PASS"); |
| mActivity.mCommit.performClick(); |
| }); |
| final UiObject2 saveUi = mUiBot.assertUpdateShowing(SAVE_DATA_TYPE_GENERIC); |
| |
| // Save it... |
| mUiBot.saveForAutofill(saveUi, true); |
| |
| // ... and assert results |
| final SaveRequest saveRequest = sReplier.getNextSaveRequest(); |
| final ViewNode inputOnSave = findNodeByResourceId(saveRequest.structure, ID_INPUT); |
| assertTextAndValue(inputOnSave, "ID"); |
| assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_PASSWORD), "PASS"); |
| |
| final String[] hintsOnSave = inputOnSave.getAutofillHints(); |
| // Cannot compare these large strings directly becauise it could cause ANR |
| assertThat(hintsOnSave).hasLength(3); |
| Helper.assertEqualsToLargeString(hintsOnSave[0]); |
| Helper.assertEqualsToLargeString(hintsOnSave[1]); |
| Helper.assertEqualsToLargeString(hintsOnSave[2]); |
| } |
| |
| /** |
| * Simple test that only uses UiAutomator to interact with the activity, so it indirectly |
| * tests the integration of Autofill with Accessibility. |
| */ |
| @Test |
| public void testAutoFillOneDatasetAndSave_usingUiAutomatorOnly() throws Exception { |
| startActivity(); |
| |
| // Set service. |
| enableService(); |
| |
| // Set expectations. |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT, ID_PASSWORD) |
| .addDataset(new CannedDataset.Builder() |
| .setField(ID_INPUT, "id") |
| .setField(ID_PASSWORD, "pass") |
| .setPresentation(createPresentation("YO")) |
| .build()) |
| .build()); |
| |
| // Trigger autofill. |
| mUiBot.assertShownByRelativeId(ID_INPUT).click(); |
| sReplier.getNextFillRequest(); |
| |
| // Select dataset... |
| mUiBot.selectDataset("YO"); |
| |
| // ...and assert autofilled values. |
| final UiObject2 input = mUiBot.assertShownByRelativeId(ID_INPUT); |
| final UiObject2 password = mUiBot.assertShownByRelativeId(ID_PASSWORD); |
| |
| assertWithMessage("wrong value for 'input'").that(input.getText()).isEqualTo("id"); |
| // TODO: password field is shown as **** ; ideally we should assert it's a password |
| // field, but UiAutomator does not exposes that info. |
| final String visiblePassword = password.getText(); |
| assertWithMessage("'password' should not be visible").that(visiblePassword) |
| .isNotEqualTo("pass"); |
| assertWithMessage("wrong value for 'password'").that(visiblePassword).hasLength(4); |
| |
| // Trigger save... |
| input.setText("ID"); |
| password.setText("PASS"); |
| mUiBot.assertShownByRelativeId(ID_COMMIT).click(); |
| mUiBot.updateForAutofill(true, SAVE_DATA_TYPE_GENERIC); |
| |
| // ... and assert results |
| final SaveRequest saveRequest = sReplier.getNextSaveRequest(); |
| assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "ID"); |
| assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_PASSWORD), "PASS"); |
| } |
| |
| @Test |
| @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough") |
| public void testSave() throws Exception { |
| saveTest(false); |
| } |
| |
| @Presubmit |
| @Test |
| public void testSave_afterRotation() throws Exception { |
| assumeTrue("Rotation is supported", Helper.isRotationSupported(mContext)); |
| mUiBot.setScreenOrientation(UiBot.PORTRAIT); |
| try { |
| saveTest(true); |
| } finally { |
| try { |
| mUiBot.setScreenOrientation(UiBot.PORTRAIT); |
| cleanUpAfterScreenOrientationIsBackToPortrait(); |
| } catch (Exception e) { |
| mSafeCleanerRule.add(e); |
| } |
| } |
| } |
| |
| private void saveTest(boolean rotate) throws Exception { |
| startActivity(); |
| |
| // Set service. |
| enableService(); |
| |
| // Set expectations. |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT) |
| .build()); |
| |
| // Trigger autofill. |
| mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus()); |
| sReplier.getNextFillRequest(); |
| |
| // Trigger save. |
| mActivity.syncRunOnUiThread(() -> { |
| mActivity.mInput.setText("108"); |
| mActivity.mCommit.performClick(); |
| }); |
| UiObject2 saveUi = mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC); |
| |
| if (rotate) { |
| // After the device rotates, the input field get focus and generate a new session. |
| sReplier.addResponse(CannedFillResponse.NO_RESPONSE); |
| |
| mUiBot.setScreenOrientation(UiBot.LANDSCAPE); |
| saveUi = mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC); |
| } |
| |
| // Save it... |
| mUiBot.saveForAutofill(saveUi, true); |
| |
| // ... and assert results |
| final SaveRequest saveRequest = sReplier.getNextSaveRequest(); |
| assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "108"); |
| } |
| |
| /** |
| * Emulates an app dyanmically adding the password field after username is typed. |
| */ |
| @Test |
| @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough") |
| public void testPartitionedSave() throws Exception { |
| startActivity(); |
| |
| // Set service. |
| enableService(); |
| |
| // 1st request |
| |
| // Set expectations. |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setRequiredSavableIds(SAVE_DATA_TYPE_USERNAME, ID_INPUT) |
| .build()); |
| |
| // Trigger autofill. |
| mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus()); |
| sReplier.getNextFillRequest(); |
| |
| // Set 1st field but don't commit session |
| mActivity.syncRunOnUiThread(() -> mActivity.mInput.setText("108")); |
| mUiBot.assertSaveNotShowing(); |
| |
| // 2nd request |
| |
| // Set expectations. |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setRequiredSavableIds(SAVE_DATA_TYPE_USERNAME | SAVE_DATA_TYPE_PASSWORD, |
| ID_INPUT, ID_PASSWORD) |
| .build()); |
| |
| // Trigger autofill. |
| mActivity.syncRunOnUiThread(() -> mActivity.mPassword.requestFocus()); |
| sReplier.getNextFillRequest(); |
| |
| // Trigger save. |
| mActivity.syncRunOnUiThread(() -> { |
| mActivity.mPassword.setText("42"); |
| mActivity.mCommit.performClick(); |
| }); |
| final UiObject2 saveUi = mUiBot.assertSaveShowing(null, SAVE_DATA_TYPE_USERNAME, |
| SAVE_DATA_TYPE_PASSWORD); |
| |
| // Save it... |
| mUiBot.saveForAutofill(saveUi, true); |
| |
| // ... and assert results |
| final SaveRequest saveRequest = sReplier.getNextSaveRequest(); |
| assertThat(saveRequest.contexts.size()).isEqualTo(2); |
| |
| assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "108"); |
| assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_PASSWORD), "42"); |
| } |
| |
| /** |
| * Emulates an app using fragments to display username and password in 2 steps. |
| */ |
| @Test |
| @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough") |
| public void testDelayedSave() throws Exception { |
| startActivity(); |
| |
| // Set service. |
| enableService(); |
| |
| // 1st fragment. |
| |
| // Set expectations. |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setSaveInfoFlags(SaveInfo.FLAG_DELAY_SAVE).build()); |
| |
| // Trigger autofill. |
| mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus()); |
| sReplier.getNextFillRequest(); |
| |
| // Trigger delayed save. |
| mActivity.syncRunOnUiThread(() -> { |
| mActivity.mInput.setText("108"); |
| mActivity.mCommit.performClick(); |
| }); |
| mUiBot.assertSaveNotShowing(); |
| |
| // 2nd fragment. |
| |
| // Set expectations. |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| // Must explicitly set visitor, otherwise setRequiredSavableIds() would get the |
| // id from the 1st context |
| .setVisitor((contexts, builder) -> { |
| final AutofillId passwordId = |
| findAutofillIdByResourceId(contexts.get(1), ID_PASSWORD); |
| final AutofillId inputId = |
| findAutofillIdByResourceId(contexts.get(0), ID_INPUT); |
| builder.setSaveInfo(new SaveInfo.Builder( |
| SAVE_DATA_TYPE_USERNAME | SAVE_DATA_TYPE_PASSWORD, |
| new AutofillId[] {inputId, passwordId}) |
| .build()); |
| }) |
| .build()); |
| |
| // Trigger autofill on second "fragment" |
| mActivity.syncRunOnUiThread(() -> mActivity.mPassword.requestFocus()); |
| sReplier.getNextFillRequest(); |
| |
| // Trigger delayed save. |
| mActivity.syncRunOnUiThread(() -> { |
| mActivity.mPassword.setText("42"); |
| mActivity.mCommit.performClick(); |
| }); |
| |
| // Save it... |
| mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_USERNAME, SAVE_DATA_TYPE_PASSWORD); |
| |
| // ... and assert results |
| final SaveRequest saveRequest = sReplier.getNextSaveRequest(); |
| assertThat(saveRequest.contexts.size()).isEqualTo(2); |
| |
| // Get username from 1st request. |
| final AssistStructure structure1 = saveRequest.contexts.get(0).getStructure(); |
| assertTextAndValue(findNodeByResourceId(structure1, ID_INPUT), "108"); |
| |
| // Get password from 2nd request. |
| final AssistStructure structure2 = saveRequest.contexts.get(1).getStructure(); |
| assertTextAndValue(findNodeByResourceId(structure2, ID_INPUT), "108"); |
| assertTextAndValue(findNodeByResourceId(structure2, ID_PASSWORD), "42"); |
| } |
| |
| @Presubmit |
| @Test |
| public void testSave_launchIntent() throws Exception { |
| startActivity(); |
| |
| // Set service. |
| enableService(); |
| |
| // Set expectations. |
| sReplier.setOnSave(WelcomeActivity.createSender(mContext, "Saved by the bell")) |
| .addResponse(new CannedFillResponse.Builder() |
| .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT) |
| .build()); |
| |
| // Trigger autofill. |
| mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus()); |
| sReplier.getNextFillRequest(); |
| |
| // Trigger save. |
| mActivity.syncRunOnUiThread(() -> { |
| mActivity.mInput.setText("108"); |
| mActivity.mCommit.performClick(); |
| |
| // Disable autofill so it's not triggered again after WelcomeActivity finishes |
| // and mActivity is resumed (with focus on mInput) after the session is closed |
| mActivity.mInput.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO); |
| }); |
| |
| // Save it... |
| mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_GENERIC); |
| sReplier.getNextSaveRequest(); |
| |
| // ... and assert activity was launched |
| WelcomeActivity.assertShowing(mUiBot, "Saved by the bell"); |
| } |
| |
| @Presubmit |
| @Test |
| public void testSaveThenStartNewSessionRightAwayShouldKeepSaveUi() throws Exception { |
| startActivity(); |
| |
| // Set service. |
| enableService(); |
| |
| // Set expectations. |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT) |
| .build()); |
| |
| // Trigger autofill. |
| mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus()); |
| sReplier.getNextFillRequest(); |
| |
| // Trigger save. |
| mActivity.syncRunOnUiThread(() -> { |
| mActivity.mInput.setText("108"); |
| mActivity.mCommit.performClick(); |
| }); |
| |
| // Make sure Save UI for 1st session was shown.... |
| mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC); |
| |
| // Start new Activity to have a new autofill session |
| startActivityOnNewTask(LoginActivity.class); |
| |
| // Make sure LoginActivity started... |
| mUiBot.assertShownByRelativeId(ID_USERNAME_CONTAINER); |
| |
| // Set expectations. |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_USERNAME) |
| .addDataset(new CannedDataset.Builder() |
| .setField(ID_USERNAME, "id") |
| .setField(ID_PASSWORD, "pwd") |
| .setPresentation(createPresentation("YO")) |
| .build()) |
| .build()); |
| // Trigger fill request on the LoginActivity |
| final LoginActivity act = LoginActivity.getCurrentActivity(); |
| act.syncRunOnUiThread(() -> act.forceAutofillOnUsername()); |
| sReplier.getNextFillRequest(); |
| |
| // Make sure Fill UI is not shown. And Save UI for 1st session was still shown. |
| mUiBot.assertNoDatasetsEver(); |
| sReplier.assertNoUnhandledFillRequests(); |
| mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC); |
| |
| mUiBot.waitForIdle(); |
| // Trigger dismiss Save UI |
| mUiBot.pressBack(); |
| |
| // Make sure Save UI was not shown.... |
| mUiBot.assertSaveNotShowing(); |
| // Make sure Fill UI is shown. |
| mUiBot.assertDatasets("YO"); |
| } |
| |
| @Presubmit |
| @Test |
| public void testCloseSaveUiThenStartNewSessionRightAway() throws Exception { |
| startActivity(); |
| |
| // Set service. |
| enableService(); |
| |
| // Set expectations. |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT) |
| .build()); |
| |
| // Trigger autofill. |
| mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus()); |
| sReplier.getNextFillRequest(); |
| |
| // Trigger save. |
| mActivity.syncRunOnUiThread(() -> { |
| mActivity.mInput.setText("108"); |
| mActivity.mCommit.performClick(); |
| }); |
| |
| // Make sure Save UI for 1st session was shown.... |
| mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC); |
| |
| // Trigger dismiss Save UI |
| mUiBot.pressBack(); |
| |
| // Make sure Save UI for 1st session was canceled. |
| mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC); |
| |
| // ...then start the new session right away (without finishing the activity). |
| // Set expectations. |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT) |
| .addDataset(new CannedDataset.Builder() |
| .setField(ID_INPUT, "id") |
| .setPresentation(createPresentation("YO")) |
| .build()) |
| .build()); |
| mActivity.syncRunOnUiThread(() -> { |
| mActivity.mInput.setText(""); |
| mActivity.getAutofillManager().requestAutofill(mActivity.mInput); |
| }); |
| sReplier.getNextFillRequest(); |
| |
| // Make sure Fill UI is shown. |
| mUiBot.assertDatasets("YO"); |
| } |
| |
| @Presubmit |
| @Test |
| public void testSaveWithParcelableOnClientState() throws Exception { |
| startActivity(); |
| |
| // Set service. |
| enableService(); |
| |
| // Set expectations. |
| final AutofillId id = new AutofillId(42); |
| final Bundle clientState = new Bundle(); |
| clientState.putParcelable("id", id); |
| clientState.putParcelable("my_id", new MyAutofillId(id)); |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT) |
| .setExtras(clientState) |
| .build()); |
| |
| // Trigger autofill. |
| mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus()); |
| sReplier.getNextFillRequest(); |
| |
| // Trigger save. |
| mActivity.syncRunOnUiThread(() -> { |
| mActivity.mInput.setText("108"); |
| mActivity.mCommit.performClick(); |
| }); |
| UiObject2 saveUi = mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC); |
| |
| // Save it... |
| mUiBot.saveForAutofill(saveUi, true); |
| |
| // ... and assert results |
| final SaveRequest saveRequest = sReplier.getNextSaveRequest(); |
| assertMyClientState(saveRequest.data); |
| |
| // Also check fillevent history |
| final FillEventHistory history = InstrumentedAutoFillService.getFillEventHistory(1); |
| @SuppressWarnings("deprecation") |
| final Bundle deprecatedState = history.getClientState(); |
| assertMyClientState(deprecatedState); |
| assertMyClientState(history.getEvents().get(0).getClientState()); |
| } |
| |
| private void assertMyClientState(Bundle data) { |
| // Must set proper classpath before reading the data, otherwise Bundle will use it's |
| // on class classloader, which is the framework's. |
| data.setClassLoader(getClass().getClassLoader()); |
| |
| final AutofillId expectedId = new AutofillId(42); |
| final AutofillId actualId = data.getParcelable("id"); |
| assertThat(actualId).isEqualTo(expectedId); |
| final MyAutofillId actualMyId = data.getParcelable("my_id"); |
| assertThat(actualMyId).isEqualTo(new MyAutofillId(expectedId)); |
| } |
| |
| @Presubmit |
| @Test |
| public void testCancelPreventsSaveUiFromShowing() throws Exception { |
| startActivity(); |
| |
| // Set service. |
| enableService(); |
| |
| // Set expectations. |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT) |
| .build()); |
| |
| // Trigger autofill. |
| mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus()); |
| sReplier.getNextFillRequest(); |
| |
| // Cancel session. |
| mActivity.getAutofillManager().cancel(); |
| |
| // Trigger save. |
| mActivity.syncRunOnUiThread(() -> { |
| mActivity.mInput.setText("108"); |
| mActivity.mCommit.performClick(); |
| }); |
| |
| // Assert it's not showing. |
| mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC); |
| } |
| |
| @Presubmit |
| @Test |
| public void testDismissSave_byTappingBack() throws Exception { |
| startActivity(); |
| dismissSaveTest(DismissType.BACK_BUTTON); |
| } |
| |
| @Test |
| public void testDismissSave_byTappingHome() throws Exception { |
| startActivity(); |
| dismissSaveTest(DismissType.HOME_BUTTON); |
| } |
| |
| @Presubmit |
| @Test |
| public void testDismissSave_byTouchingOutside() throws Exception { |
| startActivity(); |
| dismissSaveTest(DismissType.TOUCH_OUTSIDE); |
| } |
| |
| @Presubmit |
| @Test |
| public void testDismissSave_byFocusingOutside() throws Exception { |
| startActivity(); |
| dismissSaveTest(DismissType.FOCUS_OUTSIDE); |
| } |
| |
| private void dismissSaveTest(DismissType dismissType) throws Exception { |
| // Set service. |
| enableService(); |
| |
| // Set expectations. |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT) |
| .build()); |
| |
| // Trigger autofill. |
| mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus()); |
| sReplier.getNextFillRequest(); |
| |
| // Trigger save. |
| mActivity.syncRunOnUiThread(() -> { |
| mActivity.mInput.setText("108"); |
| mActivity.mCommit.performClick(); |
| }); |
| mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC); |
| |
| // Then make sure it goes away when user doesn't want it.. |
| switch (dismissType) { |
| case BACK_BUTTON: |
| mUiBot.pressBack(); |
| break; |
| case HOME_BUTTON: |
| mUiBot.pressHome(); |
| break; |
| case TOUCH_OUTSIDE: |
| mUiBot.assertShownByText(TEXT_LABEL).click(); |
| break; |
| case FOCUS_OUTSIDE: |
| mActivity.syncRunOnUiThread(() -> mActivity.mLabel.requestFocus()); |
| mUiBot.assertShownByText(TEXT_LABEL).click(); |
| break; |
| default: |
| throw new IllegalArgumentException("invalid dismiss type: " + dismissType); |
| } |
| mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC); |
| } |
| |
| @Presubmit |
| @Test |
| public void testTapHomeWhileDatasetPickerUiIsShowing() throws Exception { |
| startActivity(); |
| enableService(); |
| final MyAutofillCallback callback = mActivity.registerCallback(); |
| |
| // Set expectations. |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .addDataset(new CannedDataset.Builder() |
| .setField(ID_INPUT, "id") |
| .setField(ID_PASSWORD, "pass") |
| .setPresentation(createPresentation("YO")) |
| .build()) |
| .build()); |
| |
| // Trigger autofill. |
| mUiBot.assertShownByRelativeId(ID_INPUT).click(); |
| sReplier.getNextFillRequest(); |
| mUiBot.assertDatasets("YO"); |
| callback.assertUiShownEvent(mActivity.mInput); |
| |
| // Go home, you are drunk! |
| mUiBot.pressHome(); |
| mUiBot.assertNoDatasets(); |
| callback.assertUiHiddenEvent(mActivity.mInput); |
| |
| // Set expectations. |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .addDataset(new CannedDataset.Builder() |
| .setField(ID_INPUT, "id") |
| .setField(ID_PASSWORD, "pass") |
| .setPresentation(createPresentation("YO2")) |
| .build()) |
| .build()); |
| |
| // Switch back to the activity. |
| restartActivity(); |
| mUiBot.assertShownByText(TEXT_LABEL, Timeouts.ACTIVITY_RESURRECTION); |
| sReplier.getNextFillRequest(); |
| final UiObject2 datasetPicker = mUiBot.assertDatasets("YO2"); |
| callback.assertUiShownEvent(mActivity.mInput); |
| |
| // Now autofill it. |
| final FillExpectation autofillExpecation = mActivity.expectAutoFill("id", "pass"); |
| mUiBot.selectDataset(datasetPicker, "YO2"); |
| autofillExpecation.assertAutoFilled(); |
| } |
| |
| @Presubmit |
| @Test |
| public void testTapHomeWhileSaveUiIsShowing() throws Exception { |
| startActivity(); |
| enableService(); |
| |
| // Set expectations. |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT) |
| .build()); |
| |
| // Trigger autofill. |
| mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus()); |
| sReplier.getNextFillRequest(); |
| mUiBot.assertNoDatasetsEver(); |
| |
| // Trigger save, but don't tap it. |
| mActivity.syncRunOnUiThread(() -> { |
| mActivity.mInput.setText("108"); |
| mActivity.mCommit.performClick(); |
| }); |
| mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC); |
| |
| // Go home, you are drunk! |
| mUiBot.pressHome(); |
| mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC); |
| |
| // Prepare the response for the next session, which will be automatically triggered |
| // when the activity is brought back. |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT, ID_PASSWORD) |
| .addDataset(new CannedDataset.Builder() |
| .setField(ID_INPUT, "id") |
| .setField(ID_PASSWORD, "pass") |
| .setPresentation(createPresentation("YO")) |
| .build()) |
| .build()); |
| |
| // Switch back to the activity. |
| restartActivity(); |
| mUiBot.assertShownByText(TEXT_LABEL, Timeouts.ACTIVITY_RESURRECTION); |
| mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC); |
| sReplier.getNextFillRequest(); |
| mUiBot.assertNoDatasetsEver(); |
| |
| // Trigger and select UI. |
| mActivity.syncRunOnUiThread(() -> mActivity.mPassword.requestFocus()); |
| final FillExpectation autofillExpecation = mActivity.expectAutoFill("id", "pass"); |
| mUiBot.selectDataset("YO"); |
| |
| // Assert it. |
| autofillExpecation.assertAutoFilled(); |
| } |
| |
| @Override |
| protected void saveUiRestoredAfterTappingLinkTest(PostSaveLinkTappedAction type) |
| throws Exception { |
| startActivity(); |
| // Set service. |
| enableService(); |
| |
| // Set expectations. |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT) |
| .setSaveInfoVisitor((contexts, builder) -> builder |
| .setCustomDescription(newCustomDescription(WelcomeActivity.class))) |
| .build()); |
| |
| // Trigger autofill. |
| mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus()); |
| sReplier.getNextFillRequest(); |
| |
| // Trigger save. |
| mActivity.syncRunOnUiThread(() -> { |
| mActivity.mInput.setText("108"); |
| mActivity.mCommit.performClick(); |
| }); |
| final UiObject2 saveUi = assertSaveUiWithLinkIsShown(SAVE_DATA_TYPE_GENERIC); |
| |
| // Tap the link. |
| tapSaveUiLink(saveUi); |
| |
| // Make sure new activity is shown... |
| WelcomeActivity.assertShowingDefaultMessage(mUiBot); |
| mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC); |
| |
| // .. then do something to return to previous activity... |
| switch (type) { |
| case ROTATE_THEN_TAP_BACK_BUTTON: |
| // After the device rotates, the input field get focus and generate a new session. |
| sReplier.addResponse(CannedFillResponse.NO_RESPONSE); |
| |
| mUiBot.setScreenOrientation(UiBot.LANDSCAPE); |
| WelcomeActivity.assertShowingDefaultMessage(mUiBot); |
| // not breaking on purpose |
| case TAP_BACK_BUTTON: |
| // ..then go back and save it. |
| mUiBot.pressBack(); |
| break; |
| case FINISH_ACTIVITY: |
| // ..then finishes it. |
| WelcomeActivity.finishIt(); |
| break; |
| default: |
| throw new IllegalArgumentException("invalid type: " + type); |
| } |
| // Make sure previous activity is back... |
| mUiBot.assertShownByRelativeId(ID_INPUT); |
| |
| // ... and tap save. |
| final UiObject2 newSaveUi = assertSaveUiWithLinkIsShown(SAVE_DATA_TYPE_GENERIC); |
| mUiBot.saveForAutofill(newSaveUi, true); |
| |
| final SaveRequest saveRequest = sReplier.getNextSaveRequest(); |
| assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "108"); |
| } |
| |
| @Override |
| protected void cleanUpAfterScreenOrientationIsBackToPortrait() throws Exception { |
| sReplier.getNextFillRequest(); |
| } |
| |
| @Override |
| protected void tapLinkThenTapBackThenStartOverTest(PostSaveLinkTappedAction action, |
| boolean manualRequest) throws Exception { |
| startActivity(); |
| // Set service. |
| enableService(); |
| |
| // Set expectations. |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT) |
| .setSaveInfoVisitor((contexts, builder) -> builder |
| .setCustomDescription(newCustomDescription(WelcomeActivity.class))) |
| .build()); |
| |
| // Trigger autofill. |
| mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus()); |
| sReplier.getNextFillRequest(); |
| |
| // Trigger save. |
| mActivity.syncRunOnUiThread(() -> { |
| mActivity.mInput.setText("108"); |
| mActivity.mCommit.performClick(); |
| }); |
| final UiObject2 saveUi = assertSaveUiWithLinkIsShown(SAVE_DATA_TYPE_GENERIC); |
| |
| // Tap the link. |
| tapSaveUiLink(saveUi); |
| |
| // Make sure new activity is shown. |
| WelcomeActivity.assertShowingDefaultMessage(mUiBot); |
| mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC); |
| |
| // Tap back to restore the Save UI... |
| mUiBot.pressBack(); |
| // Make sure previous activity is back... |
| mUiBot.assertShownByRelativeId(ID_LABEL); |
| |
| // ...but don't tap it... |
| final UiObject2 saveUi2 = mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC); |
| |
| // ...instead, do something to dismiss it: |
| switch (action) { |
| case TOUCH_OUTSIDE: |
| mUiBot.assertShownByRelativeId(ID_LABEL).longClick(); |
| break; |
| case TAP_NO_ON_SAVE_UI: |
| mUiBot.saveForAutofill(saveUi2, false); |
| break; |
| case TAP_YES_ON_SAVE_UI: |
| mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_GENERIC); |
| final SaveRequest saveRequest = sReplier.getNextSaveRequest(); |
| assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "108"); |
| break; |
| default: |
| throw new IllegalArgumentException("invalid action: " + action); |
| } |
| mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC); |
| |
| // Now triggers a new session and do business as usual... |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT) |
| .build()); |
| |
| // Trigger autofill. |
| if (manualRequest) { |
| mActivity.syncRunOnUiThread( |
| () -> mActivity.getAutofillManager().requestAutofill(mActivity.mInput)); |
| } else { |
| mActivity.syncRunOnUiThread(() -> mActivity.mPassword.requestFocus()); |
| } |
| |
| sReplier.getNextFillRequest(); |
| |
| // Trigger save. |
| mActivity.syncRunOnUiThread(() -> { |
| mActivity.mInput.setText("42"); |
| mActivity.mCommit.performClick(); |
| }); |
| |
| // Save it... |
| mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_GENERIC); |
| |
| // ... and assert results |
| final SaveRequest saveRequest = sReplier.getNextSaveRequest(); |
| assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "42"); |
| } |
| |
| @Override |
| protected void saveUiCancelledAfterTappingLinkTest(PostSaveLinkTappedAction type) |
| throws Exception { |
| startActivity(false); |
| // Set service. |
| enableService(); |
| |
| // Set expectations. |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT) |
| .setSaveInfoVisitor((contexts, builder) -> builder |
| .setCustomDescription(newCustomDescription(WelcomeActivity.class))) |
| .build()); |
| |
| // Trigger autofill. |
| mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus()); |
| sReplier.getNextFillRequest(); |
| |
| // Trigger save. |
| mActivity.syncRunOnUiThread(() -> { |
| mActivity.mInput.setText("108"); |
| mActivity.mCommit.performClick(); |
| }); |
| final UiObject2 saveUi = assertSaveUiWithLinkIsShown(SAVE_DATA_TYPE_GENERIC); |
| |
| // Tap the link. |
| tapSaveUiLink(saveUi); |
| // Make sure new activity is shown... |
| WelcomeActivity.assertShowingDefaultMessage(mUiBot); |
| mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC); |
| |
| switch (type) { |
| case LAUNCH_PREVIOUS_ACTIVITY: |
| startActivityOnNewTask(SimpleSaveActivity.class); |
| break; |
| case LAUNCH_NEW_ACTIVITY: |
| // Launch a 3rd activity... |
| startActivityOnNewTask(LoginActivity.class); |
| mUiBot.assertShownByRelativeId(ID_USERNAME_CONTAINER); |
| // ...then go back |
| mUiBot.pressBack(); |
| break; |
| default: |
| throw new IllegalArgumentException("invalid type: " + type); |
| } |
| // Make sure right activity is showing |
| mUiBot.assertShownByRelativeId(ID_INPUT, Timeouts.ACTIVITY_RESURRECTION); |
| |
| mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC); |
| } |
| |
| @Presubmit |
| @Test |
| @AppModeFull(reason = "Service-specific test") |
| public void testSelectedDatasetsAreSentOnSaveRequest() throws Exception { |
| startActivity(); |
| |
| // Set service. |
| enableService(); |
| |
| // Set expectations. |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT, ID_PASSWORD) |
| // Added on reversed order on purpose |
| .addDataset(new CannedDataset.Builder() |
| .setId("D2") |
| .setField(ID_INPUT, "id again") |
| .setField(ID_PASSWORD, "pass") |
| .setPresentation(createPresentation("D2")) |
| .build()) |
| .addDataset(new CannedDataset.Builder() |
| .setId("D1") |
| .setField(ID_INPUT, "id") |
| .setPresentation(createPresentation("D1")) |
| .build()) |
| .build()); |
| |
| // Trigger autofill. |
| mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus()); |
| sReplier.getNextFillRequest(); |
| |
| // Select 1st dataset. |
| final FillExpectation autofillExpecation1 = mActivity.expectAutoFill("id"); |
| final UiObject2 picker1 = mUiBot.assertDatasets("D2", "D1"); |
| mUiBot.selectDataset(picker1, "D1"); |
| autofillExpecation1.assertAutoFilled(); |
| |
| // Select 2nd dataset. |
| mActivity.syncRunOnUiThread(() -> mActivity.mPassword.requestFocus()); |
| final FillExpectation autofillExpecation2 = mActivity.expectAutoFill("id again", "pass"); |
| final UiObject2 picker2 = mUiBot.assertDatasets("D2"); |
| mUiBot.selectDataset(picker2, "D2"); |
| autofillExpecation2.assertAutoFilled(); |
| |
| mActivity.syncRunOnUiThread(() -> { |
| mActivity.mInput.setText("ID"); |
| mActivity.mPassword.setText("PASS"); |
| mActivity.mCommit.performClick(); |
| }); |
| final UiObject2 saveUi = mUiBot.assertUpdateShowing(SAVE_DATA_TYPE_GENERIC); |
| |
| // Save it... |
| mUiBot.saveForAutofill(saveUi, true); |
| |
| // ... and assert results |
| final SaveRequest saveRequest = sReplier.getNextSaveRequest(); |
| assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "ID"); |
| assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_PASSWORD), "PASS"); |
| assertThat(saveRequest.datasetIds).containsExactly("D1", "D2").inOrder(); |
| } |
| |
| @Override |
| protected void tapLinkLaunchTrampolineActivityThenTapBackAndStartNewSessionTest() |
| throws Exception { |
| // Prepare activity. |
| startActivity(); |
| mActivity.syncRunOnUiThread(() -> mActivity.mInput.getRootView() |
| .setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS) |
| ); |
| |
| // Set service. |
| enableService(); |
| |
| // Set expectations. |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT) |
| .setSaveInfoVisitor((contexts, builder) -> builder |
| .setCustomDescription( |
| newCustomDescription(TrampolineWelcomeActivity.class))) |
| .build()); |
| |
| // Trigger autofill. |
| mActivity.syncRunOnUiThread( |
| () -> mActivity.getAutofillManager().requestAutofill(mActivity.mInput)); |
| sReplier.getNextFillRequest(); |
| |
| // Trigger save. |
| mActivity.syncRunOnUiThread(() -> { |
| mActivity.mInput.setText("108"); |
| mActivity.mCommit.performClick(); |
| }); |
| final UiObject2 saveUi = assertSaveUiWithLinkIsShown(SAVE_DATA_TYPE_GENERIC); |
| |
| // Tap the link. |
| tapSaveUiLink(saveUi); |
| |
| // Make sure new activity is shown... |
| WelcomeActivity.assertShowingDefaultMessage(mUiBot); |
| |
| // Save UI should be showing as well, since Trampoline finished. |
| mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC); |
| |
| // Dismiss Save Dialog |
| mUiBot.pressBack(); |
| // Go back and make sure it's showing the right activity. |
| mUiBot.pressBack(); |
| mUiBot.assertShownByRelativeId(ID_LABEL); |
| |
| // Now start a new session. |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_PASSWORD) |
| .build()); |
| |
| // Trigger autofill on password |
| mActivity.syncRunOnUiThread( |
| () -> mActivity.getAutofillManager().requestAutofill(mActivity.mPassword)); |
| sReplier.getNextFillRequest(); |
| |
| mActivity.syncRunOnUiThread(() -> { |
| mActivity.mPassword.setText("42"); |
| mActivity.mCommit.performClick(); |
| }); |
| mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD); |
| final SaveRequest saveRequest = sReplier.getNextSaveRequest(); |
| assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "108"); |
| assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_PASSWORD), "42"); |
| } |
| |
| @Presubmit |
| @Test |
| public void testSanitizeOnSaveWhenAppChangeValues() throws Exception { |
| startActivity(); |
| |
| // Set listeners that will change the saved value |
| new AntiTrimmerTextWatcher(mActivity.mInput); |
| new AntiTrimmerTextWatcher(mActivity.mPassword); |
| |
| // Set service. |
| enableService(); |
| |
| // Set expectations. |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT) |
| .setSaveInfoVisitor((contexts, builder) -> { |
| final FillContext context = contexts.get(0); |
| final AutofillId inputId = findAutofillIdByResourceId(context, ID_INPUT); |
| final AutofillId passwordId = findAutofillIdByResourceId(context, ID_PASSWORD); |
| builder.addSanitizer(new TextValueSanitizer(TRIMMER_PATTERN, "$1"), inputId, |
| passwordId); |
| }) |
| .build()); |
| |
| // Trigger autofill. |
| mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus()); |
| sReplier.getNextFillRequest(); |
| |
| // Trigger save. |
| mActivity.syncRunOnUiThread(() -> { |
| mActivity.mInput.setText("id"); |
| mActivity.mPassword.setText("pass"); |
| mActivity.mCommit.performClick(); |
| }); |
| |
| // Save it... |
| mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_GENERIC); |
| |
| // ... and assert results |
| final SaveRequest saveRequest = sReplier.getNextSaveRequest(); |
| assertTextValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "id"); |
| assertTextValue(findNodeByResourceId(saveRequest.structure, ID_PASSWORD), "pass"); |
| } |
| |
| @Test |
| @AppModeFull(reason = "testSanitizeOnSaveWhenAppChangeValues() is enough") |
| public void testSanitizeOnSaveNoChange() throws Exception { |
| startActivity(); |
| |
| // Set service. |
| enableService(); |
| |
| // Set expectations. |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT) |
| .setOptionalSavableIds(ID_PASSWORD) |
| .setSaveInfoVisitor((contexts, builder) -> { |
| final FillContext context = contexts.get(0); |
| final AutofillId inputId = findAutofillIdByResourceId(context, ID_INPUT); |
| final AutofillId passwordId = findAutofillIdByResourceId(context, ID_PASSWORD); |
| builder.addSanitizer(new TextValueSanitizer(TRIMMER_PATTERN, "$1"), inputId, |
| passwordId); |
| }) |
| .build()); |
| |
| // Trigger autofill. |
| mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus()); |
| sReplier.getNextFillRequest(); |
| mUiBot.assertNoDatasetsEver(); |
| |
| // Trigger save. |
| mActivity.syncRunOnUiThread(() -> { |
| mActivity.mInput.setText("#id#"); |
| mActivity.mPassword.setText("#pass#"); |
| mActivity.mCommit.performClick(); |
| }); |
| |
| // Save it... |
| mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_GENERIC); |
| |
| // ... and assert results |
| final SaveRequest saveRequest = sReplier.getNextSaveRequest(); |
| assertTextValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "id"); |
| assertTextValue(findNodeByResourceId(saveRequest.structure, ID_PASSWORD), "pass"); |
| } |
| |
| @Test |
| @AppModeFull(reason = "testSanitizeOnSaveWhenAppChangeValues() is enough") |
| public void testDontSaveWhenSanitizedValueForRequiredFieldDidntChange() throws Exception { |
| startActivity(); |
| |
| // Set listeners that will change the saved value |
| new AntiTrimmerTextWatcher(mActivity.mInput); |
| new AntiTrimmerTextWatcher(mActivity.mPassword); |
| |
| // Set service. |
| enableService(); |
| |
| // Set expectations. |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT, ID_PASSWORD) |
| .setSaveInfoVisitor((contexts, builder) -> { |
| final FillContext context = contexts.get(0); |
| final AutofillId inputId = findAutofillIdByResourceId(context, ID_INPUT); |
| final AutofillId passwordId = findAutofillIdByResourceId(context, ID_PASSWORD); |
| builder.addSanitizer(new TextValueSanitizer(TRIMMER_PATTERN, "$1"), inputId, |
| passwordId); |
| }) |
| .addDataset(new CannedDataset.Builder() |
| .setField(ID_INPUT, "id") |
| .setField(ID_PASSWORD, "pass") |
| .setPresentation(createPresentation("YO")) |
| .build()) |
| .build()); |
| |
| // Trigger autofill. |
| mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus()); |
| sReplier.getNextFillRequest(); |
| |
| mActivity.syncRunOnUiThread(() -> { |
| mActivity.mInput.setText("id"); |
| mActivity.mPassword.setText("pass"); |
| mActivity.mCommit.performClick(); |
| }); |
| |
| mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC); |
| } |
| |
| @Test |
| @AppModeFull(reason = "testSanitizeOnSaveWhenAppChangeValues() is enough") |
| public void testDontSaveWhenSanitizedValueForOptionalFieldDidntChange() throws Exception { |
| startActivity(); |
| |
| // Set service. |
| enableService(); |
| |
| // Set expectations. |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT) |
| .setOptionalSavableIds(ID_PASSWORD) |
| .setSaveInfoVisitor((contexts, builder) -> { |
| final FillContext context = contexts.get(0); |
| final AutofillId passwordId = findAutofillIdByResourceId(context, ID_PASSWORD); |
| builder.addSanitizer(new TextValueSanitizer(Pattern.compile("(pass) "), "$1"), |
| passwordId); |
| }) |
| .addDataset(new CannedDataset.Builder() |
| .setField(ID_INPUT, "id") |
| .setField(ID_PASSWORD, "pass") |
| .setPresentation(createPresentation("YO")) |
| .build()) |
| .build()); |
| |
| // Trigger autofill. |
| mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus()); |
| sReplier.getNextFillRequest(); |
| |
| mActivity.syncRunOnUiThread(() -> { |
| mActivity.mInput.setText("id"); |
| mActivity.mPassword.setText("#pass#"); |
| mActivity.mCommit.performClick(); |
| }); |
| |
| mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC); |
| } |
| |
| @Test |
| @AppModeFull(reason = "testSanitizeOnSaveWhenAppChangeValues() is enough") |
| public void testDontSaveWhenRequiredFieldFailedSanitization() throws Exception { |
| startActivity(); |
| |
| // Set service. |
| enableService(); |
| |
| // Set expectations. |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT, ID_PASSWORD) |
| .setSaveInfoVisitor((contexts, builder) -> { |
| final FillContext context = contexts.get(0); |
| final AutofillId inputId = findAutofillIdByResourceId(context, ID_INPUT); |
| final AutofillId passwordId = findAutofillIdByResourceId(context, ID_PASSWORD); |
| builder.addSanitizer(new TextValueSanitizer(Pattern.compile("dude"), "$1"), |
| inputId, passwordId); |
| }) |
| .addDataset(new CannedDataset.Builder() |
| .setField(ID_INPUT, "#id#") |
| .setField(ID_PASSWORD, "#pass#") |
| .setPresentation(createPresentation("YO")) |
| .build()) |
| .build()); |
| |
| // Trigger autofill. |
| mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus()); |
| sReplier.getNextFillRequest(); |
| |
| mActivity.syncRunOnUiThread(() -> { |
| mActivity.mInput.setText("id"); |
| mActivity.mPassword.setText("pass"); |
| mActivity.mCommit.performClick(); |
| }); |
| |
| mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC); |
| } |
| |
| @Test |
| @AppModeFull(reason = "testSanitizeOnSaveWhenAppChangeValues() is enough") |
| public void testDontSaveWhenOptionalFieldFailedSanitization() throws Exception { |
| startActivity(); |
| |
| // Set service. |
| enableService(); |
| |
| // Set expectations. |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT) |
| .setOptionalSavableIds(ID_PASSWORD) |
| .setSaveInfoVisitor((contexts, builder) -> { |
| final FillContext context = contexts.get(0); |
| final AutofillId inputId = findAutofillIdByResourceId(context, ID_INPUT); |
| final AutofillId passwordId = findAutofillIdByResourceId(context, ID_PASSWORD); |
| builder.addSanitizer(new TextValueSanitizer(Pattern.compile("dude"), "$1"), |
| inputId, passwordId); |
| |
| }) |
| .addDataset(new CannedDataset.Builder() |
| .setField(ID_INPUT, "id") |
| .setField(ID_PASSWORD, "#pass#") |
| .setPresentation(createPresentation("YO")) |
| .build()) |
| .build()); |
| |
| // Trigger autofill. |
| mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus()); |
| sReplier.getNextFillRequest(); |
| |
| mActivity.syncRunOnUiThread(() -> { |
| mActivity.mInput.setText("id"); |
| mActivity.mPassword.setText("pass"); |
| mActivity.mCommit.performClick(); |
| }); |
| |
| mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC); |
| } |
| |
| @Test |
| @AppModeFull(reason = "testSanitizeOnSaveWhenAppChangeValues() is enough") |
| public void testDontSaveWhenInitialValueAndNoUserInputAndServiceDatasets() throws Throwable { |
| // Prepare activitiy. |
| startActivity(); |
| mActivity.syncRunOnUiThread(() -> { |
| // NOTE: input's value must be a subset of the dataset value, otherwise the dataset |
| // picker is filtered out |
| mActivity.mInput.setText("f"); |
| mActivity.mPassword.setText("b"); |
| }); |
| |
| // Set service. |
| enableService(); |
| |
| // Set expectations. |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .addDataset(new CannedDataset.Builder() |
| .setField(ID_INPUT, "foo") |
| .setField(ID_PASSWORD, "bar") |
| .setPresentation(createPresentation("The Dude")) |
| .build()) |
| .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_INPUT, ID_PASSWORD).build()); |
| |
| // Trigger auto-fill. |
| mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus()); |
| sReplier.getNextFillRequest(); |
| mUiBot.assertDatasets("The Dude"); |
| |
| // Trigger save. |
| mActivity.getAutofillManager().commit(); |
| |
| // Assert it's not showing. |
| mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD); |
| } |
| |
| enum SetTextCondition { |
| NORMAL, |
| HAS_SESSION, |
| EMPTY_TEXT, |
| FOCUSED, |
| NOT_IMPORTANT_FOR_AUTOFILL, |
| INVISIBLE |
| } |
| |
| /** |
| * Tests scenario when a text field's text is set automatically, it should trigger autofill and |
| * show Save UI. |
| */ |
| @Presubmit |
| @Test |
| public void testShowSaveUiWhenSetTextAutomatically() throws Exception { |
| triggerAutofillWhenSetTextAutomaticallyTest(SetTextCondition.NORMAL); |
| } |
| |
| /** |
| * Tests scenario when a text field's text is set automatically, it should not trigger autofill |
| * when there is an existing session. |
| */ |
| @Presubmit |
| @Test |
| public void testNotTriggerAutofillWhenSetTextWhileSessionExists() throws Exception { |
| triggerAutofillWhenSetTextAutomaticallyTest(SetTextCondition.HAS_SESSION); |
| } |
| |
| /** |
| * Tests scenario when a text field's text is set automatically, it should not trigger autofill |
| * when the text is empty. |
| */ |
| @Presubmit |
| @Test |
| public void testNotTriggerAutofillWhenSetTextWhileEmptyText() throws Exception { |
| triggerAutofillWhenSetTextAutomaticallyTest(SetTextCondition.EMPTY_TEXT); |
| } |
| |
| /** |
| * Tests scenario when a text field's text is set automatically, it should not trigger autofill |
| * when the field is focused. |
| */ |
| @Presubmit |
| @Test |
| public void testNotTriggerAutofillWhenSetTextWhileFocused() throws Exception { |
| triggerAutofillWhenSetTextAutomaticallyTest(SetTextCondition.FOCUSED); |
| } |
| |
| /** |
| * Tests scenario when a text field's text is set automatically, it should not trigger autofill |
| * when the field is not important for autofill. |
| */ |
| @Presubmit |
| @Test |
| public void testNotTriggerAutofillWhenSetTextWhileNotImportantForAutofill() throws Exception { |
| triggerAutofillWhenSetTextAutomaticallyTest(SetTextCondition.NOT_IMPORTANT_FOR_AUTOFILL); |
| } |
| |
| /** |
| * Tests scenario when a text field's text is set automatically, it should not trigger autofill |
| * when the field is not visible. |
| */ |
| @Presubmit |
| @Test |
| public void testNotTriggerAutofillWhenSetTextWhileInvisible() throws Exception { |
| triggerAutofillWhenSetTextAutomaticallyTest(SetTextCondition.INVISIBLE); |
| } |
| |
| private void triggerAutofillWhenSetTextAutomaticallyTest(SetTextCondition condition) |
| throws Exception { |
| startActivity(); |
| |
| // Set service. |
| enableService(); |
| |
| // Set expectations. |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT) |
| .build()); |
| |
| CharSequence inputText = "108"; |
| |
| switch (condition) { |
| case NORMAL: |
| // Nothing. |
| break; |
| case HAS_SESSION: |
| mActivity.syncRunOnUiThread(() -> { |
| mActivity.mInput.setText("100"); |
| }); |
| sReplier.getNextFillRequest(); |
| break; |
| case EMPTY_TEXT: |
| inputText = ""; |
| break; |
| case FOCUSED: |
| mActivity.syncRunOnUiThread(() -> { |
| mActivity.mInput.requestFocus(); |
| }); |
| sReplier.getNextFillRequest(); |
| break; |
| case NOT_IMPORTANT_FOR_AUTOFILL: |
| mActivity.syncRunOnUiThread(() -> { |
| mActivity.mInput.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO); |
| }); |
| break; |
| case INVISIBLE: |
| mActivity.syncRunOnUiThread(() -> { |
| mActivity.mInput.setVisibility(View.INVISIBLE); |
| }); |
| break; |
| default: |
| throw new IllegalArgumentException("invalid condition: " + condition); |
| } |
| |
| // Trigger autofill by setting text. |
| final CharSequence text = inputText; |
| mActivity.syncRunOnUiThread(() -> { |
| mActivity.mInput.setText(text); |
| }); |
| |
| if (condition == SetTextCondition.NORMAL) { |
| sReplier.getNextFillRequest(); |
| |
| mActivity.syncRunOnUiThread(() -> { |
| mActivity.mInput.setText("100"); |
| mActivity.mCommit.performClick(); |
| }); |
| |
| mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC); |
| } else { |
| sReplier.assertOnFillRequestNotCalled(); |
| } |
| } |
| |
| @Presubmit |
| @Test |
| public void testExplicitlySaveButton() throws Exception { |
| explicitlySaveButtonTest(false, 0); |
| } |
| |
| @Presubmit |
| @Test |
| public void testExplicitlySaveButtonWhenAppClearFields() throws Exception { |
| explicitlySaveButtonTest(true, 0); |
| } |
| |
| @Presubmit |
| @Test |
| public void testExplicitlySaveButtonOnly() throws Exception { |
| explicitlySaveButtonTest(false, SaveInfo.FLAG_DONT_SAVE_ON_FINISH); |
| } |
| |
| /** |
| * Tests scenario where service explicitly indicates which button is used to save. |
| */ |
| private void explicitlySaveButtonTest(boolean clearFieldsOnSubmit, int flags) throws Exception { |
| final boolean testBitmap = false; |
| startActivity(); |
| mActivity.setAutoCommit(false); |
| mActivity.setClearFieldsOnSubmit(clearFieldsOnSubmit); |
| |
| // Set service. |
| enableService(); |
| |
| // Set expectations. |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT) |
| .setSaveTriggerId(mActivity.mCommit.getAutofillId()) |
| .setSaveInfoFlags(flags) |
| .build()); |
| |
| // Trigger autofill. |
| mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus()); |
| sReplier.getNextFillRequest(); |
| |
| // Trigger save. |
| mActivity.syncRunOnUiThread(() -> mActivity.mInput.setText("108")); |
| |
| // Take a screenshot to make sure button doesn't disappear. |
| final String commitBefore = mUiBot.assertShownByRelativeId(ID_COMMIT).getText(); |
| assertThat(commitBefore.toUpperCase()).isEqualTo("COMMIT"); |
| // Disable unnecessary screenshot tests as takeScreenshot() fails on some device. |
| |
| final Bitmap screenshotBefore = testBitmap ? mActivity.takeScreenshot(mActivity.mCommit) |
| : null; |
| |
| // Save it... |
| mActivity.syncRunOnUiThread(() -> mActivity.mCommit.performClick()); |
| final UiObject2 saveUi = mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC); |
| mUiBot.saveForAutofill(saveUi, true); |
| |
| // Make sure save button is showning (it was removed on earlier versions of the feature) |
| final String commitAfter = mUiBot.assertShownByRelativeId(ID_COMMIT).getText(); |
| assertThat(commitAfter.toUpperCase()).isEqualTo("COMMIT"); |
| final Bitmap screenshotAfter = testBitmap ? mActivity.takeScreenshot(mActivity.mCommit) |
| : null; |
| |
| // ... and assert results |
| final SaveRequest saveRequest = sReplier.getNextSaveRequest(); |
| assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "108"); |
| |
| if (testBitmap) { |
| Helper.assertBitmapsAreSame("commit-button", screenshotBefore, screenshotAfter); |
| } |
| } |
| |
| @Override |
| protected void tapLinkAfterUpdateAppliedTest(boolean updateLinkView) throws Exception { |
| startActivity(); |
| // Set service. |
| enableService(); |
| |
| // Set expectations. |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setSaveInfoVisitor((contexts, builder) -> { |
| // Set response with custom description |
| final AutofillId id = findAutofillIdByResourceId(contexts.get(0), ID_INPUT); |
| final CustomDescription.Builder customDescription = |
| newCustomDescriptionBuilder(WelcomeActivity.class); |
| final RemoteViews update = newTemplate(); |
| if (updateLinkView) { |
| update.setCharSequence(R.id.link, "setText", "TAP ME IF YOU CAN"); |
| } else { |
| update.setCharSequence(R.id.static_text, "setText", "ME!"); |
| } |
| Validator validCondition = new RegexValidator(id, Pattern.compile(".*")); |
| customDescription.batchUpdate(validCondition, |
| new BatchUpdates.Builder().updateTemplate(update).build()); |
| |
| builder.setCustomDescription(customDescription.build()); |
| }) |
| .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT) |
| .build()); |
| |
| // Trigger autofill. |
| mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus()); |
| sReplier.getNextFillRequest(); |
| // Trigger save. |
| mActivity.syncRunOnUiThread(() -> { |
| mActivity.mInput.setText("108"); |
| mActivity.mCommit.performClick(); |
| }); |
| final UiObject2 saveUi; |
| if (updateLinkView) { |
| saveUi = assertSaveUiWithLinkIsShown(SAVE_DATA_TYPE_GENERIC, "TAP ME IF YOU CAN"); |
| } else { |
| saveUi = assertSaveUiWithLinkIsShown(SAVE_DATA_TYPE_GENERIC); |
| final UiObject2 changed = saveUi.findObject(By.res(mPackageName, ID_STATIC_TEXT)); |
| assertThat(changed.getText()).isEqualTo("ME!"); |
| } |
| |
| // Tap the link. |
| tapSaveUiLink(saveUi); |
| |
| // Make sure new activity is shown... |
| WelcomeActivity.assertShowingDefaultMessage(mUiBot); |
| mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC); |
| } |
| |
| enum DescriptionType { |
| SUCCINCT, |
| CUSTOM, |
| } |
| |
| /** |
| * Tests scenarios when user taps a span in the custom description, then the new activity |
| * finishes: |
| * the Save UI should have been restored. |
| */ |
| @Presubmit |
| @Test |
| @AppModeFull(reason = "No real use case for instant mode af service") |
| public void testTapUrlSpanOnCustomDescription_thenTapBack() throws Exception { |
| saveUiRestoredAfterTappingSpanTest(DescriptionType.CUSTOM, |
| ViewActionActivity.ActivityCustomAction.NORMAL_ACTIVITY); |
| } |
| |
| /** |
| * Tests scenarios when user taps a span in the succinct description, then the new activity |
| * finishes: |
| * the Save UI should have been restored. |
| */ |
| @Presubmit |
| @Test |
| @AppModeFull(reason = "No real use case for instant mode af service") |
| public void testTapUrlSpanOnSuccinctDescription_thenTapBack() throws Exception { |
| saveUiRestoredAfterTappingSpanTest(DescriptionType.SUCCINCT, |
| ViewActionActivity.ActivityCustomAction.NORMAL_ACTIVITY); |
| } |
| |
| /** |
| * Tests scenarios when user taps a span in the custom description, then the new activity |
| * starts an another activity then it finishes: |
| * the Save UI should have been restored. |
| */ |
| @Presubmit |
| @Test |
| @AppModeFull(reason = "No real use case for instant mode af service") |
| public void testTapUrlSpanOnCustomDescription_forwardAnotherActivityThenTapBack() |
| throws Exception { |
| saveUiRestoredAfterTappingSpanTest(DescriptionType.CUSTOM, |
| ViewActionActivity.ActivityCustomAction.FAST_FORWARD_ANOTHER_ACTIVITY); |
| } |
| |
| /** |
| * Tests scenarios when user taps a span in the succinct description, then the new activity |
| * starts an another activity then it finishes: |
| * the Save UI should have been restored. |
| */ |
| @Presubmit |
| @Test |
| @AppModeFull(reason = "No real use case for instant mode af service") |
| public void testTapUrlSpanOnSuccinctDescription_forwardAnotherActivityThenTapBack() |
| throws Exception { |
| saveUiRestoredAfterTappingSpanTest(DescriptionType.SUCCINCT, |
| ViewActionActivity.ActivityCustomAction.FAST_FORWARD_ANOTHER_ACTIVITY); |
| } |
| |
| /** |
| * Tests scenarios when user taps a span in the custom description, then the new activity |
| * stops but does not finish: |
| * the Save UI should have been restored. |
| */ |
| @Presubmit |
| @Test |
| @AppModeFull(reason = "No real use case for instant mode af service") |
| public void testTapUrlSpanOnCustomDescription_tapBackWithoutFinish() throws Exception { |
| saveUiRestoredAfterTappingSpanTest(DescriptionType.CUSTOM, |
| ViewActionActivity.ActivityCustomAction.TAP_BACK_WITHOUT_FINISH); |
| } |
| |
| /** |
| * Tests scenarios when user taps a span in the succinct description, then the new activity |
| * stops but does not finish: |
| * the Save UI should have been restored. |
| */ |
| @Presubmit |
| @Test |
| @AppModeFull(reason = "No real use case for instant mode af service") |
| public void testTapUrlSpanOnSuccinctDescription_tapBackWithoutFinish() throws Exception { |
| saveUiRestoredAfterTappingSpanTest(DescriptionType.SUCCINCT, |
| ViewActionActivity.ActivityCustomAction.TAP_BACK_WITHOUT_FINISH); |
| } |
| |
| private void saveUiRestoredAfterTappingSpanTest( |
| DescriptionType type, ViewActionActivity.ActivityCustomAction action) throws Exception { |
| startActivity(); |
| // Set service. |
| enableService(); |
| |
| switch (type) { |
| case SUCCINCT: |
| // Set expectations with custom description. |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT) |
| .setSaveDescription(newDescriptionWithUrlSpan(action.toString())) |
| .build()); |
| break; |
| case CUSTOM: |
| // Set expectations with custom description. |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT) |
| .setSaveInfoVisitor((contexts, builder) -> builder |
| .setCustomDescription( |
| newCustomDescriptionWithUrlSpan(action.toString()))) |
| .build()); |
| break; |
| default: |
| throw new IllegalArgumentException("invalid type: " + type); |
| } |
| |
| // Trigger autofill. |
| mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus()); |
| sReplier.getNextFillRequest(); |
| |
| // Trigger save. |
| mActivity.syncRunOnUiThread(() -> { |
| mActivity.mInput.setText("108"); |
| mActivity.mCommit.performClick(); |
| }); |
| // Waits for the commit be processed |
| mUiBot.waitForIdle(); |
| |
| mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC); |
| |
| // Tapping URLSpan. |
| final URLSpan span = mUiBot.findFirstUrlSpanWithText("Here is URLSpan"); |
| mActivity.syncRunOnUiThread(() -> span.onClick(/* unused= */ null)); |
| // Waits for the save UI hided |
| mUiBot.waitForIdle(); |
| |
| mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC); |
| |
| // .. check activity show up as expected |
| switch (action) { |
| case FAST_FORWARD_ANOTHER_ACTIVITY: |
| // Show up second activity. |
| SecondActivity.assertShowingDefaultMessage(mUiBot); |
| break; |
| case NORMAL_ACTIVITY: |
| case TAP_BACK_WITHOUT_FINISH: |
| // Show up view action handle activity. |
| ViewActionActivity.assertShowingDefaultMessage(mUiBot); |
| break; |
| default: |
| throw new IllegalArgumentException("invalid action: " + action); |
| } |
| |
| // ..then go back and save it. |
| mUiBot.pressBack(); |
| // Waits for all UI processes to complete |
| mUiBot.waitForIdle(); |
| |
| // Make sure previous activity is back... |
| mUiBot.assertShownByRelativeId(ID_INPUT); |
| |
| // ... and tap save. |
| final UiObject2 newSaveUi = mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC); |
| mUiBot.saveForAutofill(newSaveUi, /* yesDoIt= */ true); |
| |
| final SaveRequest saveRequest = sReplier.getNextSaveRequest(); |
| assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "108"); |
| |
| SecondActivity.finishIt(); |
| ViewActionActivity.finishIt(); |
| } |
| |
| private CustomDescription newCustomDescriptionWithUrlSpan(String action) { |
| final RemoteViews presentation = newTemplate(); |
| presentation.setTextViewText(R.id.custom_text, newDescriptionWithUrlSpan(action)); |
| return new CustomDescription.Builder(presentation).build(); |
| } |
| |
| private CharSequence newDescriptionWithUrlSpan(String action) { |
| final String url = "autofillcts:" + action; |
| final SpannableString ss = new SpannableString("Here is URLSpan"); |
| ss.setSpan(new URLSpan(url), |
| /* start= */ 8, /* end= */ 15, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); |
| return ss; |
| } |
| } |