1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.autofillservice.cts; 18 19 import static android.autofillservice.cts.activities.OutOfProcessLoginActivity.getDestroyedMarker; 20 import static android.autofillservice.cts.activities.OutOfProcessLoginActivity.getStartedMarker; 21 import static android.autofillservice.cts.activities.OutOfProcessLoginActivity.getStoppedMarker; 22 import static android.autofillservice.cts.testcore.Helper.ID_LOGIN; 23 import static android.autofillservice.cts.testcore.Helper.ID_PASSWORD; 24 import static android.autofillservice.cts.testcore.Helper.ID_USERNAME; 25 import static android.autofillservice.cts.testcore.Helper.assertTextAndValue; 26 import static android.autofillservice.cts.testcore.Helper.findNodeByResourceId; 27 import static android.autofillservice.cts.testcore.Helper.getContext; 28 import static android.autofillservice.cts.testcore.UiBot.LANDSCAPE; 29 import static android.autofillservice.cts.testcore.UiBot.PORTRAIT; 30 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD; 31 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_USERNAME; 32 33 import static com.android.compatibility.common.util.ShellUtils.runShellCommand; 34 35 import static com.google.common.truth.Truth.assertThat; 36 import static com.google.common.truth.Truth.assertWithMessage; 37 38 import static org.junit.Assume.assumeFalse; 39 import static org.junit.Assume.assumeTrue; 40 41 import android.app.ActivityManager; 42 import android.app.PendingIntent; 43 import android.app.assist.AssistStructure; 44 import android.autofillservice.cts.activities.EmptyActivity; 45 import android.autofillservice.cts.activities.LoginActivity; 46 import android.autofillservice.cts.activities.ManualAuthenticationActivity; 47 import android.autofillservice.cts.activities.OutOfProcessLoginActivity; 48 import android.autofillservice.cts.commontests.AutoFillServiceTestCase; 49 import android.autofillservice.cts.testcore.CannedFillResponse; 50 import android.autofillservice.cts.testcore.Helper; 51 import android.autofillservice.cts.testcore.InstrumentedAutoFillService; 52 import android.autofillservice.cts.testcore.Timeouts; 53 import android.autofillservice.cts.testcore.UiBot; 54 import android.content.Context; 55 import android.content.Intent; 56 import android.content.IntentSender; 57 import android.os.Bundle; 58 import android.os.SystemClock; 59 import android.platform.test.annotations.AppModeFull; 60 import android.util.Log; 61 import android.view.autofill.AutofillValue; 62 63 import androidx.test.uiautomator.UiObject2; 64 65 import com.android.compatibility.common.util.Timeout; 66 67 import org.junit.After; 68 import org.junit.Before; 69 import org.junit.Test; 70 71 import java.util.concurrent.Callable; 72 73 /** 74 * Test the lifecycle of a autofill session 75 */ 76 @AppModeFull(reason = "This test requires android.permission.WRITE_EXTERNAL_STORAGE") 77 public class SessionLifecycleTest extends AutoFillServiceTestCase.ManualActivityLaunch { 78 private static final String TAG = "SessionLifecycleTest"; 79 80 private static final String ID_BUTTON = "button"; 81 private static final String ID_CANCEL = "cancel"; 82 83 /** 84 * Delay for activity start/stop. 85 */ 86 // TODO: figure out a better way to wait without using sleep(). 87 private static final long WAIT_ACTIVITY_MS = 1000; 88 89 private static final Timeout SESSION_LIFECYCLE_TIMEOUT = new Timeout( 90 "SESSION_LIFECYCLE_TIMEOUT", 5000, 2F, 5000); 91 92 /** 93 * Runs an {@code assertion}, retrying until {@code timeout} is reached. 94 */ eventually(String description, Callable<Boolean> assertion)95 private static void eventually(String description, Callable<Boolean> assertion) 96 throws Exception { 97 SESSION_LIFECYCLE_TIMEOUT.run(description, assertion); 98 } 99 SessionLifecycleTest()100 public SessionLifecycleTest() { 101 super(new UiBot(SESSION_LIFECYCLE_TIMEOUT)); 102 } 103 104 /** 105 * Prevents the screen to rotate by itself 106 */ 107 @Before disableAutoRotation()108 public void disableAutoRotation() throws Exception { 109 Helper.disableAutoRotation(mUiBot); 110 } 111 112 /** 113 * Allows the screen to rotate by itself 114 */ 115 @After allowAutoRotation()116 public void allowAutoRotation() { 117 Helper.allowAutoRotation(); 118 } 119 120 @After finishLoginActivityOnAnotherProcess()121 public void finishLoginActivityOnAnotherProcess() throws Exception { 122 runShellCommand( 123 "am broadcast --receiver-foreground -n android.autofillservice.cts/.testcore" 124 + ".OutOfProcessLoginActivityFinisherReceiver"); 125 mUiBot.assertGoneByRelativeId(ID_USERNAME, Timeouts.ACTIVITY_RESURRECTION); 126 127 if (!OutOfProcessLoginActivity.hasInstance()) { 128 Log.v(TAG, "@After: Not waiting for oop activity to be destroyed"); 129 return; 130 } 131 // Waiting for activity to be destroyed (destroy marker appears) 132 eventually("getDestroyedMarker()", () -> { 133 return getDestroyedMarker(getContext()).exists(); 134 }); 135 } 136 killOfProcessLoginActivityProcess()137 private void killOfProcessLoginActivityProcess() throws Exception { 138 // Waiting for activity to stop (stop marker appears) 139 eventually("getStoppedMarker()", () -> { 140 return getStoppedMarker(getContext()).exists(); 141 }); 142 143 // onStop might not be finished, hence wait more 144 SystemClock.sleep(WAIT_ACTIVITY_MS); 145 146 // Kill activity that is in the background 147 runShellCommand("am broadcast --receiver-foreground " 148 + "-n android.autofillservice.cts/.testcore.SelfDestructReceiver"); 149 } 150 startAndWaitExternalActivity()151 private void startAndWaitExternalActivity() throws Exception { 152 final Intent outOfProcessAcvitityStartIntent = new Intent(getContext(), 153 OutOfProcessLoginActivity.class).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 154 getStartedMarker(getContext()).delete(); 155 getContext().startActivity(outOfProcessAcvitityStartIntent); 156 eventually("getStartedMarker()", () -> { 157 return getStartedMarker(getContext()).exists(); 158 }); 159 getStartedMarker(getContext()).delete(); 160 // Even if we wait the activity started, UiObject still fails. Have to wait a little bit. 161 SystemClock.sleep(WAIT_ACTIVITY_MS); 162 163 mUiBot.assertShownByRelativeId(ID_USERNAME); 164 } 165 166 @Test testDatasetAuthResponseWhileAutofilledAppIsLifecycled()167 public void testDatasetAuthResponseWhileAutofilledAppIsLifecycled() throws Exception { 168 assumeTrue("Rotation is supported", Helper.isRotationSupported(mContext)); 169 assumeTrue("Device state is not REAR_DISPLAY", 170 !Helper.isDeviceInState(mContext, Helper.DeviceStateEnum.REAR_DISPLAY)); 171 final ActivityManager activityManager = (ActivityManager) getContext() 172 .getSystemService(Context.ACTIVITY_SERVICE); 173 assumeFalse(activityManager.isLowRamDevice()); 174 175 // Set service. 176 enableService(); 177 178 try { 179 180 // Start activity that is autofilled in a separate process so it can be killed 181 startAndWaitExternalActivity(); 182 183 // Set expectations. 184 final Bundle extras = new Bundle(); 185 extras.putString("numbers", "4815162342"); 186 187 // Create the authentication intent (launching a full screen activity) 188 IntentSender authentication = PendingIntent.getActivity(getContext(), 0, 189 new Intent(getContext(), ManualAuthenticationActivity.class), 190 PendingIntent.FLAG_MUTABLE).getIntentSender(); 191 192 // Prepare the authenticated response 193 ManualAuthenticationActivity.setResponse(new CannedFillResponse.Builder() 194 .addDataset(new CannedFillResponse.CannedDataset.Builder() 195 .setField(ID_USERNAME, AutofillValue.forText("autofilled username")) 196 .setPresentation(createPresentation("dataset")).build()) 197 .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD) 198 .setExtras(extras).build()); 199 200 CannedFillResponse response = new CannedFillResponse.Builder() 201 .setAuthentication(authentication, ID_USERNAME, ID_PASSWORD) 202 .setPresentation(createPresentation("authenticate")) 203 .build(); 204 sReplier.addResponse(response); 205 206 // Trigger autofill on username 207 mUiBot.selectByRelativeId(ID_USERNAME); 208 209 // Wait for fill request to be processed 210 sReplier.getNextFillRequest(); 211 212 // Wait until authentication is shown 213 mUiBot.assertDatasets("authenticate"); 214 215 // Change orientation which triggers a destroy -> create in the app as the activity 216 // cannot deal with such situations 217 mUiBot.setScreenOrientation(LANDSCAPE); 218 mUiBot.setScreenOrientation(PORTRAIT); 219 220 // Wait context and Views being recreated in rotation 221 mUiBot.assertShownByRelativeId(ID_USERNAME); 222 223 // Delete stopped marker 224 getStoppedMarker(getContext()).delete(); 225 226 // Authenticate 227 mUiBot.selectDataset("authenticate"); 228 229 // Kill activity that is in the background 230 killOfProcessLoginActivityProcess(); 231 232 // Change orientation which triggers a destroy -> create in the app as the activity 233 // cannot deal with such situations 234 mUiBot.setScreenOrientation(PORTRAIT); 235 236 // Approve authentication 237 mUiBot.selectByRelativeId(ID_BUTTON); 238 239 // Wait for dataset to be shown 240 mUiBot.assertDatasets("dataset"); 241 242 // Change orientation which triggers a destroy -> create in the app as the activity 243 // cannot deal with such situations 244 mUiBot.setScreenOrientation(LANDSCAPE); 245 246 // Select dataset 247 mUiBot.selectDataset("dataset"); 248 249 // Check the results. 250 eventually("getTextById(" + ID_USERNAME + ")", () -> { 251 return mUiBot.getTextByRelativeId(ID_USERNAME).equals("autofilled username"); 252 }); 253 254 // Set password 255 mUiBot.setTextByRelativeId(ID_PASSWORD, "new password"); 256 257 // Login 258 mUiBot.selectByRelativeId(ID_LOGIN); 259 260 // Wait for save UI to be shown 261 mUiBot.assertSaveShowing(SAVE_DATA_TYPE_PASSWORD); 262 263 // Change orientation to make sure save UI can handle this 264 mUiBot.setScreenOrientation(PORTRAIT); 265 266 // Tap "Save". 267 mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD); 268 269 // Get save request 270 InstrumentedAutoFillService.SaveRequest saveRequest = sReplier.getNextSaveRequest(); 271 assertWithMessage("onSave() not called").that(saveRequest).isNotNull(); 272 273 // Make sure data is correctly saved 274 final AssistStructure.ViewNode username = findNodeByResourceId(saveRequest.structure, 275 ID_USERNAME); 276 assertTextAndValue(username, "autofilled username"); 277 final AssistStructure.ViewNode password = findNodeByResourceId(saveRequest.structure, 278 ID_PASSWORD); 279 assertTextAndValue(password, "new password"); 280 281 // Make sure extras were passed back on onSave() 282 assertThat(saveRequest.data).isNotNull(); 283 final String extraValue = saveRequest.data.getString("numbers"); 284 assertWithMessage("extras not passed on save").that(extraValue).isEqualTo("4815162342"); 285 } finally { 286 mUiBot.resetScreenResolution(); 287 } 288 } 289 290 @Test testAuthCanceledWhileAutofilledAppIsLifecycled()291 public void testAuthCanceledWhileAutofilledAppIsLifecycled() throws Exception { 292 // Set service. 293 enableService(); 294 295 // Start activity that is autofilled in a separate process so it can be killed 296 startAndWaitExternalActivity(); 297 298 // Create the authentication intent (launching a full screen activity) 299 IntentSender authentication = PendingIntent.getActivity(getContext(), 0, 300 new Intent(getContext(), ManualAuthenticationActivity.class), 301 PendingIntent.FLAG_IMMUTABLE).getIntentSender(); 302 303 CannedFillResponse response = new CannedFillResponse.Builder() 304 .setAuthentication(authentication, ID_USERNAME, ID_PASSWORD) 305 .setPresentation(createPresentation("authenticate")) 306 .build(); 307 sReplier.addResponse(response); 308 309 // Trigger autofill on username 310 mUiBot.selectByRelativeId(ID_USERNAME); 311 312 // Wait for fill request to be processed 313 sReplier.getNextFillRequest(); 314 315 // Wait until authentication is shown 316 mUiBot.assertDatasets("authenticate"); 317 318 // Delete stopped marker 319 getStoppedMarker(getContext()).delete(); 320 321 // Authenticate 322 mUiBot.selectDataset("authenticate"); 323 324 // Kill activity that is in the background 325 killOfProcessLoginActivityProcess(); 326 327 // Set expectations. 328 sReplier.addResponse( 329 new CannedFillResponse.Builder() 330 .setAuthentication(authentication, ID_USERNAME, ID_PASSWORD) 331 .setPresentation(createPresentation("authenticate2")) 332 .build()); 333 334 // Cancel authentication activity 335 mUiBot.pressBack(); 336 337 // Wait for fill request to be processed 338 mUiBot.waitForIdle(); 339 sReplier.getNextFillRequest(); 340 341 // Authentication should still be shown 342 mUiBot.assertDatasets("authenticate2"); 343 } 344 345 @Test testDatasetVisibleWhileAutofilledAppIsLifecycled()346 public void testDatasetVisibleWhileAutofilledAppIsLifecycled() throws Exception { 347 // Set service. 348 enableService(); 349 350 // Start activity that is autofilled in a separate process so it can be killed 351 startAndWaitExternalActivity(); 352 353 CannedFillResponse response = new CannedFillResponse.Builder() 354 .addDataset(new CannedFillResponse.CannedDataset.Builder( 355 createPresentation("dataset")) 356 .setField(ID_USERNAME, "filled").build()) 357 .build(); 358 sReplier.addResponse(response); 359 360 // Trigger autofill on username 361 mUiBot.selectByRelativeId(ID_USERNAME); 362 363 // Wait for fill request to be processed 364 sReplier.getNextFillRequest(); 365 366 // Wait until dataset is shown 367 mUiBot.assertDatasets("dataset"); 368 369 // Delete stopped marker 370 getStoppedMarker(getContext()).delete(); 371 372 // Start an activity on top of the autofilled activity 373 Intent intent = new Intent(getContext(), EmptyActivity.class); 374 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 375 376 getContext().startActivity(intent); 377 378 // Kill activity that is in the background 379 killOfProcessLoginActivityProcess(); 380 381 // Add response for back to the first activity 382 sReplier.addResponse( 383 new CannedFillResponse.Builder() 384 .addDataset(new CannedFillResponse.CannedDataset.Builder( 385 createPresentation("dataset2")) 386 .setField(ID_USERNAME, "filled").build()) 387 .build()); 388 389 // Cancel activity on top 390 mUiBot.pressBack(); 391 392 // Wait for fill request to be processed 393 mUiBot.waitForIdle(); 394 sReplier.getNextFillRequest(); 395 396 // Dataset should still be shown 397 mUiBot.assertDatasets("dataset2"); 398 } 399 400 @Test testAutofillNestedActivitiesWhileAutofilledAppIsLifecycled()401 public void testAutofillNestedActivitiesWhileAutofilledAppIsLifecycled() throws Exception { 402 // Set service. 403 enableService(); 404 405 // Start activity that is autofilled in a separate process so it can be killed 406 startAndWaitExternalActivity(); 407 408 // Prepare response for first activity 409 CannedFillResponse response = new CannedFillResponse.Builder() 410 .addDataset(new CannedFillResponse.CannedDataset.Builder( 411 createPresentation("dataset1")) 412 .setField(ID_USERNAME, "filled").build()) 413 .build(); 414 sReplier.addResponse(response); 415 416 // Trigger autofill on username 417 mUiBot.selectByRelativeId(ID_USERNAME); 418 419 // Wait for fill request to be processed 420 sReplier.getNextFillRequest(); 421 422 // Wait until dataset1 is shown 423 mUiBot.assertDatasets("dataset1"); 424 425 // Delete stopped marker 426 getStoppedMarker(getContext()).delete(); 427 428 // Prepare response for nested activity 429 response = new CannedFillResponse.Builder() 430 .addDataset(new CannedFillResponse.CannedDataset.Builder( 431 createPresentation("dataset2")) 432 .setField(ID_USERNAME, "filled").build()) 433 .build(); 434 sReplier.addResponse(response); 435 436 // Start nested login activity 437 Intent intent = new Intent(getContext(), LoginActivity.class); 438 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 439 getContext().startActivity(intent); 440 441 // Kill activity that is in the background 442 killOfProcessLoginActivityProcess(); 443 444 // Trigger autofill on username in nested activity 445 mUiBot.selectByRelativeId(ID_USERNAME); 446 447 // Wait for fill request to be processed 448 sReplier.getNextFillRequest(); 449 450 // Wait until dataset in nested activity is shown 451 mUiBot.assertDatasets("dataset2"); 452 453 // Set expectations for back to the first activity 454 sReplier.addResponse( 455 new CannedFillResponse.Builder() 456 .addDataset(new CannedFillResponse.CannedDataset.Builder( 457 createPresentation("dataset3")) 458 .setField(ID_USERNAME, "filled").build()) 459 .build()); 460 461 boolean isMockImeAvailable = sMockImeSessionRule.getMockImeSession() != null; 462 if (!isMockImeAvailable) { 463 // If Mock IME cannot be installed, 464 // it works fine for portrait but for the platforms that the default orientation 465 // is landscape, (e.g. automotive.) 466 // the ID_CANCEL button may not be visible depending on the height of the IME. 467 LoginActivity loginActivity = LoginActivity.getCurrentActivity(); 468 loginActivity.onCancel(v -> { 469 v.getParent().requestChildFocus(v, v); 470 }); 471 } 472 473 // Tap "Cancel". 474 mUiBot.selectByRelativeId(ID_CANCEL); 475 476 // Wait for fill request to be processed 477 mUiBot.waitForIdle(); 478 sReplier.getNextFillRequest(); 479 480 // Dataset should still be shown 481 mUiBot.assertDatasets("dataset3"); 482 } 483 484 @Test testDatasetGoesAwayWhenAutofilledAppIsKilled()485 public void testDatasetGoesAwayWhenAutofilledAppIsKilled() throws Exception { 486 // Set service. 487 enableService(); 488 489 // Start activity that is autofilled in a separate process so it can be killed 490 startAndWaitExternalActivity(); 491 492 final CannedFillResponse response = new CannedFillResponse.Builder() 493 .addDataset(new CannedFillResponse.CannedDataset.Builder( 494 createPresentation("dataset")) 495 .setField(ID_USERNAME, "filled").build()) 496 .build(); 497 sReplier.addResponse(response); 498 499 // Trigger autofill on username 500 mUiBot.selectByRelativeId(ID_USERNAME); 501 502 // Wait for fill request to be processed 503 sReplier.getNextFillRequest(); 504 505 // Wait until dataset is shown 506 mUiBot.assertDatasets("dataset"); 507 508 // Kill activity 509 killOfProcessLoginActivityProcess(); 510 511 // Make sure dataset is not shown anymore 512 mUiBot.assertNoDatasetsEver(); 513 514 // Restart activity an make sure the dataset is still not shown 515 startAndWaitExternalActivity(); 516 mUiBot.assertNoDatasets(); 517 } 518 519 @Test testSaveRemainsWhenAutofilledAppIsKilled()520 public void testSaveRemainsWhenAutofilledAppIsKilled() throws Exception { 521 // Set service. 522 enableService(); 523 524 // Start activity that is autofilled in a separate process so it can be killed 525 startAndWaitExternalActivity(); 526 527 final CannedFillResponse response = new CannedFillResponse.Builder() 528 .setRequiredSavableIds(SAVE_DATA_TYPE_USERNAME, ID_USERNAME) 529 .build(); 530 sReplier.addResponse(response); 531 532 // Trigger autofill on username 533 mUiBot.selectByRelativeId(ID_USERNAME); 534 535 // Wait for fill request to be processed 536 sReplier.getNextFillRequest(); 537 538 // Wait until dataset is shown 539 mUiBot.assertNoDatasetsEver(); 540 541 // Trigger save 542 mUiBot.setTextByRelativeId(ID_USERNAME, "dude"); 543 544 // It works fine for portrait but for the platforms that the default orientation 545 // is landscape, e.g. automotive. Depending on the height of the IME, the ID_LOGIN 546 // button may not be visible. 547 548 // In order to avoid that, scroll until the ID_LOGIN button appears. 549 mUiBot.scrollToTextObject(ID_LOGIN); 550 mUiBot.selectByRelativeId(ID_LOGIN); 551 mUiBot.assertSaveShowing(SAVE_DATA_TYPE_USERNAME); 552 553 // Kill activity 554 killOfProcessLoginActivityProcess(); 555 556 // Make sure save is still showing 557 final UiObject2 saveSnackBar = mUiBot.assertSaveShowing(SAVE_DATA_TYPE_USERNAME); 558 559 mUiBot.saveForAutofill(saveSnackBar, true); 560 561 final InstrumentedAutoFillService.SaveRequest saveRequest = sReplier.getNextSaveRequest(); 562 563 // Make sure data is correctly saved 564 final AssistStructure.ViewNode username = findNodeByResourceId(saveRequest.structure, 565 ID_USERNAME); 566 assertTextAndValue(username, "dude"); 567 } 568 } 569