| /* |
| * 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; |
| |
| import static android.autofillservice.cts.CheckoutActivity.ID_ADDRESS; |
| import static android.autofillservice.cts.CheckoutActivity.ID_CC_EXPIRATION; |
| import static android.autofillservice.cts.CheckoutActivity.ID_CC_NUMBER; |
| import static android.autofillservice.cts.CheckoutActivity.ID_HOME_ADDRESS; |
| import static android.autofillservice.cts.CheckoutActivity.ID_SAVE_CC; |
| import static android.autofillservice.cts.CheckoutActivity.ID_WORK_ADDRESS; |
| import static android.autofillservice.cts.CheckoutActivity.INDEX_ADDRESS_WORK; |
| import static android.autofillservice.cts.CheckoutActivity.INDEX_CC_EXPIRATION_NEVER; |
| import static android.autofillservice.cts.CheckoutActivity.INDEX_CC_EXPIRATION_TODAY; |
| import static android.autofillservice.cts.CheckoutActivity.INDEX_CC_EXPIRATION_TOMORROW; |
| import static android.autofillservice.cts.Helper.assertListValue; |
| import static android.autofillservice.cts.Helper.assertTextAndValue; |
| import static android.autofillservice.cts.Helper.assertTextIsSanitized; |
| import static android.autofillservice.cts.Helper.assertToggleIsSanitized; |
| import static android.autofillservice.cts.Helper.assertToggleValue; |
| import static android.autofillservice.cts.Helper.findAutofillIdByResourceId; |
| import static android.autofillservice.cts.Helper.findNodeByResourceId; |
| import static android.autofillservice.cts.Helper.getContext; |
| import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD; |
| import static android.view.View.AUTOFILL_TYPE_LIST; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| import static com.google.common.truth.Truth.assertWithMessage; |
| |
| import android.app.assist.AssistStructure.ViewNode; |
| import android.autofillservice.cts.CannedFillResponse.CannedDataset; |
| import android.autofillservice.cts.InstrumentedAutoFillService.FillRequest; |
| import android.autofillservice.cts.InstrumentedAutoFillService.SaveRequest; |
| import android.platform.test.annotations.AppModeFull; |
| import android.service.autofill.CharSequenceTransformation; |
| import android.service.autofill.CustomDescription; |
| import android.service.autofill.FillContext; |
| import android.service.autofill.ImageTransformation; |
| import android.support.test.uiautomator.By; |
| import android.support.test.uiautomator.UiObject2; |
| import android.view.autofill.AutofillId; |
| import android.widget.ArrayAdapter; |
| import android.widget.RemoteViews; |
| import android.widget.Spinner; |
| |
| import org.junit.Test; |
| |
| import java.util.Arrays; |
| import java.util.regex.Pattern; |
| |
| /** |
| * Test case for an activity containing non-TextField views. |
| */ |
| public class CheckoutActivityTest |
| extends AutoFillServiceTestCase.AutoActivityLaunch<CheckoutActivity> { |
| |
| private CheckoutActivity mActivity; |
| |
| @Override |
| protected AutofillActivityTestRule<CheckoutActivity> getActivityRule() { |
| return new AutofillActivityTestRule<CheckoutActivity>(CheckoutActivity.class) { |
| @Override |
| protected void afterActivityLaunched() { |
| mActivity = getActivity(); |
| } |
| }; |
| } |
| |
| @Test |
| public void testAutofill() throws Exception { |
| // Set service. |
| enableService(); |
| |
| // Set expectations. |
| sReplier.addResponse(new CannedDataset.Builder() |
| .setPresentation(createPresentation("ACME CC")) |
| .setField(ID_CC_NUMBER, "4815162342") |
| .setField(ID_CC_EXPIRATION, INDEX_CC_EXPIRATION_NEVER) |
| .setField(ID_ADDRESS, 1) |
| .setField(ID_SAVE_CC, true) |
| .build()); |
| mActivity.expectAutoFill("4815162342", INDEX_CC_EXPIRATION_NEVER, R.id.work_address, |
| true); |
| |
| // Trigger auto-fill. |
| mActivity.onCcNumber((v) -> v.requestFocus()); |
| final FillRequest fillRequest = sReplier.getNextFillRequest(); |
| |
| // Assert properties of Spinner field. |
| final ViewNode ccExpirationNode = |
| assertTextIsSanitized(fillRequest.structure, ID_CC_EXPIRATION); |
| assertThat(ccExpirationNode.getClassName()).isEqualTo(Spinner.class.getName()); |
| assertThat(ccExpirationNode.getAutofillType()).isEqualTo(AUTOFILL_TYPE_LIST); |
| final CharSequence[] options = ccExpirationNode.getAutofillOptions(); |
| assertWithMessage("ccExpirationNode.getAutoFillOptions()").that(options).isNotNull(); |
| assertWithMessage("Wrong auto-fill options for spinner").that(options).asList() |
| .containsExactly((Object []) |
| getContext().getResources().getStringArray(R.array.cc_expiration_values)) |
| .inOrder(); |
| |
| // Auto-fill it. |
| mUiBot.selectDataset("ACME CC"); |
| |
| // Check the results. |
| mActivity.assertAutoFilled(); |
| } |
| |
| @Test |
| @AppModeFull(reason = "testAutofill() is enough") |
| public void testAutofillDynamicAdapter() throws Exception { |
| // Set activity. |
| mActivity.onCcExpiration((v) -> v.setAdapter(new ArrayAdapter<String>(getContext(), |
| android.R.layout.simple_spinner_item, |
| Arrays.asList("YESTERDAY", "TODAY", "TOMORROW", "NEVER")))); |
| |
| // Set service. |
| enableService(); |
| |
| // Set expectations. |
| sReplier.addResponse(new CannedDataset.Builder() |
| .setPresentation(createPresentation("ACME CC")) |
| .setField(ID_CC_NUMBER, "4815162342") |
| .setField(ID_CC_EXPIRATION, INDEX_CC_EXPIRATION_NEVER) |
| .setField(ID_ADDRESS, 1) |
| .setField(ID_SAVE_CC, true) |
| .build()); |
| mActivity.expectAutoFill("4815162342", INDEX_CC_EXPIRATION_NEVER, R.id.work_address, |
| true); |
| |
| // Trigger auto-fill. |
| mActivity.onCcNumber((v) -> v.requestFocus()); |
| final FillRequest fillRequest = sReplier.getNextFillRequest(); |
| |
| // Assert properties of Spinner field. |
| final ViewNode ccExpirationNode = |
| assertTextIsSanitized(fillRequest.structure, ID_CC_EXPIRATION); |
| assertThat(ccExpirationNode.getClassName()).isEqualTo(Spinner.class.getName()); |
| assertThat(ccExpirationNode.getAutofillType()).isEqualTo(AUTOFILL_TYPE_LIST); |
| final CharSequence[] options = ccExpirationNode.getAutofillOptions(); |
| assertWithMessage("ccExpirationNode.getAutoFillOptions()").that(options).isNull(); |
| |
| // Auto-fill it. |
| mUiBot.selectDataset("ACME CC"); |
| |
| // Check the results. |
| mActivity.assertAutoFilled(); |
| } |
| |
| // TODO: this should be a pure unit test exercising onProvideAutofillStructure(), |
| // but that would require creating a custom ViewStructure. |
| @Test |
| @AppModeFull(reason = "Unit test") |
| public void testGetAutofillOptionsSorted() throws Exception { |
| // Set service. |
| enableService(); |
| |
| // Set activity. |
| mActivity.onCcExpirationAdapter((adapter) -> adapter.sort((a, b) -> { |
| return ((String) a).compareTo((String) b); |
| })); |
| |
| // Set expectations. |
| sReplier.addResponse(new CannedDataset.Builder() |
| .setPresentation(createPresentation("ACME CC")) |
| .setField(ID_CC_NUMBER, "4815162342") |
| .setField(ID_CC_EXPIRATION, INDEX_CC_EXPIRATION_NEVER) |
| .setField(ID_ADDRESS, 1) |
| .setField(ID_SAVE_CC, true) |
| .build()); |
| mActivity.expectAutoFill("4815162342", INDEX_CC_EXPIRATION_NEVER, R.id.work_address, |
| true); |
| |
| // Trigger auto-fill. |
| mActivity.onCcNumber((v) -> v.requestFocus()); |
| final FillRequest fillRequest = sReplier.getNextFillRequest(); |
| |
| // Assert properties of Spinner field. |
| final ViewNode ccExpirationNode = |
| assertTextIsSanitized(fillRequest.structure, ID_CC_EXPIRATION); |
| assertThat(ccExpirationNode.getClassName()).isEqualTo(Spinner.class.getName()); |
| assertThat(ccExpirationNode.getAutofillType()).isEqualTo(AUTOFILL_TYPE_LIST); |
| final CharSequence[] options = ccExpirationNode.getAutofillOptions(); |
| assertWithMessage("Wrong auto-fill options for spinner").that(options).asList() |
| .containsExactly("never", "today", "tomorrow", "yesterday").inOrder(); |
| |
| // Auto-fill it. |
| mUiBot.selectDataset("ACME CC"); |
| |
| // Check the results. |
| mActivity.assertAutoFilled(); |
| } |
| |
| @Test |
| public void testSanitization() throws Exception { |
| // Set service. |
| enableService(); |
| |
| // Set expectations. |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setRequiredSavableIds(SAVE_DATA_TYPE_CREDIT_CARD, |
| ID_CC_NUMBER, ID_CC_EXPIRATION, ID_ADDRESS, ID_SAVE_CC) |
| .build()); |
| |
| // Dynamically change view contents |
| mActivity.onCcExpiration((v) -> v.setSelection(INDEX_CC_EXPIRATION_TOMORROW, true)); |
| mActivity.onHomeAddress((v) -> v.setChecked(true)); |
| mActivity.onSaveCc((v) -> v.setChecked(true)); |
| |
| // Trigger auto-fill. |
| mActivity.onCcNumber((v) -> v.requestFocus()); |
| |
| // Assert sanitization on fill request: everything should be sanitized! |
| final FillRequest fillRequest = sReplier.getNextFillRequest(); |
| |
| assertTextIsSanitized(fillRequest.structure, ID_CC_NUMBER); |
| assertTextIsSanitized(fillRequest.structure, ID_CC_EXPIRATION); |
| assertToggleIsSanitized(fillRequest.structure, ID_HOME_ADDRESS); |
| assertToggleIsSanitized(fillRequest.structure, ID_SAVE_CC); |
| |
| // Trigger save. |
| mActivity.onCcNumber((v) -> v.setText("4815162342")); |
| mActivity.onCcExpiration((v) -> v.setSelection(INDEX_CC_EXPIRATION_TODAY)); |
| mActivity.onAddress((v) -> v.check(R.id.work_address)); |
| mActivity.onSaveCc((v) -> v.setChecked(false)); |
| mActivity.tapBuy(); |
| mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_CREDIT_CARD); |
| final SaveRequest saveRequest = sReplier.getNextSaveRequest(); |
| |
| // Assert sanitization on save: everything should be available! |
| assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_CC_NUMBER), "4815162342"); |
| assertListValue(findNodeByResourceId(saveRequest.structure, ID_CC_EXPIRATION), |
| INDEX_CC_EXPIRATION_TODAY); |
| assertListValue(findNodeByResourceId(saveRequest.structure, ID_ADDRESS), |
| INDEX_ADDRESS_WORK); |
| assertToggleValue(findNodeByResourceId(saveRequest.structure, ID_HOME_ADDRESS), false); |
| assertToggleValue(findNodeByResourceId(saveRequest.structure, ID_WORK_ADDRESS), true); |
| assertToggleValue(findNodeByResourceId(saveRequest.structure, ID_SAVE_CC), false); |
| } |
| |
| @Test |
| @AppModeFull(reason = "Service-specific test") |
| public void testCustomizedSaveUi() throws Exception { |
| customizedSaveUi(false); |
| } |
| |
| @Test |
| @AppModeFull(reason = "Service-specific test") |
| public void testCustomizedSaveUiWithContentDescription() throws Exception { |
| customizedSaveUi(true); |
| } |
| |
| /** |
| * Tests that a spinner can be used on custom save descriptions. |
| */ |
| private void customizedSaveUi(boolean withContentDescription) throws Exception { |
| // Set service. |
| enableService(); |
| |
| // Set expectations. |
| final String packageName = getContext().getPackageName(); |
| |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setRequiredSavableIds(SAVE_DATA_TYPE_CREDIT_CARD, ID_CC_NUMBER, ID_CC_EXPIRATION) |
| .setSaveInfoVisitor((contexts, builder) -> { |
| final RemoteViews presentation = new RemoteViews(packageName, |
| R.layout.two_horizontal_text_fields); |
| final FillContext context = contexts.get(0); |
| final AutofillId ccNumberId = findAutofillIdByResourceId(context, |
| ID_CC_NUMBER); |
| final AutofillId ccExpirationId = findAutofillIdByResourceId(context, |
| ID_CC_EXPIRATION); |
| final CharSequenceTransformation trans1 = new CharSequenceTransformation |
| .Builder(ccNumberId, Pattern.compile("(.*)"), "$1") |
| .build(); |
| final CharSequenceTransformation trans2 = new CharSequenceTransformation |
| .Builder(ccExpirationId, Pattern.compile("(.*)"), "$1") |
| .build(); |
| final ImageTransformation trans3 = (withContentDescription |
| ? new ImageTransformation.Builder(ccNumberId, |
| Pattern.compile("(.*)"), R.drawable.android, |
| "One image is worth thousand words") |
| : new ImageTransformation.Builder(ccNumberId, |
| Pattern.compile("(.*)"), R.drawable.android)) |
| .build(); |
| |
| final CustomDescription customDescription = |
| new CustomDescription.Builder(presentation) |
| .addChild(R.id.first, trans1) |
| .addChild(R.id.second, trans2) |
| .addChild(R.id.img, trans3) |
| .build(); |
| builder.setCustomDescription(customDescription); |
| }) |
| .build()); |
| |
| // Dynamically change view contents |
| mActivity.onCcExpiration((v) -> v.setSelection(INDEX_CC_EXPIRATION_TOMORROW, true)); |
| |
| // Trigger auto-fill. |
| mActivity.onCcNumber((v) -> v.requestFocus()); |
| sReplier.getNextFillRequest(); |
| |
| // Trigger save. |
| mActivity.onCcNumber((v) -> v.setText("4815162342")); |
| mActivity.onCcExpiration((v) -> v.setSelection(INDEX_CC_EXPIRATION_TODAY)); |
| mActivity.tapBuy(); |
| |
| // First make sure the UI is shown... |
| final UiObject2 saveUi = mUiBot.assertSaveShowing(SAVE_DATA_TYPE_CREDIT_CARD); |
| |
| // Then make sure it does have the custom views on it... |
| final UiObject2 staticText = saveUi.findObject(By.res(packageName, Helper.ID_STATIC_TEXT)); |
| assertThat(staticText).isNotNull(); |
| assertThat(staticText.getText()).isEqualTo("YO:"); |
| |
| final UiObject2 number = saveUi.findObject(By.res(packageName, "first")); |
| assertThat(number).isNotNull(); |
| assertThat(number.getText()).isEqualTo("4815162342"); |
| |
| final UiObject2 expiration = saveUi.findObject(By.res(packageName, "second")); |
| assertThat(expiration).isNotNull(); |
| assertThat(expiration.getText()).isEqualTo("today"); |
| |
| final UiObject2 image = saveUi.findObject(By.res(packageName, "img")); |
| assertThat(image).isNotNull(); |
| final String contentDescription = image.getContentDescription(); |
| if (withContentDescription) { |
| assertThat(contentDescription).isEqualTo("One image is worth thousand words"); |
| } else { |
| assertThat(contentDescription).isNull(); |
| } |
| } |
| |
| /** |
| * Tests that a custom save description is ignored when the selected spinner element is not |
| * available in the autofill options. |
| */ |
| @Test |
| public void testCustomizedSaveUiWhenListResolutionFails() throws Exception { |
| // Set service. |
| enableService(); |
| |
| // Change spinner to return just one item so the transformation throws an exception when |
| // fetching it. |
| mActivity.getCcExpirationAdapter().setAutofillOptions("D'OH!"); |
| |
| // Set expectations. |
| final String packageName = getContext().getPackageName(); |
| sReplier.addResponse(new CannedFillResponse.Builder() |
| .setRequiredSavableIds(SAVE_DATA_TYPE_CREDIT_CARD, ID_CC_NUMBER, ID_CC_EXPIRATION) |
| .setSaveInfoVisitor((contexts, builder) -> { |
| final FillContext context = contexts.get(0); |
| final AutofillId ccNumberId = findAutofillIdByResourceId(context, |
| ID_CC_NUMBER); |
| final AutofillId ccExpirationId = findAutofillIdByResourceId(context, |
| ID_CC_EXPIRATION); |
| final RemoteViews presentation = new RemoteViews(packageName, |
| R.layout.two_horizontal_text_fields); |
| final CharSequenceTransformation trans1 = new CharSequenceTransformation |
| .Builder(ccNumberId, Pattern.compile("(.*)"), "$1") |
| .build(); |
| final CharSequenceTransformation trans2 = new CharSequenceTransformation |
| .Builder(ccExpirationId, Pattern.compile("(.*)"), "$1") |
| .build(); |
| final CustomDescription customDescription = |
| new CustomDescription.Builder(presentation) |
| .addChild(R.id.first, trans1) |
| .addChild(R.id.second, trans2) |
| .build(); |
| builder.setCustomDescription(customDescription); |
| }) |
| .build()); |
| |
| // Dynamically change view contents |
| mActivity.onCcExpiration((v) -> v.setSelection(INDEX_CC_EXPIRATION_TOMORROW, true)); |
| |
| // Trigger auto-fill. |
| mActivity.onCcNumber((v) -> v.requestFocus()); |
| sReplier.getNextFillRequest(); |
| |
| // Trigger save. |
| mActivity.onCcNumber((v) -> v.setText("4815162342")); |
| mActivity.onCcExpiration((v) -> v.setSelection(INDEX_CC_EXPIRATION_TODAY)); |
| mActivity.tapBuy(); |
| |
| // First make sure the UI is shown... |
| final UiObject2 saveUi = mUiBot.assertSaveShowing(SAVE_DATA_TYPE_CREDIT_CARD); |
| |
| // Then make sure it does not have the custom views on it... |
| assertThat(saveUi.findObject(By.res(packageName, Helper.ID_STATIC_TEXT))).isNull(); |
| } |
| } |