Home | History | Annotate | Download | only in cts
      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