| /* |
| * 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.activities.OutOfProcessLoginActivity.getDestroyedMarker; |
| import static android.autofillservice.cts.activities.OutOfProcessLoginActivity.getStartedMarker; |
| import static android.autofillservice.cts.activities.OutOfProcessLoginActivity.getStoppedMarker; |
| import static android.autofillservice.cts.testcore.Helper.ID_LOGIN; |
| import static android.autofillservice.cts.testcore.Helper.ID_PASSWORD; |
| import static android.autofillservice.cts.testcore.Helper.ID_USERNAME; |
| import static android.autofillservice.cts.testcore.Helper.assertTextAndValue; |
| import static android.autofillservice.cts.testcore.Helper.findNodeByResourceId; |
| import static android.autofillservice.cts.testcore.Helper.getContext; |
| import static android.autofillservice.cts.testcore.UiBot.LANDSCAPE; |
| import static android.autofillservice.cts.testcore.UiBot.PORTRAIT; |
| import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD; |
| import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_USERNAME; |
| |
| import static com.android.compatibility.common.util.ShellUtils.runShellCommand; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| import static com.google.common.truth.Truth.assertWithMessage; |
| |
| import static org.junit.Assume.assumeFalse; |
| import static org.junit.Assume.assumeTrue; |
| |
| import android.app.ActivityManager; |
| import android.app.PendingIntent; |
| import android.app.assist.AssistStructure; |
| import android.autofillservice.cts.activities.EmptyActivity; |
| import android.autofillservice.cts.activities.LoginActivity; |
| import android.autofillservice.cts.activities.ManualAuthenticationActivity; |
| import android.autofillservice.cts.activities.OutOfProcessLoginActivity; |
| import android.autofillservice.cts.commontests.AutoFillServiceTestCase; |
| import android.autofillservice.cts.testcore.CannedFillResponse; |
| import android.autofillservice.cts.testcore.Helper; |
| import android.autofillservice.cts.testcore.InstrumentedAutoFillService; |
| import android.autofillservice.cts.testcore.Timeouts; |
| import android.autofillservice.cts.testcore.UiBot; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentSender; |
| import android.os.Bundle; |
| import android.os.SystemClock; |
| import android.platform.test.annotations.AppModeFull; |
| import android.util.Log; |
| import android.view.autofill.AutofillValue; |
| |
| import androidx.test.uiautomator.UiObject2; |
| |
| import com.android.compatibility.common.util.Timeout; |
| |
| 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(reason = "This test requires android.permission.WRITE_EXTERNAL_STORAGE") |
| public class SessionLifecycleTest extends AutoFillServiceTestCase.ManualActivityLaunch { |
| private static final String TAG = "SessionLifecycleTest"; |
| |
| 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", 5000, 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/.testcore" |
| + ".OutOfProcessLoginActivityFinisherReceiver"); |
| mUiBot.assertGoneByRelativeId(ID_USERNAME, Timeouts.ACTIVITY_RESURRECTION); |
| |
| if (!OutOfProcessLoginActivity.hasInstance()) { |
| Log.v(TAG, "@After: Not waiting for oop activity to be destroyed"); |
| return; |
| } |
| // Waiting for activity to be destroyed (destroy marker appears) |
| eventually("getDestroyedMarker()", () -> { |
| return getDestroyedMarker(getContext()).exists(); |
| }); |
| } |
| |
| 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/.testcore.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)); |
| assumeTrue("Device state is not REAR_DISPLAY", |
| !Helper.isDeviceInState(mContext, Helper.DeviceStateEnum.REAR_DISPLAY)); |
| final ActivityManager activityManager = (ActivityManager) getContext() |
| .getSystemService(Context.ACTIVITY_SERVICE); |
| assumeFalse(activityManager.isLowRamDevice()); |
| |
| // Set service. |
| enableService(); |
| |
| try { |
| |
| // 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), |
| PendingIntent.FLAG_MUTABLE).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"); |
| } finally { |
| mUiBot.resetScreenResolution(); |
| } |
| } |
| |
| @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), |
| PendingIntent.FLAG_IMMUTABLE).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(); |
| |
| // Set expectations. |
| sReplier.addResponse( |
| new CannedFillResponse.Builder() |
| .setAuthentication(authentication, ID_USERNAME, ID_PASSWORD) |
| .setPresentation(createPresentation("authenticate2")) |
| .build()); |
| |
| // Cancel authentication activity |
| mUiBot.pressBack(); |
| |
| // Wait for fill request to be processed |
| mUiBot.waitForIdle(); |
| sReplier.getNextFillRequest(); |
| |
| // Authentication should still be shown |
| mUiBot.assertDatasets("authenticate2"); |
| } |
| |
| @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(); |
| |
| // Add response for back to the first activity |
| sReplier.addResponse( |
| new CannedFillResponse.Builder() |
| .addDataset(new CannedFillResponse.CannedDataset.Builder( |
| createPresentation("dataset2")) |
| .setField(ID_USERNAME, "filled").build()) |
| .build()); |
| |
| // Cancel activity on top |
| mUiBot.pressBack(); |
| |
| // Wait for fill request to be processed |
| mUiBot.waitForIdle(); |
| sReplier.getNextFillRequest(); |
| |
| // Dataset should still be shown |
| mUiBot.assertDatasets("dataset2"); |
| } |
| |
| @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"); |
| |
| // Set expectations for back to the first activity |
| sReplier.addResponse( |
| new CannedFillResponse.Builder() |
| .addDataset(new CannedFillResponse.CannedDataset.Builder( |
| createPresentation("dataset3")) |
| .setField(ID_USERNAME, "filled").build()) |
| .build()); |
| |
| boolean isMockImeAvailable = sMockImeSessionRule.getMockImeSession() != null; |
| if (!isMockImeAvailable) { |
| // If Mock IME cannot be installed, |
| // it works fine for portrait but for the platforms that the default orientation |
| // is landscape, (e.g. automotive.) |
| // the ID_CANCEL button may not be visible depending on the height of the IME. |
| LoginActivity loginActivity = LoginActivity.getCurrentActivity(); |
| loginActivity.onCancel(v -> { |
| v.getParent().requestChildFocus(v, v); |
| }); |
| } |
| |
| // Tap "Cancel". |
| mUiBot.selectByRelativeId(ID_CANCEL); |
| |
| // Wait for fill request to be processed |
| mUiBot.waitForIdle(); |
| sReplier.getNextFillRequest(); |
| |
| // Dataset should still be shown |
| mUiBot.assertDatasets("dataset3"); |
| } |
| |
| @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"); |
| |
| // It works fine for portrait but for the platforms that the default orientation |
| // is landscape, e.g. automotive. Depending on the height of the IME, the ID_LOGIN |
| // button may not be visible. |
| |
| // In order to avoid that, scroll until the ID_LOGIN button appears. |
| mUiBot.scrollToTextObject(ID_LOGIN); |
| 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"); |
| } |
| } |