blob: 01098e9e1f43ff21a0edfeda459a9c84187a9d77 [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.autofillservice.cts.Helper.ID_LOGIN;
import static android.autofillservice.cts.Helper.ID_PASSWORD;
import static android.autofillservice.cts.Helper.ID_USERNAME;
import static android.autofillservice.cts.Helper.assertTextAndValue;
import static android.autofillservice.cts.Helper.findNodeByResourceId;
import static android.autofillservice.cts.Helper.getContext;
import static android.autofillservice.cts.OutOfProcessLoginActivity.getStartedMarker;
import static android.autofillservice.cts.OutOfProcessLoginActivity.getStoppedMarker;
import static android.autofillservice.cts.UiBot.LANDSCAPE;
import static android.autofillservice.cts.UiBot.PORTRAIT;
import static android.autofillservice.cts.common.ShellHelper.runShellCommand;
import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_USERNAME;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assume.assumeTrue;
import android.app.PendingIntent;
import android.app.assist.AssistStructure;
import android.content.Intent;
import android.content.IntentSender;
import android.os.Bundle;
import android.os.SystemClock;
import android.platform.test.annotations.AppModeFull;
import android.support.test.uiautomator.UiObject2;
import android.view.autofill.AutofillValue;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.concurrent.Callable;
/**
* Test the lifecycle of a autofill session
*/
@AppModeFull // This test requires android.permission.WRITE_EXTERNAL_STORAGE
public class SessionLifecycleTest extends AutoFillServiceTestCase {
private static final String ID_BUTTON = "button";
private static final String ID_CANCEL = "cancel";
/**
* Delay for activity start/stop.
*/
// TODO: figure out a better way to wait without using sleep().
private static final long WAIT_ACTIVITY_MS = 1000;
private static final Timeout SESSION_LIFECYCLE_TIMEOUT = new Timeout(
"SESSION_LIFECYCLE_TIMEOUT", 2000, 2F, 5000);
/**
* Runs an {@code assertion}, retrying until {@code timeout} is reached.
*/
private static void eventually(String description, Callable<Boolean> assertion)
throws Exception {
SESSION_LIFECYCLE_TIMEOUT.run(description, assertion);
}
public SessionLifecycleTest() {
super(new UiBot(SESSION_LIFECYCLE_TIMEOUT));
}
/**
* Prevents the screen to rotate by itself
*/
@Before
public void disableAutoRotation() throws Exception {
Helper.disableAutoRotation(mUiBot);
}
/**
* Allows the screen to rotate by itself
*/
@After
public void allowAutoRotation() {
Helper.allowAutoRotation();
}
@After
public void finishLoginActivityOnAnotherProcess() throws Exception {
runShellCommand("am broadcast --receiver-foreground "
+ "-n android.autofillservice.cts/.OutOfProcessLoginActivityFinisherReceiver");
mUiBot.assertGoneByRelativeId(ID_USERNAME, Timeouts.ACTIVITY_RESURRECTION);
}
private void killOfProcessLoginActivityProcess() throws Exception {
// Waiting for activity to stop (stop marker appears)
eventually("getStoppedMarker()", () -> {
return getStoppedMarker(getContext()).exists();
});
// onStop might not be finished, hence wait more
SystemClock.sleep(WAIT_ACTIVITY_MS);
// Kill activity that is in the background
runShellCommand("am broadcast --receiver-foreground "
+ "-n android.autofillservice.cts/.SelfDestructReceiver");
}
private void startAndWaitExternalActivity() throws Exception {
final Intent outOfProcessAcvitityStartIntent = new Intent(getContext(),
OutOfProcessLoginActivity.class).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
getStartedMarker(getContext()).delete();
getContext().startActivity(outOfProcessAcvitityStartIntent);
eventually("getStartedMarker()", () -> {
return getStartedMarker(getContext()).exists();
});
getStartedMarker(getContext()).delete();
// Even if we wait the activity started, UiObject still fails. Have to wait a little bit.
SystemClock.sleep(WAIT_ACTIVITY_MS);
mUiBot.assertShownByRelativeId(ID_USERNAME);
}
@Test
public void testDatasetAuthResponseWhileAutofilledAppIsLifecycled() throws Exception {
assumeTrue("Rotation is supported", Helper.isRotationSupported(mContext));
// Set service.
enableService();
// Start activity that is autofilled in a separate process so it can be killed
startAndWaitExternalActivity();
// Set expectations.
final Bundle extras = new Bundle();
extras.putString("numbers", "4815162342");
// Create the authentication intent (launching a full screen activity)
IntentSender authentication = PendingIntent.getActivity(getContext(), 0,
new Intent(getContext(), ManualAuthenticationActivity.class),
0).getIntentSender();
// Prepare the authenticated response
ManualAuthenticationActivity.setResponse(new CannedFillResponse.Builder()
.addDataset(new CannedFillResponse.CannedDataset.Builder()
.setField(ID_USERNAME, AutofillValue.forText("autofilled username"))
.setPresentation(createPresentation("dataset")).build())
.setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
.setExtras(extras).build());
CannedFillResponse response = new CannedFillResponse.Builder()
.setAuthentication(authentication, ID_USERNAME, ID_PASSWORD)
.setPresentation(createPresentation("authenticate"))
.build();
sReplier.addResponse(response);
// Trigger autofill on username
mUiBot.selectByRelativeId(ID_USERNAME);
// Wait for fill request to be processed
sReplier.getNextFillRequest();
// Wait until authentication is shown
mUiBot.assertDatasets("authenticate");
// Change orientation which triggers a destroy -> create in the app as the activity
// cannot deal with such situations
mUiBot.setScreenOrientation(LANDSCAPE);
mUiBot.setScreenOrientation(PORTRAIT);
// Wait context and Views being recreated in rotation
mUiBot.assertShownByRelativeId(ID_USERNAME);
// Delete stopped marker
getStoppedMarker(getContext()).delete();
// Authenticate
mUiBot.selectDataset("authenticate");
// Kill activity that is in the background
killOfProcessLoginActivityProcess();
// Change orientation which triggers a destroy -> create in the app as the activity
// cannot deal with such situations
mUiBot.setScreenOrientation(PORTRAIT);
// Approve authentication
mUiBot.selectByRelativeId(ID_BUTTON);
// Wait for dataset to be shown
mUiBot.assertDatasets("dataset");
// Change orientation which triggers a destroy -> create in the app as the activity
// cannot deal with such situations
mUiBot.setScreenOrientation(LANDSCAPE);
// Select dataset
mUiBot.selectDataset("dataset");
// Check the results.
eventually("getTextById(" + ID_USERNAME + ")", () -> {
return mUiBot.getTextByRelativeId(ID_USERNAME).equals("autofilled username");
});
// Set password
mUiBot.setTextByRelativeId(ID_PASSWORD, "new password");
// Login
mUiBot.selectByRelativeId(ID_LOGIN);
// Wait for save UI to be shown
mUiBot.assertSaveShowing(SAVE_DATA_TYPE_PASSWORD);
// Change orientation to make sure save UI can handle this
mUiBot.setScreenOrientation(PORTRAIT);
// Tap "Save".
mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
// Get save request
InstrumentedAutoFillService.SaveRequest saveRequest = sReplier.getNextSaveRequest();
assertWithMessage("onSave() not called").that(saveRequest).isNotNull();
// Make sure data is correctly saved
final AssistStructure.ViewNode username = findNodeByResourceId(saveRequest.structure,
ID_USERNAME);
assertTextAndValue(username, "autofilled username");
final AssistStructure.ViewNode password = findNodeByResourceId(saveRequest.structure,
ID_PASSWORD);
assertTextAndValue(password, "new password");
// 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");
}
@Test
public void testAuthCanceledWhileAutofilledAppIsLifecycled() throws Exception {
// Set service.
enableService();
// Start activity that is autofilled in a separate process so it can be killed
startAndWaitExternalActivity();
// Create the authentication intent (launching a full screen activity)
IntentSender authentication = PendingIntent.getActivity(getContext(), 0,
new Intent(getContext(), ManualAuthenticationActivity.class),
0).getIntentSender();
CannedFillResponse response = new CannedFillResponse.Builder()
.setAuthentication(authentication, ID_USERNAME, ID_PASSWORD)
.setPresentation(createPresentation("authenticate"))
.build();
sReplier.addResponse(response);
// Trigger autofill on username
mUiBot.selectByRelativeId(ID_USERNAME);
// Wait for fill request to be processed
sReplier.getNextFillRequest();
// Wait until authentication is shown
mUiBot.assertDatasets("authenticate");
// Delete stopped marker
getStoppedMarker(getContext()).delete();
// Authenticate
mUiBot.selectDataset("authenticate");
// Kill activity that is in the background
killOfProcessLoginActivityProcess();
// Cancel authentication activity
mUiBot.pressBack();
// Authentication should still be shown
mUiBot.assertDatasets("authenticate");
}
@Test
public void testDatasetVisibleWhileAutofilledAppIsLifecycled() throws Exception {
// Set service.
enableService();
// Start activity that is autofilled in a separate process so it can be killed
startAndWaitExternalActivity();
CannedFillResponse response = new CannedFillResponse.Builder()
.addDataset(new CannedFillResponse.CannedDataset.Builder(
createPresentation("dataset"))
.setField(ID_USERNAME, "filled").build())
.build();
sReplier.addResponse(response);
// Trigger autofill on username
mUiBot.selectByRelativeId(ID_USERNAME);
// Wait for fill request to be processed
sReplier.getNextFillRequest();
// Wait until dataset is shown
mUiBot.assertDatasets("dataset");
// Delete stopped marker
getStoppedMarker(getContext()).delete();
// Start an activity on top of the autofilled activity
Intent intent = new Intent(getContext(), EmptyActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
getContext().startActivity(intent);
// Kill activity that is in the background
killOfProcessLoginActivityProcess();
// Cancel activity on top
mUiBot.pressBack();
// Dataset should still be shown
mUiBot.assertDatasets("dataset");
}
@Test
public void testAutofillNestedActivitiesWhileAutofilledAppIsLifecycled() throws Exception {
// Set service.
enableService();
// Start activity that is autofilled in a separate process so it can be killed
startAndWaitExternalActivity();
// Prepare response for first activity
CannedFillResponse response = new CannedFillResponse.Builder()
.addDataset(new CannedFillResponse.CannedDataset.Builder(
createPresentation("dataset1"))
.setField(ID_USERNAME, "filled").build())
.build();
sReplier.addResponse(response);
// Trigger autofill on username
mUiBot.selectByRelativeId(ID_USERNAME);
// Wait for fill request to be processed
sReplier.getNextFillRequest();
// Wait until dataset1 is shown
mUiBot.assertDatasets("dataset1");
// Delete stopped marker
getStoppedMarker(getContext()).delete();
// Prepare response for nested activity
response = new CannedFillResponse.Builder()
.addDataset(new CannedFillResponse.CannedDataset.Builder(
createPresentation("dataset2"))
.setField(ID_USERNAME, "filled").build())
.build();
sReplier.addResponse(response);
// Start nested login activity
Intent intent = new Intent(getContext(), LoginActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
getContext().startActivity(intent);
// Kill activity that is in the background
killOfProcessLoginActivityProcess();
// Trigger autofill on username in nested activity
mUiBot.selectByRelativeId(ID_USERNAME);
// Wait for fill request to be processed
sReplier.getNextFillRequest();
// Wait until dataset in nested activity is shown
mUiBot.assertDatasets("dataset2");
// Tap "Cancel".
mUiBot.selectByRelativeId(ID_CANCEL);
// Dataset should still be shown
mUiBot.assertDatasets("dataset1");
}
@Test
public void testDatasetGoesAwayWhenAutofilledAppIsKilled() throws Exception {
// Set service.
enableService();
// Start activity that is autofilled in a separate process so it can be killed
startAndWaitExternalActivity();
final CannedFillResponse response = new CannedFillResponse.Builder()
.addDataset(new CannedFillResponse.CannedDataset.Builder(
createPresentation("dataset"))
.setField(ID_USERNAME, "filled").build())
.build();
sReplier.addResponse(response);
// Trigger autofill on username
mUiBot.selectByRelativeId(ID_USERNAME);
// Wait for fill request to be processed
sReplier.getNextFillRequest();
// Wait until dataset is shown
mUiBot.assertDatasets("dataset");
// Kill activity
killOfProcessLoginActivityProcess();
// Make sure dataset is not shown anymore
mUiBot.assertNoDatasetsEver();
// Restart activity an make sure the dataset is still not shown
startAndWaitExternalActivity();
mUiBot.assertNoDatasets();
}
@Test
public void testSaveRemainsWhenAutofilledAppIsKilled() throws Exception {
// Set service.
enableService();
// Start activity that is autofilled in a separate process so it can be killed
startAndWaitExternalActivity();
final CannedFillResponse response = new CannedFillResponse.Builder()
.setRequiredSavableIds(SAVE_DATA_TYPE_USERNAME, ID_USERNAME)
.build();
sReplier.addResponse(response);
// Trigger autofill on username
mUiBot.selectByRelativeId(ID_USERNAME);
// Wait for fill request to be processed
sReplier.getNextFillRequest();
// Wait until dataset is shown
mUiBot.assertNoDatasetsEver();
// Trigger save
mUiBot.setTextByRelativeId(ID_USERNAME, "dude");
mUiBot.selectByRelativeId(ID_LOGIN);
mUiBot.assertSaveShowing(SAVE_DATA_TYPE_USERNAME);
// Kill activity
killOfProcessLoginActivityProcess();
// Make sure save is still showing
final UiObject2 saveSnackBar = mUiBot.assertSaveShowing(SAVE_DATA_TYPE_USERNAME);
mUiBot.saveForAutofill(saveSnackBar, true);
final InstrumentedAutoFillService.SaveRequest saveRequest = sReplier.getNextSaveRequest();
// Make sure data is correctly saved
final AssistStructure.ViewNode username = findNodeByResourceId(saveRequest.structure,
ID_USERNAME);
assertTextAndValue(username, "dude");
}
}