blob: 9b575ece6f0c9f0f2da2d2dda87d69b74d08b77b [file] [log] [blame]
/*
* 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.app.Activity.RESULT_CANCELED;
import static android.app.Activity.RESULT_OK;
import static android.autofillservice.cts.CannedFillResponse.DO_NOT_REPLY_RESPONSE;
import static android.autofillservice.cts.CannedFillResponse.NO_RESPONSE;
import static android.autofillservice.cts.CheckoutActivity.ID_CC_NUMBER;
import static android.autofillservice.cts.Helper.ID_PASSWORD;
import static android.autofillservice.cts.Helper.ID_PASSWORD_LABEL;
import static android.autofillservice.cts.Helper.ID_USERNAME;
import static android.autofillservice.cts.Helper.assertNoDanglingSessions;
import static android.autofillservice.cts.Helper.assertNumberOfChildren;
import static android.autofillservice.cts.Helper.assertTextAndValue;
import static android.autofillservice.cts.Helper.assertTextIsSanitized;
import static android.autofillservice.cts.Helper.assertValue;
import static android.autofillservice.cts.Helper.dumpStructure;
import static android.autofillservice.cts.Helper.eventually;
import static android.autofillservice.cts.Helper.findNodeByResourceId;
import static android.autofillservice.cts.Helper.getContext;
import static android.autofillservice.cts.Helper.runShellCommand;
import static android.autofillservice.cts.Helper.setUserComplete;
import static android.autofillservice.cts.InstrumentedAutoFillService.waitUntilConnected;
import static android.autofillservice.cts.InstrumentedAutoFillService.waitUntilDisconnected;
import static android.autofillservice.cts.LoginActivity.AUTHENTICATION_MESSAGE;
import static android.autofillservice.cts.LoginActivity.BACKDOOR_USERNAME;
import static android.autofillservice.cts.LoginActivity.ID_USERNAME_CONTAINER;
import static android.autofillservice.cts.LoginActivity.getWelcomeMessage;
import static android.service.autofill.FillEventHistory.Event.TYPE_AUTHENTICATION_SELECTED;
import static android.service.autofill.FillEventHistory.Event.TYPE_DATASET_AUTHENTICATION_SELECTED;
import static android.service.autofill.FillEventHistory.Event.TYPE_DATASET_SELECTED;
import static android.service.autofill.FillEventHistory.Event.TYPE_SAVE_SHOWN;
import static android.service.autofill.FillRequest.FLAG_MANUAL_REQUEST;
import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_ADDRESS;
import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD;
import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_EMAIL_ADDRESS;
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 android.text.InputType.TYPE_NULL;
import static android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD;
import static android.view.View.IMPORTANT_FOR_AUTOFILL_NO;
import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import android.app.PendingIntent;
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.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.IntentSender;
import android.graphics.Color;
import android.os.Bundle;
import android.service.autofill.FillEventHistory;
import android.service.autofill.SaveInfo;
import android.support.test.uiautomator.UiObject2;
import android.util.Log;
import android.view.View;
import android.view.View.AccessibilityDelegate;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeProvider;
import android.view.autofill.AutofillManager;
import android.view.autofill.AutofillValue;
import android.widget.RemoteViews;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* This is the test case covering most scenarios - other test cases will cover characteristics
* specific to that test's activity (for example, custom views).
*/
public class LoginActivityTest extends AutoFillServiceTestCase {
private static final String TAG = "LoginActivityTest";
@Rule
public final AutofillActivityTestRule<LoginActivity> mActivityRule =
new AutofillActivityTestRule<LoginActivity>(LoginActivity.class);
private LoginActivity mActivity;
@Before
public void setActivity() {
mActivity = mActivityRule.getActivity();
}
@After
public void finishWelcomeActivity() {
WelcomeActivity.finishIt();
}
@Test
public void testAutoFillNoDatasets() throws Exception {
// Set service.
enableService();
// Set expectations.
sReplier.addResponse(NO_RESPONSE);
// Trigger autofill.
mActivity.onUsername(View::requestFocus);
// Test connection lifecycle.
waitUntilConnected();
sReplier.getNextFillRequest();
// Make sure UI is not shown.
sUiBot.assertNoDatasets();
// Try to trigger it again...
mActivity.onPassword(View::requestFocus);
// ...and make sure it didn't
sUiBot.assertNoDatasets();
sReplier.assertNumberUnhandledFillRequests(0);
// Test connection lifecycle.
waitUntilDisconnected();
}
@Test
public void testAutofillManuallyAfterServiceReturnedNoDatasets() throws Exception {
// Set service.
enableService();
// Set expectations.
sReplier.addResponse(NO_RESPONSE);
// Trigger autofill.
mActivity.onUsername(View::requestFocus);
sReplier.getNextFillRequest();
// Make sure UI is not shown.
sUiBot.assertNoDatasets();
// Try again, forcing it
sReplier.addResponse(new CannedDataset.Builder()
.setField(ID_USERNAME, "dude")
.setField(ID_PASSWORD, "sweet")
.setPresentation(createPresentation("The Dude"))
.build());
mActivity.expectAutoFill("dude", "sweet");
mActivity.forceAutofillOnUsername();
final FillRequest fillRequest = sReplier.getNextFillRequest();
assertThat(fillRequest.flags).isEqualTo(FLAG_MANUAL_REQUEST);
// Selects the dataset.
sUiBot.selectDataset("The Dude");
// Check the results.
mActivity.assertAutoFilled();
}
@Test
public void testAutofillManuallyAndSaveAfterServiceReturnedNoDatasets() throws Exception {
// Set service.
enableService();
// Set expectations.
sReplier.addResponse(NO_RESPONSE);
// Trigger autofill.
mActivity.onUsername(View::requestFocus);
sReplier.getNextFillRequest();
// Make sure UI is not shown.
sUiBot.assertNoDatasets();
sReplier.assertNumberUnhandledFillRequests(0);
mActivity.onPassword(View::requestFocus);
sUiBot.assertNoDatasets();
sReplier.assertNumberUnhandledFillRequests(0);
// Try again, forcing it
saveOnlyTest(true);
}
@Test
public void testAutoFillOneDataset() throws Exception {
// Set service.
enableService();
// Set expectations.
sReplier.addResponse(new CannedDataset.Builder()
.setField(ID_USERNAME, "dude")
.setField(ID_PASSWORD, "sweet")
.setPresentation(createPresentation("The Dude"))
.build());
mActivity.expectAutoFill("dude", "sweet");
// Dynamically set password to make sure it's sanitized.
mActivity.onPassword((v) -> v.setText("I AM GROOT"));
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
// Auto-fill it.
sUiBot.selectDataset("The Dude");
// Check the results.
mActivity.assertAutoFilled();
// Sanity checks.
// Make sure input was sanitized.
final FillRequest request = sReplier.getNextFillRequest();
assertWithMessage("CancelationSignal is null").that(request.cancellationSignal).isNotNull();
assertTextIsSanitized(request.structure, ID_PASSWORD);
// Make sure initial focus was properly set.
assertWithMessage("Username node is not focused").that(
findNodeByResourceId(request.structure, ID_USERNAME).isFocused()).isTrue();
assertWithMessage("Password node is focused").that(
findNodeByResourceId(request.structure, ID_PASSWORD).isFocused()).isFalse();
}
@Test
public void testAutoFillTwoDatasetsSameNumberOfFields() throws Exception {
// Set service.
enableService();
// Set expectations.
sReplier.addResponse(new CannedFillResponse.Builder()
.addDataset(new CannedDataset.Builder()
.setField(ID_USERNAME, "dude")
.setField(ID_PASSWORD, "sweet")
.setPresentation(createPresentation("The Dude"))
.build())
.addDataset(new CannedDataset.Builder()
.setField(ID_USERNAME, "DUDE")
.setField(ID_PASSWORD, "SWEET")
.setPresentation(createPresentation("THE DUDE"))
.build())
.build());
mActivity.expectAutoFill("dude", "sweet");
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
sReplier.getNextFillRequest();
// Make sure all datasets are available...
sUiBot.assertDatasets("The Dude", "THE DUDE");
// ... on all fields.
mActivity.onPassword(View::requestFocus);
sUiBot.assertDatasets("The Dude", "THE DUDE");
// Auto-fill it.
sUiBot.selectDataset("The Dude");
// Check the results.
mActivity.assertAutoFilled();
}
@Test
public void testAutoFillTwoDatasetsUnevenNumberOfFieldsFillsAll() throws Exception {
autoFillTwoDatasetsUnevenNumberOfFieldsTest(true);
}
@Test
public void testAutoFillTwoDatasetsUnevenNumberOfFieldsFillsOne() throws Exception {
autoFillTwoDatasetsUnevenNumberOfFieldsTest(false);
}
private void autoFillTwoDatasetsUnevenNumberOfFieldsTest(boolean fillsAll) throws Exception {
// Set service.
enableService();
// Set expectations.
sReplier.addResponse(new CannedFillResponse.Builder()
.addDataset(new CannedDataset.Builder()
.setField(ID_USERNAME, "dude")
.setField(ID_PASSWORD, "sweet")
.setPresentation(createPresentation("The Dude"))
.build())
.addDataset(new CannedDataset.Builder()
.setField(ID_USERNAME, "DUDE")
.setPresentation(createPresentation("THE DUDE"))
.build())
.build());
if (fillsAll) {
mActivity.expectAutoFill("dude", "sweet");
} else {
mActivity.expectAutoFill("DUDE");
}
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
sReplier.getNextFillRequest();
// Make sure all datasets are available on username...
sUiBot.assertDatasets("The Dude", "THE DUDE");
// ... but just one for password
mActivity.onPassword(View::requestFocus);
sUiBot.assertDatasets("The Dude");
// Auto-fill it.
mActivity.onUsername(View::requestFocus);
sUiBot.assertDatasets("The Dude", "THE DUDE");
if (fillsAll) {
sUiBot.selectDataset("The Dude");
} else {
sUiBot.selectDataset("THE DUDE");
}
// Check the results.
mActivity.assertAutoFilled();
}
@Test
public void testAutoFillWhenViewHasChildAccessibilityNodes() throws Exception {
mActivity.onUsername((v) -> v.setAccessibilityDelegate(new AccessibilityDelegate() {
@Override
public AccessibilityNodeProvider getAccessibilityNodeProvider(View host) {
return new AccessibilityNodeProvider() {
@Override
public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
final AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain();
if (virtualViewId == View.NO_ID) {
info.addChild(v, 108);
}
return info;
}
};
}
}));
testAutoFillOneDataset();
}
@Test
public void testAutoFillOneDatasetAndMoveFocusAround() throws Exception {
// Set service.
enableService();
// Set expectations.
sReplier.addResponse(new CannedDataset.Builder()
.setField(ID_USERNAME, "dude")
.setField(ID_PASSWORD, "sweet")
.setPresentation(createPresentation("The Dude"))
.build());
mActivity.expectAutoFill("dude", "sweet");
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
sReplier.getNextFillRequest();
// Make sure tapping on other fields from the dataset does not trigger it again
mActivity.onPassword(View::requestFocus);
sReplier.assertNumberUnhandledFillRequests(0);
mActivity.onUsername(View::requestFocus);
sReplier.assertNumberUnhandledFillRequests(0);
// Auto-fill it.
sUiBot.selectDataset("The Dude");
// Check the results.
mActivity.assertAutoFilled();
// Make sure tapping on other fields from the dataset does not trigger it again
mActivity.onPassword(View::requestFocus);
mActivity.onUsername(View::requestFocus);
}
@Test
public void testUiNotShownAfterAutofilled() throws Exception {
// Set service.
enableService();
// Set expectations.
sReplier.addResponse(new CannedDataset.Builder()
.setField(ID_USERNAME, "dude")
.setField(ID_PASSWORD, "sweet")
.setPresentation(createPresentation("The Dude"))
.build());
mActivity.expectAutoFill("dude", "sweet");
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
sReplier.getNextFillRequest();
sUiBot.selectDataset("The Dude");
// Check the results.
mActivity.assertAutoFilled();
// Make sure tapping on autofilled field does not trigger it again
mActivity.onPassword(View::requestFocus);
sUiBot.assertNoDatasets();
mActivity.onUsername(View::requestFocus);
sUiBot.assertNoDatasets();
}
@Test
public void testAutofillCallbacks() throws Exception {
// Set service.
enableService();
final MyAutofillCallback callback = mActivity.registerCallback();
// Set expectations.
sReplier.addResponse(new CannedDataset.Builder()
.setField(ID_USERNAME, "dude")
.setField(ID_PASSWORD, "sweet")
.setPresentation(createPresentation("The Dude"))
.build());
mActivity.expectAutoFill("dude", "sweet");
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
sReplier.getNextFillRequest();
final View username = mActivity.getUsername();
final View password = mActivity.getPassword();
callback.assertUiShownEvent(username);
mActivity.onPassword(View::requestFocus);
callback.assertUiHiddenEvent(username);
callback.assertUiShownEvent(password);
mActivity.onUsername(View::requestFocus);
mActivity.unregisterCallback();
callback.assertNumberUnhandledEvents(0);
// Auto-fill it.
sUiBot.selectDataset("The Dude");
// Check the results.
mActivity.assertAutoFilled();
}
@Test
public void testAutofillCallbackDisabled() throws Exception {
// Set service.
disableService();
final MyAutofillCallback callback = mActivity.registerCallback();
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
// Assert callback was called
final View username = mActivity.getUsername();
callback.assertUiUnavailableEvent(username);
}
@Test
public void testAutofillCallbackNoDatasets() throws Exception {
callbackUnavailableTest(NO_RESPONSE);
}
@Test
public void testAutofillCallbackNoDatasetsButSaveInfo() throws Exception {
callbackUnavailableTest(new CannedFillResponse.Builder()
.setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
.build());
}
private void callbackUnavailableTest(CannedFillResponse response) throws Exception {
// Set service.
enableService();
final MyAutofillCallback callback = mActivity.registerCallback();
// Set expectations.
sReplier.addResponse(response);
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
sReplier.getNextFillRequest();
// Auto-fill it.
sUiBot.assertNoDatasets();
// Assert callback was called
final View username = mActivity.getUsername();
callback.assertUiUnavailableEvent(username);
}
@Test
public void testAutoFillOneDatasetAndSave() throws Exception {
// Set service.
enableService();
// Set expectations.
final Bundle extras = new Bundle();
extras.putString("numbers", "4815162342");
sReplier.addResponse(new CannedFillResponse.Builder()
.addDataset(new CannedDataset.Builder()
.setField(ID_USERNAME, "dude")
.setField(ID_PASSWORD, "sweet")
.setPresentation(createPresentation("The Dude"))
.build())
.setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
.setExtras(extras)
.build());
mActivity.expectAutoFill("dude", "sweet");
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
// Since this is a Presubmit test, wait for connection to avoid flakiness.
waitUntilConnected();
sReplier.getNextFillRequest();
// Auto-fill it.
sUiBot.selectDataset("The Dude");
// Check the results.
mActivity.assertAutoFilled();
// Try to login, it will fail.
final String loginMessage = mActivity.tapLogin();
assertWithMessage("Wrong login msg").that(loginMessage).isEqualTo(AUTHENTICATION_MESSAGE);
// Set right password...
mActivity.onPassword((v) -> v.setText("dude"));
// ... and try again
final String expectedMessage = getWelcomeMessage("dude");
final String actualMessage = mActivity.tapLogin();
assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
// Assert the snack bar is shown and tap "Save".
sUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
final SaveRequest saveRequest = sReplier.getNextSaveRequest();
// Assert value of expected fields - should not be sanitized.
final ViewNode username = findNodeByResourceId(saveRequest.structure, ID_USERNAME);
assertTextAndValue(username, "dude");
final ViewNode password = findNodeByResourceId(saveRequest.structure, ID_USERNAME);
assertTextAndValue(password, "dude");
// Make sure extras were passed back on onSave()
assertThat(saveRequest.data).isNotNull();
final String extraValue = saveRequest.data.getString("numbers");
assertWithMessage("extras not passed on save").that(extraValue).isEqualTo("4815162342");
// Sanity check: once saved, the session should be finished.
assertNoDanglingSessions();
}
@Test
public void testAutoFillOneDatasetAndSaveHidingOverlays() throws Exception {
// Set service.
enableService();
// Set expectations.
final Bundle extras = new Bundle();
extras.putString("numbers", "4815162342");
sReplier.addResponse(new CannedFillResponse.Builder()
.addDataset(new CannedDataset.Builder()
.setField(ID_USERNAME, "dude")
.setField(ID_PASSWORD, "sweet")
.setPresentation(createPresentation("The Dude"))
.build())
.setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
.setExtras(extras)
.build());
mActivity.expectAutoFill("dude", "sweet");
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
// Since this is a Presubmit test, wait for connection to avoid flakiness.
waitUntilConnected();
sReplier.getNextFillRequest();
// Add an overlay on top of the whole screen
final View[] overlay = new View[1];
try {
// Allow ourselves to add overlays
runShellCommand("appops set " + getContext().getPackageName()
+ " SYSTEM_ALERT_WINDOW allow");
// Make sure the fill UI is shown.
sUiBot.assertDatasets("The Dude");
final CountDownLatch latch = new CountDownLatch(1);
mActivity.runOnUiThread(() -> {
// This overlay is focusable, full-screen, which should block interaction
// with the fill UI unless the platform successfully hides overlays.
final WindowManager.LayoutParams params = new WindowManager.LayoutParams();
params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
params.flags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
params.width = ViewGroup.LayoutParams.MATCH_PARENT;
params.height = ViewGroup.LayoutParams.MATCH_PARENT;
final View view = new View(getContext()) {
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
latch.countDown();
}
};
view.setBackgroundColor(Color.RED);
WindowManager windowManager = getContext().getSystemService(WindowManager.class);
windowManager.addView(view, params);
overlay[0] = view;
});
// Wait for the window being added.
assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue();
// Auto-fill it.
sUiBot.selectDataset("The Dude");
// Check the results.
mActivity.assertAutoFilled();
// Try to login, it will fail.
final String loginMessage = mActivity.tapLogin();
assertWithMessage("Wrong login msg").that(loginMessage).isEqualTo(
AUTHENTICATION_MESSAGE);
// Set right password...
mActivity.onPassword((v) -> v.setText("dude"));
// ... and try again
final String expectedMessage = getWelcomeMessage("dude");
final String actualMessage = mActivity.tapLogin();
assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
// Assert the snack bar is shown and tap "Save".
sUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
final SaveRequest saveRequest = sReplier.getNextSaveRequest();
// Assert value of expected fields - should not be sanitized.
final ViewNode username = findNodeByResourceId(saveRequest.structure, ID_USERNAME);
assertTextAndValue(username, "dude");
final ViewNode password = findNodeByResourceId(saveRequest.structure, ID_USERNAME);
assertTextAndValue(password, "dude");
// Make sure extras were passed back on onSave()
assertThat(saveRequest.data).isNotNull();
final String extraValue = saveRequest.data.getString("numbers");
assertWithMessage("extras not passed on save").that(extraValue).isEqualTo("4815162342");
// Sanity check: once saved, the session should be finished.
assertNoDanglingSessions();
} finally {
// Make sure we can no longer add overlays
runShellCommand("appops set " + getContext().getPackageName()
+ " SYSTEM_ALERT_WINDOW ignore");
// Make sure the overlay is removed
mActivity.runOnUiThread(() -> {
WindowManager windowManager = getContext().getSystemService(WindowManager.class);
windowManager.removeView(overlay[0]);
});
}
}
@Test
public void testAutoFillMultipleDatasetsPickFirst() throws Exception {
multipleDatasetsTest(1);
}
@Test
public void testAutoFillMultipleDatasetsPickSecond() throws Exception {
multipleDatasetsTest(2);
}
@Test
public void testAutoFillMultipleDatasetsPickThird() throws Exception {
multipleDatasetsTest(3);
}
private void multipleDatasetsTest(int number) throws Exception {
// Set service.
enableService();
// Set expectations.
sReplier.addResponse(new CannedFillResponse.Builder()
.addDataset(new CannedDataset.Builder()
.setField(ID_USERNAME, "mr_plow")
.setField(ID_PASSWORD, "D'OH!")
.setPresentation(createPresentation("Mr Plow"))
.build())
.addDataset(new CannedDataset.Builder()
.setField(ID_USERNAME, "el barto")
.setField(ID_PASSWORD, "aycaramba!")
.setPresentation(createPresentation("El Barto"))
.build())
.addDataset(new CannedDataset.Builder()
.setField(ID_USERNAME, "mr sparkle")
.setField(ID_PASSWORD, "Aw3someP0wer")
.setPresentation(createPresentation("Mr Sparkle"))
.build())
.build());
final String name;
switch (number) {
case 1:
name = "Mr Plow";
mActivity.expectAutoFill("mr_plow", "D'OH!");
break;
case 2:
name = "El Barto";
mActivity.expectAutoFill("el barto", "aycaramba!");
break;
case 3:
name = "Mr Sparkle";
mActivity.expectAutoFill("mr sparkle", "Aw3someP0wer");
break;
default:
throw new IllegalArgumentException("invalid dataset number: " + number);
}
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
sReplier.getNextFillRequest();
// Make sure all datasets are shown.
final UiObject2 picker = sUiBot.assertDatasets("Mr Plow", "El Barto", "Mr Sparkle");
// Auto-fill it.
sUiBot.selectDataset(picker, name);
// Check the results.
mActivity.assertAutoFilled();
}
/**
* Tests the scenario where the service uses custom remote views for different fields (username
* and password).
*/
@Test
public void testAutofillOneDatasetCustomPresentation() throws Exception {
// Set service.
enableService();
// Set expectations.
sReplier.addResponse(new CannedDataset.Builder()
.setField(ID_USERNAME, "dude",
createPresentation("The Dude"))
.setField(ID_PASSWORD, "sweet",
createPresentation("Dude's password"))
.build());
mActivity.expectAutoFill("dude", "sweet");
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
sReplier.getNextFillRequest();
// Check initial field.
sUiBot.assertDatasets("The Dude");
// Then move around...
mActivity.onPassword(View::requestFocus);
sUiBot.assertDatasets("Dude's password");
mActivity.onUsername(View::requestFocus);
sUiBot.assertDatasets("The Dude");
// Auto-fill it.
mActivity.onPassword(View::requestFocus);
sUiBot.selectDataset("Dude's password");
// Check the results.
mActivity.assertAutoFilled();
}
/**
* Tests the scenario where the service uses custom remote views for different fields (username
* and password) and the dataset itself, and each dataset has the same number of fields.
*/
@Test
public void testAutofillMultipleDatasetsCustomPresentations() throws Exception {
// Set service.
enableService();
// Set expectations.
sReplier.addResponse(new CannedFillResponse.Builder()
.addDataset(new CannedDataset.Builder(createPresentation("Dataset1"))
.setField(ID_USERNAME, "user1") // no presentation
.setField(ID_PASSWORD, "pass1", createPresentation("Pass1"))
.build())
.addDataset(new CannedDataset.Builder()
.setField(ID_USERNAME, "user2", createPresentation("User2"))
.setField(ID_PASSWORD, "pass2") // no presentation
.setPresentation(createPresentation("Dataset2"))
.build())
.build());
mActivity.expectAutoFill("user1", "pass1");
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
sReplier.getNextFillRequest();
// Check initial field.
sUiBot.assertDatasets("Dataset1", "User2");
// Then move around...
mActivity.onPassword(View::requestFocus);
sUiBot.assertDatasets("Pass1", "Dataset2");
mActivity.onUsername(View::requestFocus);
sUiBot.assertDatasets("Dataset1", "User2");
// Auto-fill it.
mActivity.onPassword(View::requestFocus);
sUiBot.selectDataset("Pass1");
// Check the results.
mActivity.assertAutoFilled();
}
/**
* Tests the scenario where the service uses custom remote views for different fields (username
* and password), and each dataset has the same number of fields.
*/
@Test
public void testAutofillMultipleDatasetsCustomPresentationSameFields() throws Exception {
// Set service.
enableService();
// Set expectations.
sReplier.addResponse(new CannedFillResponse.Builder()
.addDataset(new CannedDataset.Builder()
.setField(ID_USERNAME, "user1", createPresentation("User1"))
.setField(ID_PASSWORD, "pass1", createPresentation("Pass1"))
.build())
.addDataset(new CannedDataset.Builder()
.setField(ID_USERNAME, "user2", createPresentation("User2"))
.setField(ID_PASSWORD, "pass2", createPresentation("Pass2"))
.build())
.build());
mActivity.expectAutoFill("user1", "pass1");
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
sReplier.getNextFillRequest();
// Check initial field.
sUiBot.assertDatasets("User1", "User2");
// Then move around...
mActivity.onPassword(View::requestFocus);
sUiBot.assertDatasets("Pass1", "Pass2");
mActivity.onUsername(View::requestFocus);
sUiBot.assertDatasets("User1", "User2");
// Auto-fill it.
mActivity.onPassword(View::requestFocus);
sUiBot.selectDataset("Pass1");
// Check the results.
mActivity.assertAutoFilled();
}
/**
* Tests the scenario where the service uses custom remote views for different fields (username
* and password), but each dataset has a different number of fields.
*/
@Test
public void testAutofillMultipleDatasetsCustomPresentationFirstDatasetMissingSecondField()
throws Exception {
// Set service.
enableService();
// Set expectations.
sReplier.addResponse(new CannedFillResponse.Builder()
.addDataset(new CannedDataset.Builder()
.setField(ID_USERNAME, "user1", createPresentation("User1"))
.build())
.addDataset(new CannedDataset.Builder()
.setField(ID_USERNAME, "user2", createPresentation("User2"))
.setField(ID_PASSWORD, "pass2", createPresentation("Pass2"))
.build())
.build());
mActivity.expectAutoFill("user2", "pass2");
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
sReplier.getNextFillRequest();
// Check initial field.
sUiBot.assertDatasets("User1", "User2");
// Then move around...
mActivity.onPassword(View::requestFocus);
sUiBot.assertDatasets("Pass2");
mActivity.onUsername(View::requestFocus);
sUiBot.assertDatasets("User1", "User2");
// Auto-fill it.
sUiBot.selectDataset("User2");
// Check the results.
mActivity.assertAutoFilled();
}
/**
* Tests the scenario where the service uses custom remote views for different fields (username
* and password), but each dataset has a different number of fields.
*/
@Test
public void testAutofillMultipleDatasetsCustomPresentationSecondDatasetMissingFirstField()
throws Exception {
// Set service.
enableService();
// Set expectations.
sReplier.addResponse(new CannedFillResponse.Builder()
.addDataset(new CannedDataset.Builder()
.setField(ID_USERNAME, "user1", createPresentation("User1"))
.setField(ID_PASSWORD, "pass1", createPresentation("Pass1"))
.build())
.addDataset(new CannedDataset.Builder()
.setField(ID_PASSWORD, "pass2", createPresentation("Pass2"))
.build())
.build());
mActivity.expectAutoFill("user1", "pass1");
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
sReplier.getNextFillRequest();
// Check initial field.
sUiBot.assertDatasets("User1");
// Then move around...
mActivity.onPassword(View::requestFocus);
sUiBot.assertDatasets("Pass1", "Pass2");
mActivity.onUsername(View::requestFocus);
sUiBot.assertDatasets("User1");
// Auto-fill it.
sUiBot.selectDataset("User1");
// Check the results.
mActivity.assertAutoFilled();
}
@Test
public void filterText() throws Exception {
final String AA = "Two A's";
final String AB = "A and B";
final String B = "Only B";
enableService();
// Set expectations.
sReplier.addResponse(new CannedFillResponse.Builder()
.addDataset(new CannedDataset.Builder()
.setField(ID_USERNAME, "aa")
.setPresentation(createPresentation(AA))
.build())
.addDataset(new CannedDataset.Builder()
.setField(ID_USERNAME, "ab")
.setPresentation(createPresentation(AB))
.build())
.addDataset(new CannedDataset.Builder()
.setField(ID_USERNAME, "b")
.setPresentation(createPresentation(B))
.build())
.build());
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
sReplier.getNextFillRequest();
// With no filter text all datasets should be shown
sUiBot.assertDatasets(AA, AB, B);
// Only two datasets start with 'a'
runShellCommand("input keyevent KEYCODE_A");
sUiBot.assertDatasets(AA, AB);
// Only one dataset start with 'aa'
runShellCommand("input keyevent KEYCODE_A");
sUiBot.assertDatasets(AA);
// Only two datasets start with 'a'
runShellCommand("input keyevent KEYCODE_DEL");
sUiBot.assertDatasets(AA, AB);
// With no filter text all datasets should be shown
runShellCommand("input keyevent KEYCODE_DEL");
sUiBot.assertDatasets(AA, AB, B);
// No dataset start with 'aaa'
runShellCommand("input keyevent KEYCODE_A");
runShellCommand("input keyevent KEYCODE_A");
runShellCommand("input keyevent KEYCODE_A");
sUiBot.assertNoDatasets();
}
@Test
public void filterTextNullValuesAlwaysMatched() throws Exception {
final String AA = "Two A's";
final String AB = "A and B";
final String B = "Only B";
enableService();
// Set expectations.
sReplier.addResponse(new CannedFillResponse.Builder()
.addDataset(new CannedDataset.Builder()
.setField(ID_USERNAME, "aa")
.setPresentation(createPresentation(AA))
.build())
.addDataset(new CannedDataset.Builder()
.setField(ID_USERNAME, "ab")
.setPresentation(createPresentation(AB))
.build())
.addDataset(new CannedDataset.Builder()
.setField(ID_USERNAME, (String) null)
.setPresentation(createPresentation(B))
.build())
.build());
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
sReplier.getNextFillRequest();
// With no filter text all datasets should be shown
sUiBot.assertDatasets(AA, AB, B);
// Two datasets start with 'a' and one with null value always shown
runShellCommand("input keyevent KEYCODE_A");
sUiBot.assertDatasets(AA, AB, B);
// One dataset start with 'aa' and one with null value always shown
runShellCommand("input keyevent KEYCODE_A");
sUiBot.assertDatasets(AA, B);
// Two datasets start with 'a' and one with null value always shown
runShellCommand("input keyevent KEYCODE_DEL");
sUiBot.assertDatasets(AA, AB, B);
// With no filter text all datasets should be shown
runShellCommand("input keyevent KEYCODE_DEL");
sUiBot.assertDatasets(AA, AB, B);
// No dataset start with 'aaa' and one with null value always shown
runShellCommand("input keyevent KEYCODE_A");
runShellCommand("input keyevent KEYCODE_A");
runShellCommand("input keyevent KEYCODE_A");
sUiBot.assertDatasets(B);
}
@Test
public void filterTextDifferentPrefixes() throws Exception {
final String A = "aaa";
final String B = "bra";
final String C = "cadabra";
enableService();
// Set expectations.
sReplier.addResponse(new CannedFillResponse.Builder()
.addDataset(new CannedDataset.Builder()
.setField(ID_USERNAME, A)
.setPresentation(createPresentation(A))
.build())
.addDataset(new CannedDataset.Builder()
.setField(ID_USERNAME, B)
.setPresentation(createPresentation(B))
.build())
.addDataset(new CannedDataset.Builder()
.setField(ID_USERNAME, C)
.setPresentation(createPresentation(C))
.build())
.build());
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
sReplier.getNextFillRequest();
// With no filter text all datasets should be shown
sUiBot.assertDatasets(A, B, C);
mActivity.onUsername((v) -> v.setText("a"));
sUiBot.assertDatasets(A);
mActivity.onUsername((v) -> v.setText("b"));
sUiBot.assertDatasets(B);
mActivity.onUsername((v) -> v.setText("c"));
sUiBot.assertDatasets(C);
}
@Test
public void testSaveOnly() throws Exception {
saveOnlyTest(false);
}
@Test
public void testSaveOnlyTriggeredManually() throws Exception {
saveOnlyTest(false);
}
private void saveOnlyTest(boolean manually) throws Exception {
enableService();
// Set expectations.
sReplier.addResponse(new CannedFillResponse.Builder()
.setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
.build());
// Trigger auto-fill.
if (manually) {
mActivity.forceAutofillOnUsername();
} else {
mActivity.onUsername(View::requestFocus);
}
// Sanity check.
sUiBot.assertNoDatasets();
// Wait for onFill() before proceeding, otherwise the fields might be changed before
// the session started
sReplier.getNextFillRequest();
// Set credentials...
mActivity.onUsername((v) -> v.setText("malkovich"));
mActivity.onPassword((v) -> v.setText("malkovich"));
// ...and login
final String expectedMessage = getWelcomeMessage("malkovich");
final String actualMessage = mActivity.tapLogin();
assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
// Assert the snack bar is shown and tap "Save".
sUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
final SaveRequest saveRequest = sReplier.getNextSaveRequest();
sReplier.assertNumberUnhandledSaveRequests(0);
// Assert value of expected fields - should not be sanitized.
try {
final ViewNode username = findNodeByResourceId(saveRequest.structure, ID_USERNAME);
assertTextAndValue(username, "malkovich");
final ViewNode password = findNodeByResourceId(saveRequest.structure, ID_PASSWORD);
assertTextAndValue(password, "malkovich");
} catch (AssertionError | RuntimeException e) {
dumpStructure("saveOnlyTest() failed", saveRequest.structure);
throw e;
}
// Sanity check: once saved, the session should be finished.
assertNoDanglingSessions();
}
@Test
public void testSaveGoesAwayWhenTappingHomeButton() throws Exception {
saveGoesAway(DismissType.HOME_BUTTON);
}
@Test
public void testSaveGoesAwayWhenTappingBackButton() throws Exception {
saveGoesAway(DismissType.BACK_BUTTON);
}
@Test
public void testSaveGoesAwayWhenTappingRecentsButton() throws Exception {
// Launches new activity first...
startCheckoutActivityAsNewTask();
try {
// .. then the real activity being tested.
sUiBot.switchAppsUsingRecents();
sUiBot.assertShownByRelativeId(ID_USERNAME_CONTAINER);
saveGoesAway(DismissType.RECENTS_BUTTON);
} finally {
CheckoutActivity.finishIt();
}
}
@Test
public void testSaveGoesAwayWhenTouchingOutside() throws Exception {
saveGoesAway(DismissType.TOUCH_OUTSIDE);
}
@Test
public void testSaveGoesAwayWhenLaunchingNewActivity() throws Exception {
try {
saveGoesAway(DismissType.LAUNCH_ACTIVITY);
} finally {
CheckoutActivity.finishIt();
}
}
private void saveGoesAway(DismissType dismissType) throws Exception {
enableService();
// Set expectations.
sReplier.addResponse(new CannedFillResponse.Builder()
.setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
.build());
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
// Sanity check.
sUiBot.assertNoDatasets();
// Wait for onFill() before proceeding, otherwise the fields might be changed before
// the session started
sReplier.getNextFillRequest();
// Set credentials...
mActivity.onUsername((v) -> v.setText("malkovich"));
mActivity.onPassword((v) -> v.setText("malkovich"));
// ...and login
final String expectedMessage = getWelcomeMessage("malkovich");
final String actualMessage = mActivity.tapLogin();
assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
// Assert the snack bar is shown and tap "Save".
sUiBot.assertSaveShowing(SAVE_DATA_TYPE_PASSWORD);
// Then make sure it goes away when user doesn't want it..
switch (dismissType) {
case BACK_BUTTON:
sUiBot.pressBack();
break;
case HOME_BUTTON:
sUiBot.pressHome();
break;
case TOUCH_OUTSIDE:
sUiBot.assertShownByText(expectedMessage).click();
break;
case RECENTS_BUTTON:
sUiBot.switchAppsUsingRecents();
sUiBot.assertShownByRelativeId(CheckoutActivity.ID_ADDRESS);
break;
case LAUNCH_ACTIVITY:
startCheckoutActivityAsNewTask();
break;
default:
throw new IllegalArgumentException("invalid dismiss type: " + dismissType);
}
sUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
}
@Test
public void testSaveOnlyPreFilled() throws Exception {
saveOnlyTestPreFilled(false);
}
@Test
public void testSaveOnlyTriggeredManuallyPreFilled() throws Exception {
saveOnlyTestPreFilled(true);
}
private void saveOnlyTestPreFilled(boolean manually) throws Exception {
enableService();
// Set expectations.
sReplier.addResponse(new CannedFillResponse.Builder()
.setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
.build());
// Set activity
mActivity.onUsername((v) -> v.setText("user_before"));
mActivity.onPassword((v) -> v.setText("pass_before"));
// Trigger auto-fill.
if (manually) {
mActivity.forceAutofillOnUsername();
} else {
mActivity.onUsername(View::requestFocus);
}
// Sanity check.
sUiBot.assertNoDatasets();
// Wait for onFill() before proceeding, otherwise the fields might be changed before
// the session started
sReplier.getNextFillRequest();
// Set credentials...
mActivity.onUsername((v) -> v.setText("user_after"));
mActivity.onPassword((v) -> v.setText("pass_after"));
// ...and login
final String expectedMessage = getWelcomeMessage("user_after");
final String actualMessage = mActivity.tapLogin();
assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
// Assert the snack bar is shown and tap "Save".
sUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
final SaveRequest saveRequest = sReplier.getNextSaveRequest();
sReplier.assertNumberUnhandledSaveRequests(0);
// Assert value of expected fields - should not be sanitized.
try {
final ViewNode username = findNodeByResourceId(saveRequest.structure, ID_USERNAME);
assertTextAndValue(username, "user_after");
final ViewNode password = findNodeByResourceId(saveRequest.structure, ID_PASSWORD);
assertTextAndValue(password, "pass_after");
} catch (AssertionError | RuntimeException e) {
dumpStructure("saveOnlyTest() failed", saveRequest.structure);
throw e;
}
// Sanity check: once saved, the session should be finsihed.
assertNoDanglingSessions();
}
@Test
public void testSaveOnlyTwoRequiredFieldsOnePrefilled() throws Exception {
enableService();
// Set expectations.
sReplier.addResponse(new CannedFillResponse.Builder()
.setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
.build());
// Set activity
mActivity.onUsername((v) -> v.setText("I_AM_USER"));
// Trigger auto-fill.
mActivity.onPassword(View::requestFocus);
// Wait for onFill() before changing value, otherwise the fields might be changed before
// the session started
sReplier.getNextFillRequest();
sUiBot.assertNoDatasets();
// Set credentials...
mActivity.onPassword((v) -> v.setText("thou should pass")); // contains pass
// ...and login
final String expectedMessage = getWelcomeMessage("I_AM_USER"); // contains pass
final String actualMessage = mActivity.tapLogin();
assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
// Assert the snack bar is shown and tap "Save".
sUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
final SaveRequest saveRequest = sReplier.getNextSaveRequest();
sReplier.assertNumberUnhandledSaveRequests(0);
// Assert value of expected fields - should not be sanitized.
try {
final ViewNode username = findNodeByResourceId(saveRequest.structure, ID_USERNAME);
assertTextAndValue(username, "I_AM_USER");
final ViewNode password = findNodeByResourceId(saveRequest.structure, ID_PASSWORD);
assertTextAndValue(password, "thou should pass");
} catch (AssertionError | RuntimeException e) {
dumpStructure("saveOnlyTest() failed", saveRequest.structure);
throw e;
}
// Sanity check: once saved, the session should be finsihed.
assertNoDanglingSessions();
}
@Test
public void testSaveOnlyOptionalField() throws Exception {
enableService();
// Set expectations.
sReplier.addResponse(new CannedFillResponse.Builder()
.setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME)
.setOptionalSavableIds(ID_PASSWORD)
.build());
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
// Sanity check.
sUiBot.assertNoDatasets();
// Wait for onFill() before proceeding, otherwise the fields might be changed before
// the session started
sReplier.getNextFillRequest();
// Set credentials...
mActivity.onUsername((v) -> v.setText("malkovich"));
mActivity.onPassword(View::requestFocus);
mActivity.onPassword((v) -> v.setText("malkovich"));
// ...and login
final String expectedMessage = getWelcomeMessage("malkovich");
final String actualMessage = mActivity.tapLogin();
assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
// Assert the snack bar is shown and tap "Save".
sUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
final SaveRequest saveRequest = sReplier.getNextSaveRequest();
// Assert value of expected fields - should not be sanitized.
final ViewNode username = findNodeByResourceId(saveRequest.structure, ID_USERNAME);
assertTextAndValue(username, "malkovich");
final ViewNode password = findNodeByResourceId(saveRequest.structure, ID_PASSWORD);
assertTextAndValue(password, "malkovich");
// Sanity check: once saved, the session should be finsihed.
assertNoDanglingSessions();
}
@Test
public void testSaveNoRequiredField_NoneFilled() throws Exception {
optionalOnlyTest(FilledFields.NONE);
}
@Test
public void testSaveNoRequiredField_OneFilled() throws Exception {
optionalOnlyTest(FilledFields.USERNAME_ONLY);
}
@Test
public void testSaveNoRequiredField_BothFilled() throws Exception {
optionalOnlyTest(FilledFields.BOTH);
}
enum FilledFields {
NONE,
USERNAME_ONLY,
BOTH
}
private void optionalOnlyTest(FilledFields filledFields) throws Exception {
enableService();
// Set expectations.
sReplier.addResponse(new CannedFillResponse.Builder()
.setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD)
.setOptionalSavableIds(ID_USERNAME, ID_PASSWORD)
.build());
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
// Sanity check.
sUiBot.assertNoDatasets();
// Wait for onFill() before proceeding, otherwise the fields might be changed before
// the session started
sReplier.getNextFillRequest();
// Set credentials...
final String expectedUsername;
if (filledFields == FilledFields.USERNAME_ONLY || filledFields == FilledFields.BOTH) {
expectedUsername = BACKDOOR_USERNAME;
mActivity.onUsername((v) -> v.setText(BACKDOOR_USERNAME));
} else {
expectedUsername = "";
}
mActivity.onPassword(View::requestFocus);
if (filledFields == FilledFields.BOTH) {
mActivity.onPassword((v) -> v.setText("whatever"));
}
// ...and login
final String expectedMessage = getWelcomeMessage(expectedUsername);
final String actualMessage = mActivity.tapLogin();
assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
if (filledFields == FilledFields.NONE) {
sUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
assertNoDanglingSessions();
return;
}
// Assert the snack bar is shown and tap "Save".
sUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
final SaveRequest saveRequest = sReplier.getNextSaveRequest();
// Assert value of expected fields - should not be sanitized.
final ViewNode username = findNodeByResourceId(saveRequest.structure, ID_USERNAME);
assertTextAndValue(username, BACKDOOR_USERNAME);
if (filledFields == FilledFields.BOTH) {
final ViewNode password = findNodeByResourceId(saveRequest.structure, ID_PASSWORD);
assertTextAndValue(password, "whatever");
}
// Sanity check: once saved, the session should be finished.
assertNoDanglingSessions();
}
@Test
public void testGenericSave() throws Exception {
customizedSaveTest(SAVE_DATA_TYPE_GENERIC);
}
@Test
public void testCustomizedSavePassword() throws Exception {
customizedSaveTest(SAVE_DATA_TYPE_PASSWORD);
}
@Test
public void testCustomizedSaveAddress() throws Exception {
customizedSaveTest(SAVE_DATA_TYPE_ADDRESS);
}
@Test
public void testCustomizedSaveCreditCard() throws Exception {
customizedSaveTest(SAVE_DATA_TYPE_CREDIT_CARD);
}
@Test
public void testCustomizedSaveUsername() throws Exception {
customizedSaveTest(SAVE_DATA_TYPE_USERNAME);
}
@Test
public void testCustomizedSaveEmailAddress() throws Exception {
customizedSaveTest(SAVE_DATA_TYPE_EMAIL_ADDRESS);
}
private void customizedSaveTest(int type) throws Exception {
// Set service.
enableService();
// Set expectations.
final String saveDescription = "Your data will be saved with love and care...";
sReplier.addResponse(new CannedFillResponse.Builder()
.setRequiredSavableIds(type, ID_USERNAME, ID_PASSWORD)
.setSaveDescription(saveDescription)
.build());
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
// Sanity check.
sUiBot.assertNoDatasets();
// Wait for onFill() before proceeding, otherwise the fields might be changed before
// the session started.
sReplier.getNextFillRequest();
// Set credentials...
mActivity.onUsername((v) -> v.setText("malkovich"));
mActivity.onPassword((v) -> v.setText("malkovich"));
// ...and login
final String expectedMessage = getWelcomeMessage("malkovich");
final String actualMessage = mActivity.tapLogin();
assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
// Assert the snack bar is shown and tap "Save".
final UiObject2 saveSnackBar = sUiBot.assertSaveShowing(saveDescription, type);
sUiBot.saveForAutofill(saveSnackBar, true);
// Assert save was called.
sReplier.getNextSaveRequest();
}
@Test
public void testAutoFillOneDatasetAndSaveWhenFlagSecure() throws Exception {
mActivity.setFlags(FLAG_SECURE);
testAutoFillOneDatasetAndSave();
}
@Test
public void testAutoFillOneDatasetWhenFlagSecure() throws Exception {
mActivity.setFlags(FLAG_SECURE);
testAutoFillOneDataset();
}
@Test
public void testFillResponseAuthBothFields() throws Exception {
fillResponseAuthBothFields(false);
}
@Test
public void testFillResponseAuthBothFieldsUserCancelsFirstAttempt() throws Exception {
fillResponseAuthBothFields(true);
}
private void fillResponseAuthBothFields(boolean cancelFirstAttempt) throws Exception {
// Set service.
enableService();
final MyAutofillCallback callback = mActivity.registerCallback();
// Prepare the authenticated response
final Bundle clientState = new Bundle();
clientState.putString("numbers", "4815162342");
final IntentSender authentication = AuthenticationActivity.createSender(getContext(), 1,
new CannedFillResponse.Builder().addDataset(
new CannedDataset.Builder()
.setField(ID_USERNAME, "dude")
.setField(ID_PASSWORD, "sweet")
.setId("name")
.setPresentation(createPresentation("Dataset"))
.build())
.setExtras(clientState).build());
// Configure the service behavior
sReplier.addResponse(new CannedFillResponse.Builder()
.setAuthentication(authentication, ID_USERNAME, ID_PASSWORD)
.setPresentation(createPresentation("Tap to auth response"))
.setExtras(clientState)
.build());
// Set expectation for the activity
mActivity.expectAutoFill("dude", "sweet");
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
// Wait for onFill() before proceeding.
sReplier.getNextFillRequest();
final View username = mActivity.getUsername();
callback.assertUiShownEvent(username);
sUiBot.assertDatasets("Tap to auth response");
// Make sure UI is show on 2nd field as well
final View password = mActivity.getPassword();
mActivity.onPassword(View::requestFocus);
callback.assertUiHiddenEvent(username);
callback.assertUiShownEvent(password);
sUiBot.assertDatasets("Tap to auth response");
// Now tap on 1st field to show it again...
mActivity.onUsername(View::requestFocus);
callback.assertUiHiddenEvent(password);
callback.assertUiShownEvent(username);
if (cancelFirstAttempt) {
// Trigger the auth dialog, but emulate cancel.
AuthenticationActivity.setResultCode(RESULT_CANCELED);
sUiBot.selectDataset("Tap to auth response");
callback.assertUiHiddenEvent(username);
callback.assertUiShownEvent(username);
sUiBot.assertDatasets("Tap to auth response");
// Make sure it's still shown on other fields...
mActivity.onPassword(View::requestFocus);
callback.assertUiHiddenEvent(username);
callback.assertUiShownEvent(password);
sUiBot.assertDatasets("Tap to auth response");
// Tap on 1st field to show it again...
mActivity.onUsername(View::requestFocus);
callback.assertUiHiddenEvent(password);
callback.assertUiShownEvent(username);
}
// ...and select it this time
AuthenticationActivity.setResultCode(RESULT_OK);
sUiBot.selectDataset("Tap to auth response");
callback.assertUiHiddenEvent(username);
callback.assertUiShownEvent(username);
final UiObject2 picker = sUiBot.assertDatasets("Dataset");
sUiBot.selectDataset(picker, "Dataset");
callback.assertUiHiddenEvent(username);
sUiBot.assertNoDatasets();
// Check the results.
mActivity.assertAutoFilled();
final Bundle data = AuthenticationActivity.getData();
assertThat(data).isNotNull();
final String extraValue = data.getString("numbers");
assertThat(extraValue).isEqualTo("4815162342");
}
@Test
public void testFillResponseAuthJustOneField() throws Exception {
// Set service.
enableService();
final MyAutofillCallback callback = mActivity.registerCallback();
// Prepare the authenticated response
final Bundle clientState = new Bundle();
clientState.putString("numbers", "4815162342");
final IntentSender authentication = AuthenticationActivity.createSender(getContext(), 1,
new CannedFillResponse.Builder().addDataset(
new CannedDataset.Builder()
.setField(ID_USERNAME, "dude")
.setField(ID_PASSWORD, "sweet")
.setPresentation(createPresentation("Dataset"))
.build())
.build());
// Configure the service behavior
sReplier.addResponse(new CannedFillResponse.Builder()
.setAuthentication(authentication, ID_USERNAME)
.setIgnoreFields(ID_PASSWORD)
.setPresentation(createPresentation("Tap to auth response"))
.setExtras(clientState)
.build());
// Set expectation for the activity
mActivity.expectAutoFill("dude", "sweet");
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
// Wait for onFill() before proceeding.
sReplier.getNextFillRequest();
final View username = mActivity.getUsername();
callback.assertUiShownEvent(username);
sUiBot.assertDatasets("Tap to auth response");
// Make sure UI is not show on 2nd field
mActivity.onPassword(View::requestFocus);
callback.assertUiHiddenEvent(username);
sUiBot.assertNoDatasets();
// Now tap on 1st field to show it again...
mActivity.onUsername(View::requestFocus);
callback.assertUiShownEvent(username);
// ...and select it this time
sUiBot.selectDataset("Tap to auth response");
callback.assertUiHiddenEvent(username);
final UiObject2 picker = sUiBot.assertDatasets("Dataset");
callback.assertUiShownEvent(username);
sUiBot.selectDataset(picker, "Dataset");
callback.assertUiHiddenEvent(username);
sUiBot.assertNoDatasets();
// Check the results.
mActivity.assertAutoFilled();
final Bundle data = AuthenticationActivity.getData();
assertThat(data).isNotNull();
final String extraValue = data.getString("numbers");
assertThat(extraValue).isEqualTo("4815162342");
}
@Test
public void testFillResponseAuthWhenAppCallsCancel() throws Exception {
// Set service.
enableService();
final MyAutofillCallback callback = mActivity.registerCallback();
// Prepare the authenticated response
final IntentSender authentication = AuthenticationActivity.createSender(getContext(), 1,
new CannedFillResponse.Builder().addDataset(
new CannedDataset.Builder()
.setField(ID_USERNAME, "dude")
.setField(ID_PASSWORD, "sweet")
.setId("name")
.setPresentation(createPresentation("Dataset"))
.build())
.build());
// Configure the service behavior
sReplier.addResponse(new CannedFillResponse.Builder()
.setAuthentication(authentication, ID_USERNAME, ID_PASSWORD)
.setPresentation(createPresentation("Tap to auth response"))
.build());
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
// Wait for onFill() before proceeding.
sReplier.getNextFillRequest();
final View username = mActivity.getUsername();
callback.assertUiShownEvent(username);
sUiBot.assertDatasets("Tap to auth response");
// Auto fill it.
final CountDownLatch latch = new CountDownLatch(1);
AuthenticationActivity.setResultCode(latch, RESULT_OK);
sUiBot.selectDataset("Tap to auth response");
callback.assertUiHiddenEvent(username);
// Cancel session...
mActivity.getAutofillManager().cancel();
// ...before finishing the Auth UI.
latch.countDown();
sUiBot.assertNoDatasets();
}
@Test
public void testFillResponseAuthServiceHasNoData() throws Exception {
// Set service.
enableService();
final MyAutofillCallback callback = mActivity.registerCallback();
// Prepare the authenticated response
final IntentSender authentication = AuthenticationActivity.createSender(getContext(), 1,
new CannedFillResponse.Builder()
.setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
.build());
// Configure the service behavior
sReplier.addResponse(new CannedFillResponse.Builder()
.setAuthentication(authentication, ID_USERNAME, ID_PASSWORD)
.setPresentation(createPresentation("Tap to auth response"))
.build());
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
// Wait for onFill() before proceeding.
sReplier.getNextFillRequest();
final View username = mActivity.getUsername();
callback.assertUiShownEvent(username);
// Select the authentication dialog.
sUiBot.selectDataset("Tap to auth response");
callback.assertUiHiddenEvent(username);
sUiBot.assertNoDatasets();
}
@Test
public void testDatasetAuthTwoFields() throws Exception {
datasetAuthTwoFields(false);
}
@Test
public void testDatasetAuthTwoFieldsUserCancelsFirstAttempt() throws Exception {
datasetAuthTwoFields(true);
}
private void datasetAuthTwoFields(boolean cancelFirstAttempt) throws Exception {
// TODO: current API requires these fields...
final RemoteViews bogusPresentation = createPresentation("Whatever man, I'm not used...");
final String bogusValue = "Y U REQUIRE IT?";
// Set service.
enableService();
final MyAutofillCallback callback = mActivity.registerCallback();
// Prepare the authenticated response
final IntentSender authentication = AuthenticationActivity.createSender(getContext(), 1,
new CannedDataset.Builder()
.setField(ID_USERNAME, "dude")
.setField(ID_PASSWORD, "sweet")
.setPresentation(bogusPresentation)
.build());
// Configure the service behavior
sReplier.addResponse(new CannedFillResponse.Builder()
.addDataset(new CannedDataset.Builder()
.setField(ID_USERNAME, bogusValue)
.setField(ID_PASSWORD, bogusValue)
.setPresentation(createPresentation("Tap to auth dataset"))
.setAuthentication(authentication)
.build())
.build());
// Set expectation for the activity
mActivity.expectAutoFill("dude", "sweet");
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
// Wait for onFill() before proceeding.
sReplier.getNextFillRequest();
final View username = mActivity.getUsername();
callback.assertUiShownEvent(username);
sUiBot.assertDatasets("Tap to auth dataset");
// Make sure UI is show on 2nd field as well
final View password = mActivity.getPassword();
mActivity.onPassword(View::requestFocus);
callback.assertUiHiddenEvent(username);
callback.assertUiShownEvent(password);
sUiBot.assertDatasets("Tap to auth dataset");
// Now tap on 1st field to show it again...
mActivity.onUsername(View::requestFocus);
callback.assertUiHiddenEvent(password);
callback.assertUiShownEvent(username);
sUiBot.assertDatasets("Tap to auth dataset");
if (cancelFirstAttempt) {
// Trigger the auth dialog, but emulate cancel.
AuthenticationActivity.setResultCode(RESULT_CANCELED);
sUiBot.selectDataset("Tap to auth dataset");
callback.assertUiHiddenEvent(username);
callback.assertUiShownEvent(username);
sUiBot.assertDatasets("Tap to auth dataset");
// Make sure it's still shown on other fields...
mActivity.onPassword(View::requestFocus);
callback.assertUiHiddenEvent(username);
callback.assertUiShownEvent(password);
sUiBot.assertDatasets("Tap to auth dataset");
// Tap on 1st field to show it again...
mActivity.onUsername(View::requestFocus);
callback.assertUiHiddenEvent(password);
callback.assertUiShownEvent(username);
}
// ...and select it this time
AuthenticationActivity.setResultCode(RESULT_OK);
sUiBot.selectDataset("Tap to auth dataset");
callback.assertUiHiddenEvent(username);
sUiBot.assertNoDatasets();
// Check the results.
mActivity.assertAutoFilled();
}
@Test
public void testDatasetAuthTwoFieldsReplaceResponse() throws Exception {
// Set service.
enableService();
final MyAutofillCallback callback = mActivity.registerCallback();
// Prepare the authenticated response
final IntentSender authentication = AuthenticationActivity.createSender(getContext(), 1,
new CannedFillResponse.Builder().addDataset(
new CannedDataset.Builder()
.setField(ID_USERNAME, "dude")
.setField(ID_PASSWORD, "sweet")
.setPresentation(createPresentation("Dataset"))
.build())
.build());
// Set up the authentication response client state
final Bundle authentionClientState = new Bundle();
authentionClientState.putCharSequence("clientStateKey1", "clientStateValue1");
// Configure the service behavior
sReplier.addResponse(new CannedFillResponse.Builder()
.addDataset(new CannedDataset.Builder()
.setField(ID_USERNAME, (AutofillValue) null)
.setField(ID_PASSWORD, (AutofillValue) null)
.setPresentation(createPresentation("Tap to auth dataset"))
.setAuthentication(authentication)
.build())
.setExtras(authentionClientState)
.build());
// Set expectation for the activity
mActivity.expectAutoFill("dude", "sweet");
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
// Wait for onFill() before proceeding.
sReplier.getNextFillRequest();
final View username = mActivity.getUsername();
// Authenticate
callback.assertUiShownEvent(username);
sUiBot.selectDataset("Tap to auth dataset");
callback.assertUiHiddenEvent(username);
// Select a dataset from the new response
callback.assertUiShownEvent(username);
sUiBot.selectDataset("Dataset");
callback.assertUiHiddenEvent(username);
sUiBot.assertNoDatasets();
// Check the results.
mActivity.assertAutoFilled();
final Bundle data = AuthenticationActivity.getData();
assertThat(data).isNotNull();
final String extraValue = data.getString("clientStateKey1");
assertThat(extraValue).isEqualTo("clientStateValue1");
}
@Test
public void testDatasetAuthTwoFieldsNoValues() throws Exception {
// Set service.
enableService();
final MyAutofillCallback callback = mActivity.registerCallback();
// Create the authentication intent
final IntentSender authentication = AuthenticationActivity.createSender(getContext(), 1,
new CannedDataset.Builder()
.setField(ID_USERNAME, "dude")
.setField(ID_PASSWORD, "sweet")
.setPresentation(createPresentation("Dataset"))
.build());
// Configure the service behavior
sReplier.addResponse(new CannedFillResponse.Builder()
.addDataset(new CannedDataset.Builder()
.setField(ID_USERNAME, (String) null)
.setField(ID_PASSWORD, (String) null)
.setPresentation(createPresentation("Tap to auth dataset"))
.setAuthentication(authentication)
.build())
.build());
// Set expectation for the activity
mActivity.expectAutoFill("dude", "sweet");
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
// Wait for onFill() before proceeding.
sReplier.getNextFillRequest();
final View username = mActivity.getUsername();
// Authenticate
callback.assertUiShownEvent(username);
sUiBot.selectDataset("Tap to auth dataset");
callback.assertUiHiddenEvent(username);
sUiBot.assertNoDatasets();
// Check the results.
mActivity.assertAutoFilled();
}
@Test
public void testDatasetAuthTwoDatasets() throws Exception {
// TODO: current API requires these fields...
final RemoteViews bogusPresentation = createPresentation("Whatever man, I'm not used...");
final String bogusValue = "Y U REQUIRE IT?";
// Set service.
enableService();
final MyAutofillCallback callback = mActivity.registerCallback();
// Create the authentication intents
final CannedDataset unlockedDataset = new CannedDataset.Builder()
.setField(ID_USERNAME, "dude")
.setField(ID_PASSWORD, "sweet")
.setPresentation(bogusPresentation)
.build();
final IntentSender authentication1 = AuthenticationActivity.createSender(getContext(), 1,
unlockedDataset);
final IntentSender authentication2 = AuthenticationActivity.createSender(getContext(), 2,
unlockedDataset);
// Configure the service behavior
sReplier.addResponse(new CannedFillResponse.Builder()
.addDataset(new CannedDataset.Builder()
.setField(ID_USERNAME, bogusValue)
.setField(ID_PASSWORD, bogusValue)
.setPresentation(createPresentation("Tap to auth dataset 1"))
.setAuthentication(authentication1)
.build())
.addDataset(new CannedDataset.Builder()
.setField(ID_USERNAME, bogusValue)
.setField(ID_PASSWORD, bogusValue)
.setPresentation(createPresentation("Tap to auth dataset 2"))
.setAuthentication(authentication2)
.build())
.build());
// Set expectation for the activity
mActivity.expectAutoFill("dude", "sweet");
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
// Wait for onFill() before proceeding.
sReplier.getNextFillRequest();
final View username = mActivity.getUsername();
// Authenticate
callback.assertUiShownEvent(username);
sUiBot.assertDatasets("Tap to auth dataset 1", "Tap to auth dataset 2");
sUiBot.selectDataset("Tap to auth dataset 1");
callback.assertUiHiddenEvent(username);
sUiBot.assertNoDatasets();
// Check the results.
mActivity.assertAutoFilled();
}
@Test
public void testDatasetAuthMixedSelectAuth() throws Exception {
datasetAuthMixedTest(true);
}
@Test
public void testDatasetAuthMixedSelectNonAuth() throws Exception {
datasetAuthMixedTest(false);
}
private void datasetAuthMixedTest(boolean selectAuth) throws Exception {
// Set service.
enableService();
final MyAutofillCallback callback = mActivity.registerCallback();
// Prepare the authenticated response
final IntentSender authentication = AuthenticationActivity.createSender(getContext(), 1,
new CannedDataset.Builder()
.setField(ID_USERNAME, "dude")
.setField(ID_PASSWORD, "sweet")
.setPresentation(createPresentation("Dataset"))
.build());
// Configure the service behavior
sReplier.addResponse(new CannedFillResponse.Builder()
.addDataset(new CannedDataset.Builder()
.setField(ID_USERNAME, "dude")
.setField(ID_PASSWORD, "sweet")
.setPresentation(createPresentation("Tap to auth dataset"))
.setAuthentication(authentication)
.build())
.addDataset(new CannedDataset.Builder()
.setField(ID_USERNAME, "DUDE")
.setField(ID_PASSWORD, "SWEET")
.setPresentation(createPresentation("What, me auth?"))
.build())
.build());
// Set expectation for the activity
if (selectAuth) {
mActivity.expectAutoFill("dude", "sweet");
} else {
mActivity.expectAutoFill("DUDE", "SWEET");
}
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
// Wait for onFill() before proceeding.
sReplier.getNextFillRequest();
final View username = mActivity.getUsername();
// Authenticate
callback.assertUiShownEvent(username);
sUiBot.assertDatasets("Tap to auth dataset", "What, me auth?");
final String chosenOne = selectAuth ? "Tap to auth dataset" : "What, me auth?";
sUiBot.selectDataset(chosenOne);
callback.assertUiHiddenEvent(username);
sUiBot.assertNoDatasets();
// Check the results.
mActivity.assertAutoFilled();
}
@Test
public void testDisableSelf() throws Exception {
enableService();
// Can disable while connected.
mActivity.runOnUiThread(() -> getContext().getSystemService(
AutofillManager.class).disableAutofillServices());
// Ensure disabled.
assertServiceDisabled();
}
@Test
public void testRejectStyleNegativeSaveButton() throws Exception {
enableService();
// Set service behavior.
final String intentAction = "android.autofillservice.cts.CUSTOM_ACTION";
// Configure the save UI.
final IntentSender listener = PendingIntent.getBroadcast(
getContext(), 0, new Intent(intentAction), 0).getIntentSender();
sReplier.addResponse(new CannedFillResponse.Builder()
.setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
.setNegativeAction(SaveInfo.NEGATIVE_BUTTON_STYLE_REJECT, listener)
.build());
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
// Wait for onFill() before proceeding.
sReplier.getNextFillRequest();
// Trigger save.
mActivity.onUsername((v) -> v.setText("foo"));
mActivity.onPassword((v) -> v.setText("foo"));
mActivity.tapLogin();
// Start watching for the negative intent
final CountDownLatch latch = new CountDownLatch(1);
final IntentFilter intentFilter = new IntentFilter(intentAction);
getContext().registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
getContext().unregisterReceiver(this);
latch.countDown();
}
}, intentFilter);
// Trigger the negative button.
sUiBot.saveForAutofill(SaveInfo.NEGATIVE_BUTTON_STYLE_REJECT,
false, SAVE_DATA_TYPE_PASSWORD);
// Wait for the custom action.
assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue();
assertNoDanglingSessions();
}
@Test
public void testCancelStyleNegativeSaveButton() throws Exception {
enableService();
// Set service behavior.
final String intentAction = "android.autofillservice.cts.CUSTOM_ACTION";
// Configure the save UI.
final IntentSender listener = PendingIntent.getBroadcast(
getContext(), 0, new Intent(intentAction), 0).getIntentSender();
sReplier.addResponse(new CannedFillResponse.Builder()
.setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
.setNegativeAction(SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, listener)
.build());
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
// Wait for onFill() before proceeding.
sReplier.getNextFillRequest();
// Trigger save.
mActivity.onUsername((v) -> v.setText("foo"));
mActivity.onPassword((v) -> v.setText("foo"));
mActivity.tapLogin();
// Start watching for the negative intent
final CountDownLatch latch = new CountDownLatch(1);
final IntentFilter intentFilter = new IntentFilter(intentAction);
getContext().registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
getContext().unregisterReceiver(this);
latch.countDown();
}
}, intentFilter);
// Trigger the negative button.
sUiBot.saveForAutofill(SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL,
false, SAVE_DATA_TYPE_PASSWORD);
// Wait for the custom action.
assertThat(latch.await(500, TimeUnit.SECONDS)).isTrue();
assertNoDanglingSessions();
}
@Test
public void testGetTextInputType() throws Exception {
// Set service.
enableService();
// Set expectations.
sReplier.addResponse(NO_RESPONSE);
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
// Assert input text on fill request:
final FillRequest fillRequest = sReplier.getNextFillRequest();
final ViewNode label = findNodeByResourceId(fillRequest.structure, ID_PASSWORD_LABEL);
assertThat(label.getInputType()).isEqualTo(TYPE_NULL);
final ViewNode password = findNodeByResourceId(fillRequest.structure, ID_PASSWORD);
assertWithMessage("No TYPE_TEXT_VARIATION_PASSWORD on %s", password.getInputType())
.that(password.getInputType() & TYPE_TEXT_VARIATION_PASSWORD)
.isEqualTo(TYPE_TEXT_VARIATION_PASSWORD);
}
@Test
public void testNoContainers() throws Exception {
// Set service.
enableService();
// Set expectations.
sReplier.addResponse(NO_RESPONSE);
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
sUiBot.assertNoDatasets();
final FillRequest fillRequest = sReplier.getNextFillRequest();
// Assert it only has 1 root view with 10 "leaf" nodes:
// 1.text view for app title
// 2.username text label
// 3.username text field
// 4.password text label
// 5.password text field
// 6.output text field
// 7.clear button
// 8.save button
// 9.login button
// 10.cancel button
//
// But it also has an intermediate container (for username) that should be included because
// it has a resource id.
assertNumberOfChildren(fillRequest.structure, 12);
// Make sure container with a resource id was included:
final ViewNode usernameContainer = findNodeByResourceId(fillRequest.structure,
ID_USERNAME_CONTAINER);
assertThat(usernameContainer).isNotNull();
assertThat(usernameContainer.getChildCount()).isEqualTo(2);
}
@Test
public void testAutofillManuallyOneDataset() throws Exception {
// Set service.
enableService();
// And activity.
mActivity.onUsername((v) -> v.setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_NO));
// Set expectations.
sReplier.addResponse(new CannedDataset.Builder()
.setField(ID_USERNAME, "dude")
.setField(ID_PASSWORD, "sweet")
.setPresentation(createPresentation("The Dude"))
.build());
mActivity.expectAutoFill("dude", "sweet");
// Explicitly uses the contextual menu to test that functionality.
sUiBot.getAutofillMenuOption(ID_USERNAME).click();
final FillRequest fillRequest = sReplier.getNextFillRequest();
assertThat(fillRequest.flags).isEqualTo(FLAG_MANUAL_REQUEST);
// Should have been automatically filled.
sUiBot.selectDataset("The Dude");
// Check the results.
mActivity.assertAutoFilled();
}
@Test
public void testAutofillManuallyTwoDatasetsPickFirst() throws Exception {
autofillManuallyTwoDatasets(true);
}
@Test
public void testAutofillManuallyTwoDatasetsPickSecond() throws Exception {
autofillManuallyTwoDatasets(false);
}
private void autofillManuallyTwoDatasets(boolean pickFirst) throws Exception {
// Set service.
enableService();
// Set expectations.
sReplier.addResponse(new CannedFillResponse.Builder()
.addDataset(new CannedDataset.Builder()
.setField(ID_USERNAME, "dude")
.setField(ID_PASSWORD, "sweet")
.setPresentation(createPresentation("The Dude"))
.build())
.addDataset(new CannedDataset.Builder()
.setField(ID_USERNAME, "jenny")
.setField(ID_PASSWORD, "8675309")
.setPresentation(createPresentation("Jenny"))
.build())
.build());
if (pickFirst) {
mActivity.expectAutoFill("dude", "sweet");
} else {
mActivity.expectAutoFill("jenny", "8675309");
}
// Force a manual autofill request.
mActivity.forceAutofillOnUsername();
final FillRequest fillRequest = sReplier.getNextFillRequest();
assertThat(fillRequest.flags).isEqualTo(FLAG_MANUAL_REQUEST);
// Auto-fill it.
final UiObject2 picker = sUiBot.assertDatasets("The Dude", "Jenny");
sUiBot.selectDataset(picker, pickFirst ? "The Dude" : "Jenny");
// Check the results.
mActivity.assertAutoFilled();
}
@Test
public void testAutofillManuallyPartialField() throws Exception {
// Set service.
enableService();
// And activity.
mActivity.onUsername((v) -> v.setText("dud"));
mActivity.onPassword((v) -> v.setText("IamSecretMan"));
// Set expectations.
sReplier.addResponse(new CannedDataset.Builder()
.setField(ID_USERNAME, "dude")
.setField(ID_PASSWORD, "sweet")
.setPresentation(createPresentation("The Dude"))
.build());
mActivity.expectAutoFill("dude", "sweet");
// Force a manual autofill request.
mActivity.forceAutofillOnUsername();
final FillRequest fillRequest = sReplier.getNextFillRequest();
assertThat(fillRequest.flags).isEqualTo(FLAG_MANUAL_REQUEST);
// Username value should be available because it triggered the manual request...
assertValue(fillRequest.structure, ID_USERNAME, "dud");
// ... but password didn't
assertTextIsSanitized(fillRequest.structure, ID_PASSWORD);
// Selects the dataset.
sUiBot.selectDataset("The Dude");
// Check the results.
mActivity.assertAutoFilled();
}
@Test
public void testAutofillManuallyAgainAfterAutomaticallyAutofilledBefore() throws Exception {
// Set service.
enableService();
/*
* 1st fill (automatic).
*/
// Set expectations.
sReplier.addResponse(new CannedDataset.Builder()
.setField(ID_USERNAME, "dude")
.setField(ID_PASSWORD, "sweet")
.setPresentation(createPresentation("The Dude"))
.build());
mActivity.expectAutoFill("dude", "sweet");
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
// Assert request.
final FillRequest fillRequest1 = sReplier.getNextFillRequest();
assertThat(fillRequest1.flags).isEqualTo(0);
assertTextIsSanitized(fillRequest1.structure, ID_USERNAME);
assertTextIsSanitized(fillRequest1.structure, ID_PASSWORD);
// Select it.
sUiBot.selectDataset("The Dude");
// Check the results.
mActivity.assertAutoFilled();
/*
* 2nd fill (manual).
*/
// Set expectations.
sReplier.addResponse(new CannedDataset.Builder()
.setField(ID_USERNAME, "DUDE")
.setField(ID_PASSWORD, "SWEET")
.setPresentation(createPresentation("THE DUDE"))
.build());
mActivity.expectAutoFill("DUDE", "SWEET");
// Change password to make sure it's not sent to the service.
mActivity.onPassword((v) -> v.setText("IamSecretMan"));
// Trigger auto-fill.
mActivity.forceAutofillOnUsername();
// Assert request.
final FillRequest fillRequest2 = sReplier.getNextFillRequest();
assertThat(fillRequest2.flags).isEqualTo(FLAG_MANUAL_REQUEST);
assertValue(fillRequest2.structure, ID_USERNAME, "dude");
assertTextIsSanitized(fillRequest2.structure, ID_PASSWORD);
// Select it.
sUiBot.selectDataset("THE DUDE");
// Check the results.
mActivity.assertAutoFilled();
}
@Test
public void testAutofillManuallyAgainAfterManuallyAutofilledBefore() throws Exception {
// Set service.
enableService();
/*
* 1st fill (manual).
*/
// Set expectations.
sReplier.addResponse(new CannedDataset.Builder()
.setField(ID_USERNAME, "dude")
.setField(ID_PASSWORD, "sweet")
.setPresentation(createPresentation("The Dude"))
.build());
mActivity.expectAutoFill("dude", "sweet");
// Trigger auto-fill.
mActivity.forceAutofillOnUsername();
// Assert request.
final FillRequest fillRequest1 = sReplier.getNextFillRequest();
assertThat(fillRequest1.flags).isEqualTo(FLAG_MANUAL_REQUEST);
assertValue(fillRequest1.structure, ID_USERNAME, "");
assertTextIsSanitized(fillRequest1.structure, ID_PASSWORD);
// Select it.
sUiBot.selectDataset("The Dude");
// Check the results.
mActivity.assertAutoFilled();
/*
* 2nd fill (manual).
*/
// Set expectations.
sReplier.addResponse(new CannedDataset.Builder()
.setField(ID_USERNAME, "DUDE")
.setField(ID_PASSWORD, "SWEET")
.setPresentation(createPresentation("THE DUDE"))
.build());
mActivity.expectAutoFill("DUDE", "SWEET");
// Change password to make sure it's not sent to the service.
mActivity.onPassword((v) -> v.setText("IamSecretMan"));
// Trigger auto-fill.
mActivity.forceAutofillOnUsername();
// Assert request.
final FillRequest fillRequest2 = sReplier.getNextFillRequest();
assertThat(fillRequest2.flags).isEqualTo(FLAG_MANUAL_REQUEST);
assertValue(fillRequest2.structure, ID_USERNAME, "dude");
assertTextIsSanitized(fillRequest2.structure, ID_PASSWORD);
// Select it.
sUiBot.selectDataset("THE DUDE");
// Check the results.
mActivity.assertAutoFilled();
}
@Test
public void testCommitMultipleTimes() throws Throwable {
// Set service.
enableService();
final CannedFillResponse response = new CannedFillResponse.Builder()
.setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
.build();
for (int i = 1; i <= 3; i++) {
final String username = "user-" + i;
final String password = "pass-" + i;
try {
// Set expectations.
sReplier.addResponse(response);
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
// Sanity check.
sUiBot.assertNoDatasets();
// Wait for onFill() before proceeding, otherwise the fields might be changed before
// the session started
waitUntilConnected();
sReplier.getNextFillRequest();
// Set credentials...
mActivity.onUsername((v) -> v.setText(username));
mActivity.onPassword((v) -> v.setText(password));
// Change focus to prepare for next step - must do it before session is gone
mActivity.onPassword(View::requestFocus);
// ...and save them
mActivity.tapSave();
// Assert the snack bar is shown and tap "Save".
sUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
final SaveRequest saveRequest = sReplier.getNextSaveRequest();
// Assert value of expected fields - should not be sanitized.
final ViewNode usernameNode = findNodeByResourceId(saveRequest.structure,
ID_USERNAME);
assertTextAndValue(usernameNode, username);
final ViewNode passwordNode = findNodeByResourceId(saveRequest.structure,
ID_PASSWORD);
assertTextAndValue(passwordNode, password);
waitUntilDisconnected();
assertNoDanglingSessions();
} catch (RetryableException e) {
throw e;
} catch (Throwable t) {
throw new Throwable("Error on step " + i, t);
}
}
}
@Test
public void testCancelMultipleTimes() throws Throwable {
// Set service.
enableService();
for (int i = 1; i <= 3; i++) {
final String username = "user-" + i;
final String password = "pass-" + i;
sReplier.addResponse(new CannedDataset.Builder()
.setField(ID_USERNAME, username)
.setField(ID_PASSWORD, password)
.setPresentation(createPresentation("The Dude"))
.build());
mActivity.expectAutoFill(username, password);
try {
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
waitUntilConnected();
sReplier.getNextFillRequest();
// Auto-fill it.
sUiBot.selectDataset("The Dude");
// Check the results.
mActivity.assertAutoFilled();
// Change focus to prepare for next step - must do it before session is gone
mActivity.onPassword(View::requestFocus);
// Rinse and repeat...
mActivity.tapClear();
waitUntilDisconnected();
assertNoDanglingSessions();
} catch (RetryableException e) {
throw e;
} catch (Throwable t) {
throw new Throwable("Error on step " + i, t);
}
}
}
@Test
public void testClickCustomButton() throws Exception {
// Set service.
enableService();
Intent intent = new Intent(getContext(), EmptyActivity.class);
IntentSender sender = PendingIntent.getActivity(getContext(), 0, intent,
PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT)
.getIntentSender();
RemoteViews presentation = new RemoteViews(getContext().getPackageName(),
R.layout.list_item);
presentation.setTextViewText(R.id.text1, "Poke");
Intent firstIntent = new Intent(getContext(), DummyActivity.class);
presentation.setOnClickPendingIntent(R.id.text1, PendingIntent.getActivity(
getContext(), 0, firstIntent, PendingIntent.FLAG_ONE_SHOT
| PendingIntent.FLAG_CANCEL_CURRENT));
// Set expectations.
sReplier.addResponse(new CannedFillResponse.Builder()
.setAuthentication(sender, ID_USERNAME)
.setPresentation(presentation)
.build());
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
// Wait for onFill() before proceeding.
sReplier.getNextFillRequest();
// Click on the custom button
sUiBot.selectByText("Poke");
// Make sure the click worked
sUiBot.selectByText("foo");
// Go back to the filled app.
sUiBot.pressBack();
// The session should be gone
assertNoDanglingSessions();
}
@Test
public void checkFillSelectionAfterSelectingDatasetAuthentication() 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(getContext(), 1,
new CannedDataset.Builder()
.setField(ID_USERNAME, "dude")
.setField(ID_PASSWORD, "sweet")
.setPresentation(createPresentation("Dataset"))
.build());
sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
new CannedDataset.Builder()
.setField(ID_USERNAME, "username")
.setId("name")
.setPresentation(createPresentation("authentication"))
.setAuthentication(authentication)
.build())
.setExtras(clientState).build());
// Trigger autofill.
mActivity.onUsername(View::requestFocus);
// Authenticate
sUiBot.selectDataset("authentication");
sReplier.getNextFillRequest();
eventually(() -> {
// Verify fill selection
FillEventHistory selection = InstrumentedAutoFillService.peekInstance()
.getFillEventHistory();
assertThat(selection.getClientState().getCharSequence("clientStateKey")).isEqualTo(
"clientStateValue");
assertThat(selection.getEvents().size()).isEqualTo(1);
FillEventHistory.Event event = selection.getEvents().get(0);
assertThat(event.getType()).isEqualTo(TYPE_DATASET_AUTHENTICATION_SELECTED);
assertThat(event.getDatasetId()).isEqualTo("name");
});
}
@Test
public void checkFillSelectionAfterSelectingAuthentication() 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(getContext(), 1,
new CannedFillResponse.Builder().addDataset(
new CannedDataset.Builder()
.setField(ID_USERNAME, "username")
.setId("name")
.setPresentation(createPresentation("dataset"))
.build())
.setExtras(clientState).build());
sReplier.addResponse(new CannedFillResponse.Builder().setExtras(clientState)
.setPresentation(createPresentation("authentication"))
.setAuthentication(authentication, ID_USERNAME)
.build());
// Trigger autofill.
mActivity.onUsername(View::requestFocus);
// Authenticate
sUiBot.selectDataset("authentication");
sReplier.getNextFillRequest();
eventually(() -> {
// Verify fill selection
FillEventHistory selection = InstrumentedAutoFillService.peekInstance()
.getFillEventHistory();
assertThat(selection.getClientState().getCharSequence("clientStateKey")).isEqualTo(
"clientStateValue");
assertThat(selection.getEvents().size()).isEqualTo(1);
FillEventHistory.Event event = selection.getEvents().get(0);
assertThat(event.getType()).isEqualTo(TYPE_AUTHENTICATION_SELECTED);
assertThat(event.getDatasetId()).isNull();
});
}
@Test
public void checkFillSelectionAfterSelectingTwoDatasets() throws Exception {
enableService();
// Set up first partition with an anonymous dataset
sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
new CannedDataset.Builder()
.setField(ID_USERNAME, "username")
.setPresentation(createPresentation("dataset1"))
.build())
.build());
// Trigger autofill on username
mActivity.onUsername(View::requestFocus);
sUiBot.selectDataset("dataset1");
sReplier.getNextFillRequest();
eventually(() -> {
// Verify fill selection
FillEventHistory selection = InstrumentedAutoFillService.peekInstance()
.getFillEventHistory();
assertThat(selection.getClientState()).isNull();
assertThat(selection.getEvents().size()).isEqualTo(1);
FillEventHistory.Event event = selection.getEvents().get(0);
assertThat(event.getType()).isEqualTo(TYPE_DATASET_SELECTED);
assertThat(event.getDatasetId()).isNull();
});
// Set up second partition with a named dataset
Bundle clientState = new Bundle();
clientState.putCharSequence("clientStateKey", "clientStateValue");
sReplier.addResponse(new CannedFillResponse.Builder()
.addDataset(
new CannedDataset.Builder()
.setField(ID_PASSWORD, "password2")
.setPresentation(createPresentation("dataset2"))
.setId("name2")
.build())
.addDataset(
new CannedDataset.Builder()
.setField(ID_PASSWORD, "password3")
.setPresentation(createPresentation("dataset3"))
.setId("name3")
.build())
.setExtras(clientState)
.setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_PASSWORD).build());
// Trigger autofill on password
mActivity.onPassword(View::requestFocus);
sUiBot.selectDataset("dataset3");
sReplier.getNextFillRequest();
eventually(() -> {
// Verify fill selection
FillEventHistory selection = InstrumentedAutoFillService.peekInstance()
.getFillEventHistory();
assertThat(selection.getClientState().getCharSequence("clientStateKey")).isEqualTo(
"clientStateValue");
assertThat(selection.getEvents().size()).isEqualTo(1);
FillEventHistory.Event event = selection.getEvents().get(0);
assertThat(event.getType()).isEqualTo(TYPE_DATASET_SELECTED);
assertThat(event.getDatasetId()).isEqualTo("name3");
});
mActivity.onPassword((v) -> v.setText("new password"));
mActivity.syncRunOnUiThread(() -> mActivity.finish());
eventually(() -> {
// Verify fill selection
FillEventHistory selection = InstrumentedAutoFillService.peekInstance()
.getFillEventHistory();
assertThat(selection.getClientState().getCharSequence("clientStateKey")).isEqualTo(
"clientStateValue");
assertThat(selection.getEvents().size()).isEqualTo(2);
FillEventHistory.Event event1 = selection.getEvents().get(0);
assertThat(event1.getType()).isEqualTo(TYPE_DATASET_SELECTED);
assertThat(event1.getDatasetId()).isEqualTo("name3");
FillEventHistory.Event event2 = selection.getEvents().get(1);
assertThat(event2.getType()).isEqualTo(TYPE_SAVE_SHOWN);
assertThat(event2.getDatasetId()).isNull();
});
}
@Test
public void checkFillSelectionIsResetAfterReturningNull() throws Exception {
enableService();
// First reset
sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
new CannedDataset.Builder()
.setField(ID_USERNAME, "username")
.setPresentation(createPresentation("dataset1"))
.build())
.build());
mActivity.onUsername(View::requestFocus);
sReplier.getNextFillRequest();
sUiBot.selectDataset("dataset1");
eventually(() -> {
// Verify fill selection
FillEventHistory selection = InstrumentedAutoFillService.peekInstance()
.getFillEventHistory();
assertThat(selection.getClientState()).isNull();
assertThat(selection.getEvents().size()).isEqualTo(1);
FillEventHistory.Event event = selection.getEvents().get(0);
assertThat(event.getType()).isEqualTo(TYPE_DATASET_SELECTED);
assertThat(event.getDatasetId()).isNull();
});
// Second request
sReplier.addResponse(NO_RESPONSE);
mActivity.onPassword(View::requestFocus);
sReplier.getNextFillRequest();
sUiBot.assertNoDatasets();
eventually(() -> {
// Verify fill selection
FillEventHistory selection = InstrumentedAutoFillService.peekInstance()
.getFillEventHistory();
assertThat(selection).isNull();
});
}
@Test
public void checkFillSelectionIsResetAfterReturningError() throws Exception {
enableService();
// First reset
sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
new CannedDataset.Builder()
.setField(ID_USERNAME, "username")
.setPresentation(createPresentation("dataset1"))
.build())
.build());
mActivity.onUsername(View::requestFocus);
waitUntilConnected();
sReplier.getNextFillRequest();
sUiBot.selectDataset("dataset1");
eventually(() -> {
// Verify fill selection
FillEventHistory selection = InstrumentedAutoFillService.peekInstance()
.getFillEventHistory();
assertThat(selection.getClientState()).isNull();
assertThat(selection.getEvents().size()).isEqualTo(1);
FillEventHistory.Event event = selection.getEvents().get(0);
assertThat(event.getType()).isEqualTo(TYPE_DATASET_SELECTED);
assertThat(event.getDatasetId()).isNull();
});
// Second request
sReplier.addResponse(new CannedFillResponse.Builder().returnFailure("D'OH!").build());
mActivity.onPassword(View::requestFocus);
sReplier.getNextFillRequest();
sUiBot.assertNoDatasets();
eventually(() -> {
// Verify fill selection
FillEventHistory selection = InstrumentedAutoFillService.peekInstance()
.getFillEventHistory();
assertThat(selection).isNull();
});
}
@Test
public void checkFillSelectionIsResetAfterTimeout() throws Exception {
enableService();
// First reset
sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
new CannedDataset.Builder()
.setField(ID_USERNAME, "username")
.setPresentation(createPresentation("dataset1"))
.build())
.build());
mActivity.onUsername(View::requestFocus);
waitUntilConnected();
sReplier.getNextFillRequest();
sUiBot.selectDataset("dataset1");
eventually(() -> {
// Verify fill selection
FillEventHistory selection = InstrumentedAutoFillService.peekInstance()
.getFillEventHistory();
assertThat(selection.getClientState()).isNull();
assertThat(selection.getEvents().size()).isEqualTo(1);
FillEventHistory.Event event = selection.getEvents().get(0);
assertThat(event.getType()).isEqualTo(TYPE_DATASET_SELECTED);
assertThat(event.getDatasetId()).isNull();
});
// Second request
sReplier.addResponse(DO_NOT_REPLY_RESPONSE);
mActivity.onPassword(View::requestFocus);
sReplier.getNextFillRequest();
waitUntilDisconnected();
eventually(() -> {
// Verify fill selection
FillEventHistory selection = InstrumentedAutoFillService.peekInstance()
.getFillEventHistory();
assertThat(selection).isNull();
});
}
private Bundle getBundle(String key, String value) {
final Bundle bundle = new Bundle();
bundle.putString(key, value);
return bundle;
}
/**
* 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>User triggers save on Activity A - at this point, service should have stats of
* activity B, and stats for activity A should have beeen discarded.
* </ol>
*/
@Test
public void checkFillSelectionFromPreviousSessionIsDiscarded() 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 on activity A
mActivity.onUsername(View::requestFocus);
waitUntilConnected();
sReplier.getNextFillRequest();
// Verify fill selection for Activity A
FillEventHistory selectionA = InstrumentedAutoFillService.peekInstance()
.getFillEventHistory();
assertThat(selectionA.getClientState().getString("activity")).isEqualTo("A");
assertThat(selectionA.getEvents()).isNull();
// Launch activity B
getContext().startActivity(new Intent(getContext(), CheckoutActivity.class));
// Trigger autofill on activity B
sReplier.addResponse(new CannedFillResponse.Builder()
.setExtras(getBundle("activity", "B"))
.addDataset(new CannedDataset.Builder()
.setField(ID_CC_NUMBER, "4815162342")
.setPresentation(createPresentation("datasetB"))
.build())
.build());
sUiBot.focusByRelativeId(ID_CC_NUMBER);
sReplier.getNextFillRequest();
// Verify fill selection for Activity B
final FillEventHistory selectionB = InstrumentedAutoFillService.peekInstance()
.getFillEventHistory();
assertThat(selectionB.getClientState().getString("activity")).isEqualTo("B");
assertThat(selectionB.getEvents()).isNull();
// Now switch back to A...
sUiBot.pressBack(); // dismiss keyboard
sUiBot.pressBack(); // dismiss task
// ...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);
sUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
sReplier.getNextSaveRequest();
// Finally, make sure history is right
final FillEventHistory finalSelection = InstrumentedAutoFillService.peekInstance()
.getFillEventHistory();
assertThat(finalSelection.getClientState().getString("activity")).isEqualTo("B");
assertThat(finalSelection.getEvents()).isNull();
}
@Test
public void testIsServiceEnabled() throws Exception {
disableService();
final AutofillManager afm = mActivity.getAutofillManager();
assertThat(afm.hasEnabledAutofillServices()).isFalse();
try {
enableService();
assertThat(afm.hasEnabledAutofillServices()).isTrue();
} finally {
disableService();
}
}
@Test
public void testSetupComplete() throws Exception {
enableService();
// Sanity check.
final AutofillManager afm = mActivity.getAutofillManager();
assertThat(afm.isEnabled()).isTrue();
// Now disable user_complete and try again.
try {
setUserComplete(getContext(), false);
assertThat(afm.isEnabled()).isFalse();
} finally {
setUserComplete(getContext(), true);
}
}
@Test
public void testPopupGoesAwayWhenServiceIsChanged() throws Exception {
// Set service.
enableService();
// Set expectations.
sReplier.addResponse(new CannedDataset.Builder()
.setField(ID_USERNAME, "dude")
.setField(ID_PASSWORD, "sweet")
.setPresentation(createPresentation("The Dude"))
.build());
mActivity.expectAutoFill("dude", "sweet");
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
sReplier.getNextFillRequest();
sUiBot.assertDatasets("The Dude");
// Now disable service by setting another service
Helper.enableAutofillService(getContext(), NoOpAutofillService.SERVICE_NAME);
// ...and make sure popup's gone
sUiBot.assertNoDatasets();
}
@Test
public void testAutofillMovesCursorToTheEnd() throws Exception {
// Set service.
enableService();
// Set expectations.
sReplier.addResponse(new CannedDataset.Builder()
.setField(ID_USERNAME, "dude")
.setField(ID_PASSWORD, "sweet")
.setPresentation(createPresentation("The Dude"))
.build());
mActivity.expectAutoFill("dude", "sweet");
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
sReplier.getNextFillRequest();
// Auto-fill it.
sUiBot.selectDataset("The Dude");
// Check the results.
mActivity.assertAutoFilled();
// NOTE: need to call getSelectionEnd() inside the UI thread, otherwise it returns 0
final AtomicInteger atomicBombToKillASmallInsect = new AtomicInteger();
mActivity.onUsername((v) -> atomicBombToKillASmallInsect.set(v.getSelectionEnd()));
assertWithMessage("Wrong position on username").that(atomicBombToKillASmallInsect.get())
.isEqualTo(4);
mActivity.onPassword((v) -> atomicBombToKillASmallInsect.set(v.getSelectionEnd()));
assertWithMessage("Wrong position on password").that(atomicBombToKillASmallInsect.get())
.isEqualTo(5);
}
@Test
public void testAutofillLargeNumberOfDatasets() throws Exception {
// Set service.
enableService();
final StringBuilder bigStringBuilder = new StringBuilder();
for (int i = 0; i < 10_000 ; i++) {
bigStringBuilder.append("BigAmI");
}
final String bigString = bigStringBuilder.toString();
final int size = 100;
Log.d(TAG, "testAutofillLargeNumberOfDatasets(): " + size + " datasets with "
+ bigString.length() +"-bytes id");
final CannedFillResponse.Builder response = new CannedFillResponse.Builder();
for (int i = 0; i < size; i++) {
final String suffix = "-" + (i + 1);
response.addDataset(new CannedDataset.Builder()
.setField(ID_USERNAME, "user" + suffix)
.setField(ID_PASSWORD, "pass" + suffix)
.setId(bigString)
.setPresentation(createPresentation("DS" + suffix))
.build());
}
// Set expectations.
sReplier.addResponse(response.build());
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
sReplier.getNextFillRequest();
// Make sure all datasets are shown.
// TODO: improve assertDatasets() so it supports scrolling, and assert all of them are
// shown
sUiBot.assertDatasets("DS-1", "DS-2", "DS-3");
// TODO: once it supports scrolling, selects the last dataset and asserts it's filled.
}
@Test
public void testCancellationSignalCalledAfterTimeout() throws Exception {
// Set service.
enableService();
// Set expectations.
final OneTimeCancellationSignalListener listener =
new OneTimeCancellationSignalListener(Helper.FILL_TIMEOUT_MS + 2000);
sReplier.addResponse(DO_NOT_REPLY_RESPONSE);
// Trigger auto-fill.
mActivity.onUsername(View::requestFocus);
// Attach listener to CancellationSignal.
waitUntilConnected();
sReplier.getNextFillRequest().cancellationSignal.setOnCancelListener(listener);
// AssertResults
waitUntilDisconnected();
listener.assertOnCancelCalled();
}
private void startCheckoutActivityAsNewTask() {
final Context context = getContext();
final Intent intent = new Intent(context, CheckoutActivity.class);
intent.setFlags(
Intent.FLAG_ACTIVITY_NEW_DOCUMENT | Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS);
context.startActivity(intent);
sUiBot.assertShownByRelativeId(CheckoutActivity.ID_ADDRESS);
}
}