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.Helper.ID_PASSWORD; 20 import static android.autofillservice.cts.Helper.ID_USERNAME; 21 import static android.autofillservice.cts.Helper.getContext; 22 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC; 23 24 import static com.google.common.truth.Truth.assertThat; 25 import static com.google.common.truth.Truth.assertWithMessage; 26 27 import android.platform.test.annotations.AppModeFull; 28 import android.service.autofill.BatchUpdates; 29 import android.service.autofill.CharSequenceTransformation; 30 import android.service.autofill.CustomDescription; 31 import android.service.autofill.ImageTransformation; 32 import android.service.autofill.RegexValidator; 33 import android.service.autofill.Validator; 34 import android.support.test.uiautomator.By; 35 import android.support.test.uiautomator.UiObject2; 36 import android.view.View; 37 import android.view.autofill.AutofillId; 38 import android.widget.RemoteViews; 39 40 import androidx.annotation.NonNull; 41 import androidx.annotation.Nullable; 42 43 import org.junit.Before; 44 import org.junit.Rule; 45 import org.junit.Test; 46 47 import java.util.function.BiFunction; 48 import java.util.regex.Pattern; 49 50 @AppModeFull // Service-specific test 51 public class CustomDescriptionTest extends AutoFillServiceTestCase { 52 @Rule 53 public final AutofillActivityTestRule<LoginActivity> mActivityRule = 54 new AutofillActivityTestRule<>(LoginActivity.class); 55 56 private LoginActivity mActivity; 57 58 @Before 59 public void setActivity() { 60 mActivity = mActivityRule.getActivity(); 61 } 62 63 /** 64 * Base test 65 * 66 * @param descriptionBuilder method to build a custom description 67 * @param uiVerifier Ran when the custom description is shown 68 */ 69 private void testCustomDescription( 70 @NonNull BiFunction<AutofillId, AutofillId, CustomDescription> descriptionBuilder, 71 @Nullable Runnable uiVerifier) throws Exception { 72 enableService(); 73 74 final AutofillId usernameId = mActivity.getUsername().getAutofillId(); 75 final AutofillId passwordId = mActivity.getPassword().getAutofillId(); 76 77 // Set response with custom description 78 sReplier.addResponse(new CannedFillResponse.Builder() 79 .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_USERNAME, ID_PASSWORD) 80 .setCustomDescription(descriptionBuilder.apply(usernameId, passwordId)) 81 .build()); 82 83 // Trigger autofill with custom description 84 mActivity.onPassword(View::requestFocus); 85 86 // Wait for onFill() before proceeding. 87 sReplier.getNextFillRequest(); 88 89 // Trigger save. 90 mActivity.onUsername((v) -> v.setText("usernm")); 91 mActivity.onPassword((v) -> v.setText("passwd")); 92 mActivity.tapLogin(); 93 94 if (uiVerifier != null) { 95 uiVerifier.run(); 96 } 97 98 mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_GENERIC); 99 sReplier.getNextSaveRequest(); 100 } 101 102 @Test 103 public void validTransformation() throws Exception { 104 testCustomDescription((usernameId, passwordId) -> { 105 RemoteViews presentation = newTemplate(R.layout.two_horizontal_text_fields); 106 107 CharSequenceTransformation trans1 = new CharSequenceTransformation 108 .Builder(usernameId, Pattern.compile("(.*)"), "$1") 109 .addField(passwordId, Pattern.compile(".*(..)"), "..$1") 110 .build(); 111 @SuppressWarnings("deprecation") 112 ImageTransformation trans2 = new ImageTransformation 113 .Builder(usernameId, Pattern.compile(".*"), 114 R.drawable.android).build(); 115 116 return new CustomDescription.Builder(presentation) 117 .addChild(R.id.first, trans1) 118 .addChild(R.id.img, trans2) 119 .build(); 120 }, () -> assertSaveUiIsShownWithTwoLines("usernm..wd")); 121 } 122 123 @Test 124 public void validTransformationWithOneTemplateUpdate() throws Exception { 125 testCustomDescription((usernameId, passwordId) -> { 126 RemoteViews presentation = newTemplate(R.layout.two_horizontal_text_fields); 127 128 CharSequenceTransformation trans1 = new CharSequenceTransformation 129 .Builder(usernameId, Pattern.compile("(.*)"), "$1") 130 .addField(passwordId, Pattern.compile(".*(..)"), "..$1") 131 .build(); 132 @SuppressWarnings("deprecation") 133 ImageTransformation trans2 = new ImageTransformation 134 .Builder(usernameId, Pattern.compile(".*"), 135 R.drawable.android).build(); 136 RemoteViews update = newTemplate(0); // layout id not really used 137 update.setViewVisibility(R.id.second, View.GONE); 138 Validator condition = new RegexValidator(usernameId, Pattern.compile(".*")); 139 140 return new CustomDescription.Builder(presentation) 141 .addChild(R.id.first, trans1) 142 .addChild(R.id.img, trans2) 143 .batchUpdate(condition, 144 new BatchUpdates.Builder().updateTemplate(update).build()) 145 .build(); 146 }, () -> assertSaveUiIsShownWithJustOneLine("usernm..wd")); 147 } 148 149 @Test 150 public void validTransformationWithMultipleTemplateUpdates() throws Exception { 151 testCustomDescription((usernameId, passwordId) -> { 152 RemoteViews presentation = newTemplate(R.layout.two_horizontal_text_fields); 153 154 CharSequenceTransformation trans1 = new CharSequenceTransformation.Builder(usernameId, 155 Pattern.compile("(.*)"), "$1") 156 .addField(passwordId, Pattern.compile(".*(..)"), "..$1") 157 .build(); 158 @SuppressWarnings("deprecation") 159 ImageTransformation trans2 = new ImageTransformation.Builder(usernameId, 160 Pattern.compile(".*"), R.drawable.android) 161 .build(); 162 163 Validator validCondition = new RegexValidator(usernameId, Pattern.compile(".*")); 164 Validator invalidCondition = new RegexValidator(usernameId, Pattern.compile("D'OH")); 165 166 // Line 1 updates 167 RemoteViews update1 = newTemplate(666); // layout id not really used 168 update1.setContentDescription(R.id.first, "First am I"); // valid 169 RemoteViews update2 = newTemplate(0); // layout id not really used 170 update2.setViewVisibility(R.id.first, View.GONE); // invalid 171 172 // Line 2 updates 173 RemoteViews update3 = newTemplate(-666); // layout id not really used 174 update3.setTextViewText(R.id.second, "First of his second name"); // valid 175 RemoteViews update4 = newTemplate(0); // layout id not really used 176 update4.setTextViewText(R.id.second, "SECOND of his second name"); // invalid 177 178 return new CustomDescription.Builder(presentation) 179 .addChild(R.id.first, trans1) 180 .addChild(R.id.img, trans2) 181 .batchUpdate(validCondition, 182 new BatchUpdates.Builder().updateTemplate(update1).build()) 183 .batchUpdate(invalidCondition, 184 new BatchUpdates.Builder().updateTemplate(update2).build()) 185 .batchUpdate(validCondition, 186 new BatchUpdates.Builder().updateTemplate(update3).build()) 187 .batchUpdate(invalidCondition, 188 new BatchUpdates.Builder().updateTemplate(update4).build()) 189 .build(); 190 }, () -> assertSaveUiWithLinesIsShown( 191 (line1) -> assertWithMessage("Wrong content description for line1") 192 .that(line1.getContentDescription()).isEqualTo("First am I"), 193 (line2) -> assertWithMessage("Wrong text for line2").that(line2.getText()) 194 .isEqualTo("First of his second name"), 195 null)); 196 } 197 198 @Test 199 public void testMultipleBatchUpdates_noConditionPass() throws Exception { 200 multipleBatchUpdatesTest(BatchUpdatesConditionType.NONE_PASS); 201 } 202 203 @Test 204 public void testMultipleBatchUpdates_secondConditionPass() throws Exception { 205 multipleBatchUpdatesTest(BatchUpdatesConditionType.SECOND_PASS); 206 } 207 208 @Test 209 public void testMultipleBatchUpdates_thirdConditionPass() throws Exception { 210 multipleBatchUpdatesTest(BatchUpdatesConditionType.THIRD_PASS); 211 } 212 213 @Test 214 public void testMultipleBatchUpdates_allConditionsPass() throws Exception { 215 multipleBatchUpdatesTest(BatchUpdatesConditionType.ALL_PASS); 216 } 217 218 private enum BatchUpdatesConditionType { 219 NONE_PASS, 220 SECOND_PASS, 221 THIRD_PASS, 222 ALL_PASS 223 } 224 225 /** 226 * Tests a custom description that has 3 transformations, one applied directly and the other 227 * 2 in batch updates. 228 * 229 * @param conditionsType defines which batch updates conditions will pass. 230 */ 231 private void multipleBatchUpdatesTest(BatchUpdatesConditionType conditionsType) 232 throws Exception { 233 234 final boolean line2Pass = conditionsType == BatchUpdatesConditionType.SECOND_PASS 235 || conditionsType == BatchUpdatesConditionType.ALL_PASS; 236 final boolean line3Pass = conditionsType == BatchUpdatesConditionType.THIRD_PASS 237 || conditionsType == BatchUpdatesConditionType.ALL_PASS; 238 239 final Visitor<UiObject2> line1Visitor = (line1) -> assertWithMessage("Wrong text for line1") 240 .that(line1.getText()).isEqualTo("L1-u"); 241 242 final Visitor<UiObject2> line2Visitor; 243 if (line2Pass) { 244 line2Visitor = (line2) -> assertWithMessage("Wrong text for line2") 245 .that(line2.getText()).isEqualTo("L2-u"); 246 } else { 247 line2Visitor = null; 248 } 249 250 final Visitor<UiObject2> line3Visitor; 251 if (line3Pass) { 252 line3Visitor = (line3) -> assertWithMessage("Wrong text for line3") 253 .that(line3.getText()).isEqualTo("L3-p"); 254 } else { 255 line3Visitor = null; 256 } 257 258 testCustomDescription((usernameId, passwordId) -> { 259 Validator validCondition = new RegexValidator(usernameId, Pattern.compile(".*")); 260 Validator invalidCondition = new RegexValidator(usernameId, Pattern.compile("D'OH")); 261 Pattern firstCharGroupRegex = Pattern.compile("^(.).*$"); 262 263 final RemoteViews presentation = 264 newTemplate(R.layout.three_horizontal_text_fields_last_two_invisible); 265 266 final CharSequenceTransformation line1Transformation = 267 new CharSequenceTransformation.Builder(usernameId, firstCharGroupRegex, "L1-$1") 268 .build(); 269 270 final CharSequenceTransformation line2Transformation = 271 new CharSequenceTransformation.Builder(usernameId, firstCharGroupRegex, "L2-$1") 272 .build(); 273 final RemoteViews line2Updates = newTemplate(666); // layout id not really used 274 line2Updates.setViewVisibility(R.id.second, View.VISIBLE); 275 276 final CharSequenceTransformation line3Transformation = 277 new CharSequenceTransformation.Builder(passwordId, firstCharGroupRegex, "L3-$1") 278 .build(); 279 final RemoteViews line3Updates = newTemplate(666); // layout id not really used 280 line3Updates.setViewVisibility(R.id.third, View.VISIBLE); 281 282 return new CustomDescription.Builder(presentation) 283 .addChild(R.id.first, line1Transformation) 284 .batchUpdate(line2Pass ? validCondition : invalidCondition, 285 new BatchUpdates.Builder() 286 .transformChild(R.id.second, line2Transformation) 287 .updateTemplate(line2Updates) 288 .build()) 289 .batchUpdate(line3Pass ? validCondition : invalidCondition, 290 new BatchUpdates.Builder() 291 .transformChild(R.id.third, line3Transformation) 292 .updateTemplate(line3Updates) 293 .build()) 294 .build(); 295 }, () -> assertSaveUiWithLinesIsShown(line1Visitor, line2Visitor, line3Visitor)); 296 } 297 298 @Test 299 public void testBatchUpdatesApplyUpdateFirstThenTransformations() throws Exception { 300 301 final Visitor<UiObject2> line1Visitor = (line1) -> assertWithMessage("Wrong text for line1") 302 .that(line1.getText()).isEqualTo("L1-u"); 303 final Visitor<UiObject2> line2Visitor = (line2) -> assertWithMessage("Wrong text for line2") 304 .that(line2.getText()).isEqualTo("L2-u"); 305 final Visitor<UiObject2> line3Visitor = (line3) -> assertWithMessage("Wrong text for line3") 306 .that(line3.getText()).isEqualTo("L3-p"); 307 308 testCustomDescription((usernameId, passwordId) -> { 309 Validator validCondition = new RegexValidator(usernameId, Pattern.compile(".*")); 310 Pattern firstCharGroupRegex = Pattern.compile("^(.).*$"); 311 312 final RemoteViews presentation = 313 newTemplate(R.layout.two_horizontal_text_fields); 314 315 final CharSequenceTransformation line1Transformation = 316 new CharSequenceTransformation.Builder(usernameId, firstCharGroupRegex, "L1-$1") 317 .build(); 318 319 final CharSequenceTransformation line2Transformation = 320 new CharSequenceTransformation.Builder(usernameId, firstCharGroupRegex, "L2-$1") 321 .build(); 322 323 final CharSequenceTransformation line3Transformation = 324 new CharSequenceTransformation.Builder(passwordId, firstCharGroupRegex, "L3-$1") 325 .build(); 326 final RemoteViews line3Presentation = newTemplate(R.layout.third_line_only); 327 final RemoteViews line3Updates = newTemplate(666); // layout id not really used 328 line3Updates.addView(R.id.parent, line3Presentation); 329 330 return new CustomDescription.Builder(presentation) 331 .addChild(R.id.first, line1Transformation) 332 .batchUpdate(validCondition, 333 new BatchUpdates.Builder() 334 .transformChild(R.id.second, line2Transformation) 335 .build()) 336 .batchUpdate(validCondition, 337 new BatchUpdates.Builder() 338 .updateTemplate(line3Updates) 339 .transformChild(R.id.third, line3Transformation) 340 .build()) 341 .build(); 342 }, () -> assertSaveUiWithLinesIsShown(line1Visitor, line2Visitor, line3Visitor)); 343 } 344 345 @Test 346 public void badImageTransformation() throws Exception { 347 testCustomDescription((usernameId, passwordId) -> { 348 RemoteViews presentation = newTemplate(R.layout.two_horizontal_text_fields); 349 350 @SuppressWarnings("deprecation") 351 ImageTransformation trans = new ImageTransformation.Builder(usernameId, 352 Pattern.compile(".*"), 1).build(); 353 354 return new CustomDescription.Builder(presentation) 355 .addChild(R.id.img, trans) 356 .build(); 357 }, () -> assertSaveUiWithCustomDescriptionIsShown()); 358 } 359 360 @Test 361 public void unusedImageTransformation() throws Exception { 362 testCustomDescription((usernameId, passwordId) -> { 363 RemoteViews presentation = newTemplate(R.layout.two_horizontal_text_fields); 364 365 @SuppressWarnings("deprecation") 366 ImageTransformation trans = new ImageTransformation 367 .Builder(usernameId, Pattern.compile("invalid"), R.drawable.android) 368 .build(); 369 370 return new CustomDescription.Builder(presentation) 371 .addChild(R.id.img, trans) 372 .build(); 373 }, () -> assertSaveUiWithCustomDescriptionIsShown()); 374 } 375 376 @Test 377 public void applyImageTransformationToTextView() throws Exception { 378 testCustomDescription((usernameId, passwordId) -> { 379 RemoteViews presentation = newTemplate(R.layout.two_horizontal_text_fields); 380 381 @SuppressWarnings("deprecation") 382 ImageTransformation trans = new ImageTransformation 383 .Builder(usernameId, Pattern.compile(".*"), R.drawable.android) 384 .build(); 385 386 return new CustomDescription.Builder(presentation) 387 .addChild(R.id.first, trans) 388 .build(); 389 }, () -> assertSaveUiWithoutCustomDescriptionIsShown()); 390 } 391 392 @Test 393 public void failFirstFailAll() throws Exception { 394 testCustomDescription((usernameId, passwordId) -> { 395 RemoteViews presentation = newTemplate(R.layout.two_horizontal_text_fields); 396 397 CharSequenceTransformation trans = new CharSequenceTransformation 398 .Builder(usernameId, Pattern.compile("(.*)"), "$42") 399 .addField(passwordId, Pattern.compile(".*(..)"), "..$1") 400 .build(); 401 402 return new CustomDescription.Builder(presentation) 403 .addChild(R.id.first, trans) 404 .build(); 405 }, () -> assertSaveUiWithoutCustomDescriptionIsShown()); 406 } 407 408 @Test 409 public void failSecondFailAll() throws Exception { 410 testCustomDescription((usernameId, passwordId) -> { 411 RemoteViews presentation = newTemplate(R.layout.two_horizontal_text_fields); 412 413 CharSequenceTransformation trans = new CharSequenceTransformation 414 .Builder(usernameId, Pattern.compile("(.*)"), "$1") 415 .addField(passwordId, Pattern.compile(".*(..)"), "..$42") 416 .build(); 417 418 return new CustomDescription.Builder(presentation) 419 .addChild(R.id.first, trans) 420 .build(); 421 }, () -> assertSaveUiWithoutCustomDescriptionIsShown()); 422 } 423 424 @Test 425 public void applyCharSequenceTransformationToImageView() throws Exception { 426 testCustomDescription((usernameId, passwordId) -> { 427 RemoteViews presentation = newTemplate(R.layout.two_horizontal_text_fields); 428 429 CharSequenceTransformation trans = new CharSequenceTransformation 430 .Builder(usernameId, Pattern.compile("(.*)"), "$1") 431 .build(); 432 433 return new CustomDescription.Builder(presentation) 434 .addChild(R.id.img, trans) 435 .build(); 436 }, () -> assertSaveUiWithoutCustomDescriptionIsShown()); 437 } 438 439 private void multipleTransformationsForSameFieldTest(boolean matchFirst) throws Exception { 440 enableService(); 441 442 // Set response with custom description 443 final AutofillId usernameId = mActivity.getUsername().getAutofillId(); 444 final CharSequenceTransformation firstTrans = new CharSequenceTransformation 445 .Builder(usernameId, Pattern.compile("(marco)"), "polo") 446 .build(); 447 final CharSequenceTransformation secondTrans = new CharSequenceTransformation 448 .Builder(usernameId, Pattern.compile("(MARCO)"), "POLO") 449 .build(); 450 RemoteViews presentation = newTemplate(R.layout.two_horizontal_text_fields); 451 final CustomDescription customDescription = new CustomDescription.Builder(presentation) 452 .addChild(R.id.first, firstTrans) 453 .addChild(R.id.first, secondTrans) 454 .build(); 455 sReplier.addResponse(new CannedFillResponse.Builder() 456 .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_USERNAME) 457 .setCustomDescription(customDescription) 458 .build()); 459 460 // Trigger autofill with custom description 461 mActivity.onPassword(View::requestFocus); 462 463 // Wait for onFill() before proceeding. 464 sReplier.getNextFillRequest(); 465 466 // Trigger save. 467 final String username = matchFirst ? "marco" : "MARCO"; 468 mActivity.onUsername((v) -> v.setText(username)); 469 mActivity.onPassword((v) -> v.setText(LoginActivity.BACKDOOR_PASSWORD_SUBSTRING)); 470 mActivity.tapLogin(); 471 472 final String expectedText = matchFirst ? "polo" : "POLO"; 473 assertSaveUiIsShownWithTwoLines(expectedText); 474 } 475 476 @Test 477 public void applyMultipleTransformationsForSameField_matchFirst() throws Exception { 478 multipleTransformationsForSameFieldTest(true); 479 } 480 481 @Test 482 public void applyMultipleTransformationsForSameField_matchSecond() throws Exception { 483 multipleTransformationsForSameFieldTest(false); 484 } 485 486 private RemoteViews newTemplate(int resourceId) { 487 return new RemoteViews(getContext().getPackageName(), resourceId); 488 } 489 490 private UiObject2 assertSaveUiShowing() { 491 try { 492 return mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC); 493 } catch (Exception e) { 494 throw new RuntimeException(e); 495 } 496 } 497 498 private void assertSaveUiWithoutCustomDescriptionIsShown() { 499 // First make sure the UI is shown... 500 final UiObject2 saveUi = assertSaveUiShowing(); 501 502 // Then make sure it does not have the custom view on it. 503 assertWithMessage("found static_text on SaveUI (%s)", mUiBot.getChildrenAsText(saveUi)) 504 .that(saveUi.findObject(By.res(mPackageName, "static_text"))).isNull(); 505 } 506 507 private UiObject2 assertSaveUiWithCustomDescriptionIsShown() { 508 // First make sure the UI is shown... 509 final UiObject2 saveUi = assertSaveUiShowing(); 510 511 // Then make sure it does have the custom view on it... 512 final UiObject2 staticText = saveUi.findObject(By.res(mPackageName, "static_text")); 513 assertThat(staticText).isNotNull(); 514 assertThat(staticText.getText()).isEqualTo("YO:"); 515 516 return saveUi; 517 } 518 519 /** 520 * Asserts the save ui only has {@code first} and {@code second} lines (i.e, {@code third} is 521 * invisible), but only {@code first} has text. 522 */ 523 private UiObject2 assertSaveUiIsShownWithTwoLines(String expectedTextOnFirst) { 524 return assertSaveUiWithLinesIsShown( 525 (line1) -> assertWithMessage("Wrong text for child with id 'first'") 526 .that(line1.getText()).isEqualTo(expectedTextOnFirst), 527 (line2) -> assertWithMessage("Wrong text for child with id 'second'") 528 .that(line2.getText()).isNull(), 529 null); 530 } 531 532 /** 533 * Asserts the save ui only has {@code first} line (i.e., {@code second} and {@code third} are 534 * invisible). 535 */ 536 private void assertSaveUiIsShownWithJustOneLine(String expectedTextOnFirst) { 537 assertSaveUiWithLinesIsShown( 538 (line1) -> assertWithMessage("Wrong text for child with id 'first'") 539 .that(line1.getText()).isEqualTo(expectedTextOnFirst), 540 null, null); 541 } 542 543 private UiObject2 assertSaveUiWithLinesIsShown(@Nullable Visitor<UiObject2> line1Visitor, 544 @Nullable Visitor<UiObject2> line2Visitor, @Nullable Visitor<UiObject2> line3Visitor) { 545 final UiObject2 saveUi = assertSaveUiWithCustomDescriptionIsShown(); 546 mUiBot.assertChild(saveUi, "first", line1Visitor); 547 mUiBot.assertChild(saveUi, "second", line2Visitor); 548 mUiBot.assertChild(saveUi, "third", line3Visitor); 549 return saveUi; 550 } 551 } 552