Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2015 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.widget;
     18 
     19 import static android.support.test.espresso.Espresso.onView;
     20 import static android.support.test.espresso.action.ViewActions.click;
     21 import static android.support.test.espresso.action.ViewActions.longClick;
     22 import static android.support.test.espresso.action.ViewActions.pressKey;
     23 import static android.support.test.espresso.action.ViewActions.replaceText;
     24 import static android.support.test.espresso.assertion.ViewAssertions.matches;
     25 import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
     26 import static android.support.test.espresso.matcher.ViewMatchers.withId;
     27 import static android.support.test.espresso.matcher.ViewMatchers.withText;
     28 import static android.widget.espresso.CustomViewActions.longPressAtRelativeCoordinates;
     29 import static android.widget.espresso.DragHandleUtils.assertNoSelectionHandles;
     30 import static android.widget.espresso.DragHandleUtils.onHandleView;
     31 import static android.widget.espresso.FloatingToolbarEspressoUtils.assertFloatingToolbarContainsItem;
     32 import static android.widget.espresso.FloatingToolbarEspressoUtils.assertFloatingToolbarDoesNotContainItem;
     33 import static android.widget.espresso.FloatingToolbarEspressoUtils.assertFloatingToolbarIsDisplayed;
     34 import static android.widget.espresso.FloatingToolbarEspressoUtils.assertFloatingToolbarIsNotDisplayed;
     35 import static android.widget.espresso.FloatingToolbarEspressoUtils.assertFloatingToolbarItemIndex;
     36 import static android.widget.espresso.FloatingToolbarEspressoUtils.clickFloatingToolbarItem;
     37 import static android.widget.espresso.FloatingToolbarEspressoUtils.sleepForFloatingToolbarPopup;
     38 import static android.widget.espresso.TextViewActions.Handle;
     39 import static android.widget.espresso.TextViewActions.clickOnTextAtIndex;
     40 import static android.widget.espresso.TextViewActions.doubleClickOnTextAtIndex;
     41 import static android.widget.espresso.TextViewActions.doubleTapAndDragOnText;
     42 import static android.widget.espresso.TextViewActions.dragHandle;
     43 import static android.widget.espresso.TextViewActions.longPressAndDragOnText;
     44 import static android.widget.espresso.TextViewActions.longPressOnTextAtIndex;
     45 import static android.widget.espresso.TextViewAssertions.doesNotHaveStyledText;
     46 import static android.widget.espresso.TextViewAssertions.hasInsertionPointerAtIndex;
     47 import static android.widget.espresso.TextViewAssertions.hasSelection;
     48 
     49 import static junit.framework.Assert.assertFalse;
     50 import static junit.framework.Assert.assertTrue;
     51 
     52 import static org.hamcrest.Matchers.anyOf;
     53 import static org.hamcrest.Matchers.is;
     54 
     55 import android.app.Activity;
     56 import android.app.Instrumentation;
     57 import android.content.ClipData;
     58 import android.content.ClipboardManager;
     59 import android.support.test.InstrumentationRegistry;
     60 import android.support.test.espresso.action.EspressoKey;
     61 import android.support.test.filters.MediumTest;
     62 import android.support.test.rule.ActivityTestRule;
     63 import android.support.test.runner.AndroidJUnit4;
     64 import android.text.InputType;
     65 import android.text.Selection;
     66 import android.text.Spannable;
     67 import android.view.ActionMode;
     68 import android.view.KeyEvent;
     69 import android.view.Menu;
     70 import android.view.MenuItem;
     71 import android.view.textclassifier.TextClassificationManager;
     72 import android.view.textclassifier.TextClassifier;
     73 import android.widget.espresso.CustomViewActions.RelativeCoordinatesProvider;
     74 
     75 import com.android.frameworks.coretests.R;
     76 
     77 import org.junit.Before;
     78 import org.junit.Rule;
     79 import org.junit.Test;
     80 import org.junit.runner.RunWith;
     81 
     82 /**
     83  * Tests the TextView widget from an Activity
     84  */
     85 @RunWith(AndroidJUnit4.class)
     86 @MediumTest
     87 public class TextViewActivityTest {
     88 
     89     @Rule
     90     public ActivityTestRule<TextViewActivity> mActivityRule = new ActivityTestRule<>(
     91             TextViewActivity.class);
     92 
     93     private Activity mActivity;
     94     private Instrumentation mInstrumentation;
     95 
     96     @Before
     97     public void setUp() {
     98         mActivity = mActivityRule.getActivity();
     99         mInstrumentation = InstrumentationRegistry.getInstrumentation();
    100         mActivity.getSystemService(TextClassificationManager.class)
    101                 .setTextClassifier(TextClassifier.NO_OP);
    102     }
    103 
    104     @Test
    105     public void testTypedTextIsOnScreen() {
    106         final String helloWorld = "Hello world!";
    107         // We use replaceText instead of typeTextIntoFocusedView to input text to avoid
    108         // unintentional interactions with software keyboard.
    109         onView(withId(R.id.textview)).perform(replaceText(helloWorld));
    110 
    111         onView(withId(R.id.textview)).check(matches(withText(helloWorld)));
    112     }
    113     @Test
    114     public void testPositionCursorAtTextAtIndex() {
    115         final String helloWorld = "Hello world!";
    116         onView(withId(R.id.textview)).perform(replaceText(helloWorld));
    117         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(helloWorld.indexOf("world")));
    118 
    119         // Delete text at specified index and see if we got the right one.
    120         onView(withId(R.id.textview)).perform(pressKey(KeyEvent.KEYCODE_FORWARD_DEL));
    121         onView(withId(R.id.textview)).check(matches(withText("Hello orld!")));
    122     }
    123 
    124     @Test
    125     public void testPositionCursorAtTextAtIndex_arabic() {
    126         // Arabic text. The expected cursorable boundary is
    127         // | \u0623 \u064F | \u067A | \u0633 \u0652 |
    128         final String text = "\u0623\u064F\u067A\u0633\u0652";
    129         onView(withId(R.id.textview)).perform(replaceText(text));
    130 
    131         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(0));
    132         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(0));
    133         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(1));
    134         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(anyOf(is(0), is(2))));
    135         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(2));
    136         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(2));
    137         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(3));
    138         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(3));
    139         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(4));
    140         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(anyOf(is(3), is(5))));
    141         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(5));
    142         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(5));
    143     }
    144 
    145     @Test
    146     public void testPositionCursorAtTextAtIndex_devanagari() {
    147         // Devanagari text. The expected cursorable boundary is | \u0915 \u093E |
    148         final String text = "\u0915\u093E";
    149         onView(withId(R.id.textview)).perform(replaceText(text));
    150 
    151         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(0));
    152         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(0));
    153         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(1));
    154         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(anyOf(is(0), is(2))));
    155         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(2));
    156         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(2));
    157     }
    158 
    159     @Test
    160     public void testLongPressToSelect() {
    161         final String helloWorld = "Hello Kirk!";
    162         onView(withId(R.id.textview)).perform(click());
    163         onView(withId(R.id.textview)).perform(replaceText(helloWorld));
    164         onView(withId(R.id.textview)).perform(
    165                 longPressOnTextAtIndex(helloWorld.indexOf("Kirk")));
    166 
    167         onView(withId(R.id.textview)).check(hasSelection("Kirk"));
    168     }
    169 
    170     @Test
    171     public void testLongPressEmptySpace() {
    172         final String helloWorld = "Hello big round sun!";
    173         onView(withId(R.id.textview)).perform(replaceText(helloWorld));
    174         // Move cursor somewhere else
    175         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(helloWorld.indexOf("big")));
    176         // Long-press at end of line.
    177         onView(withId(R.id.textview)).perform(longPressAtRelativeCoordinates(
    178                 RelativeCoordinatesProvider.HorizontalReference.RIGHT, -5,
    179                 RelativeCoordinatesProvider.VerticalReference.CENTER, 0));
    180 
    181         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(helloWorld.length()));
    182     }
    183 
    184     @Test
    185     public void testLongPressAndDragToSelect() {
    186         final String helloWorld = "Hello little handsome boy!";
    187         onView(withId(R.id.textview)).perform(replaceText(helloWorld));
    188         onView(withId(R.id.textview)).perform(
    189                 longPressAndDragOnText(helloWorld.indexOf("little"), helloWorld.indexOf(" boy!")));
    190 
    191         onView(withId(R.id.textview)).check(hasSelection("little handsome"));
    192     }
    193 
    194     @Test
    195     public void testLongPressAndDragToSelect_emoji() {
    196         final String text = "\uD83D\uDE00\uD83D\uDE01\uD83D\uDE02\uD83D\uDE03";
    197         onView(withId(R.id.textview)).perform(replaceText(text));
    198 
    199         onView(withId(R.id.textview)).perform(longPressAndDragOnText(4, 6));
    200         onView(withId(R.id.textview)).check(hasSelection("\uD83D\uDE02"));
    201 
    202         onView(withId(R.id.textview)).perform(click());
    203 
    204         onView(withId(R.id.textview)).perform(longPressAndDragOnText(4, 2));
    205         onView(withId(R.id.textview)).check(hasSelection("\uD83D\uDE01"));
    206     }
    207 
    208     @Test
    209     public void testDragAndDrop() {
    210         final String text = "abc def ghi.";
    211         onView(withId(R.id.textview)).perform(replaceText(text));
    212         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf("e")));
    213 
    214         onView(withId(R.id.textview)).perform(
    215                 longPressAndDragOnText(text.indexOf("e"), text.length()));
    216 
    217         onView(withId(R.id.textview)).check(matches(withText("abc ghi.def")));
    218         onView(withId(R.id.textview)).check(hasSelection(""));
    219         assertNoSelectionHandles();
    220         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex("abc ghi.def".length()));
    221 
    222         // Test undo returns to the original state.
    223         onView(withId(R.id.textview)).perform(pressKey(
    224                 (new EspressoKey.Builder()).withCtrlPressed(true).withKeyCode(KeyEvent.KEYCODE_Z)
    225                         .build()));
    226         onView(withId(R.id.textview)).check(matches(withText(text)));
    227     }
    228 
    229     @Test
    230     public void testDoubleTapToSelect() {
    231         final String helloWorld = "Hello SuetYi!";
    232         onView(withId(R.id.textview)).perform(replaceText(helloWorld));
    233 
    234         onView(withId(R.id.textview)).perform(
    235                 doubleClickOnTextAtIndex(helloWorld.indexOf("SuetYi")));
    236 
    237         onView(withId(R.id.textview)).check(hasSelection("SuetYi"));
    238     }
    239 
    240     @Test
    241     public void testDoubleTapAndDragToSelect() {
    242         final String helloWorld = "Hello young beautiful person!";
    243         onView(withId(R.id.textview)).perform(replaceText(helloWorld));
    244         onView(withId(R.id.textview)).perform(doubleTapAndDragOnText(helloWorld.indexOf("young"),
    245                         helloWorld.indexOf(" person!")));
    246 
    247         onView(withId(R.id.textview)).check(hasSelection("young beautiful"));
    248     }
    249 
    250     @Test
    251     public void testDoubleTapAndDragToSelect_multiLine() {
    252         final String helloWorld = "abcd\n" + "efg\n" + "hijklm\n" + "nop";
    253         onView(withId(R.id.textview)).perform(replaceText(helloWorld));
    254         onView(withId(R.id.textview)).perform(
    255                 doubleTapAndDragOnText(helloWorld.indexOf("m"), helloWorld.indexOf("a")));
    256         onView(withId(R.id.textview)).check(hasSelection("abcd\nefg\nhijklm"));
    257     }
    258 
    259     @Test
    260     public void testSelectBackwordsByTouch() {
    261         final String helloWorld = "Hello king of the Jungle!";
    262         onView(withId(R.id.textview)).perform(replaceText(helloWorld));
    263         onView(withId(R.id.textview)).perform(
    264                 doubleTapAndDragOnText(helloWorld.indexOf(" Jungle!"), helloWorld.indexOf("king")));
    265 
    266         onView(withId(R.id.textview)).check(hasSelection("king of the"));
    267     }
    268 
    269     @Test
    270     public void testToolbarAppearsAfterSelection() {
    271         final String text = "Toolbar appears after selection.";
    272         assertFloatingToolbarIsNotDisplayed();
    273         onView(withId(R.id.textview)).perform(replaceText(text));
    274         onView(withId(R.id.textview)).perform(
    275                 longPressOnTextAtIndex(text.indexOf("appears")));
    276 
    277         sleepForFloatingToolbarPopup();
    278         assertFloatingToolbarIsDisplayed();
    279 
    280         final String text2 = "Toolbar disappears after typing text.";
    281         onView(withId(R.id.textview)).perform(replaceText(text2));
    282         sleepForFloatingToolbarPopup();
    283         assertFloatingToolbarIsNotDisplayed();
    284     }
    285 
    286     @Test
    287     public void testToolbarAppearsAfterSelection_withFirstStringLtrAlgorithmAndRtlHint()
    288             throws Throwable {
    289         // after the hint layout change, the floating toolbar was not visible in the case below
    290         // this test tests that the floating toolbar is displayed on the screen and is visible to
    291         // user.
    292         mActivityRule.runOnUiThread(() -> {
    293             final TextView textView = mActivity.findViewById(R.id.textview);
    294             textView.setTextDirection(TextView.TEXT_DIRECTION_FIRST_STRONG_LTR);
    295             textView.setInputType(InputType.TYPE_CLASS_TEXT);
    296             textView.setSingleLine(true);
    297             textView.setHint("");
    298         });
    299         mInstrumentation.waitForIdleSync();
    300 
    301         onView(withId(R.id.textview)).perform(replaceText("test"));
    302         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(1));
    303         clickFloatingToolbarItem(mActivity.getString(com.android.internal.R.string.cut));
    304         onView(withId(R.id.textview)).perform(longClick());
    305         sleepForFloatingToolbarPopup();
    306 
    307         assertFloatingToolbarIsDisplayed();
    308     }
    309 
    310     @Test
    311     public void testToolbarAndInsertionHandle() {
    312         final String text = "text";
    313         onView(withId(R.id.textview)).perform(replaceText(text));
    314         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(text.length()));
    315         assertFloatingToolbarIsNotDisplayed();
    316 
    317         onHandleView(com.android.internal.R.id.insertion_handle).perform(click());
    318         sleepForFloatingToolbarPopup();
    319         assertFloatingToolbarIsDisplayed();
    320 
    321         assertFloatingToolbarContainsItem(
    322                 mActivity.getString(com.android.internal.R.string.selectAll));
    323         assertFloatingToolbarDoesNotContainItem(
    324                 mActivity.getString(com.android.internal.R.string.copy));
    325         assertFloatingToolbarDoesNotContainItem(
    326                 mActivity.getString(com.android.internal.R.string.cut));
    327     }
    328 
    329     @Test
    330     public void testToolbarAndSelectionHandle() {
    331         final String text = "abcd efg hijk";
    332         onView(withId(R.id.textview)).perform(replaceText(text));
    333 
    334         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf("f")));
    335         sleepForFloatingToolbarPopup();
    336         assertFloatingToolbarIsDisplayed();
    337 
    338         assertFloatingToolbarContainsItem(
    339                 mActivity.getString(com.android.internal.R.string.selectAll));
    340         assertFloatingToolbarContainsItem(
    341                 mActivity.getString(com.android.internal.R.string.copy));
    342         assertFloatingToolbarContainsItem(
    343                 mActivity.getString(com.android.internal.R.string.cut));
    344 
    345         final TextView textView = mActivity.findViewById(R.id.textview);
    346         onHandleView(com.android.internal.R.id.selection_start_handle)
    347                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('a')));
    348         sleepForFloatingToolbarPopup();
    349         assertFloatingToolbarIsDisplayed();
    350 
    351         onHandleView(com.android.internal.R.id.selection_end_handle)
    352                 .perform(dragHandle(textView, Handle.SELECTION_END, text.length()));
    353         sleepForFloatingToolbarPopup();
    354         assertFloatingToolbarIsDisplayed();
    355 
    356         assertFloatingToolbarDoesNotContainItem(
    357                 mActivity.getString(com.android.internal.R.string.selectAll));
    358         assertFloatingToolbarContainsItem(
    359                 mActivity.getString(com.android.internal.R.string.copy));
    360         assertFloatingToolbarContainsItem(
    361                 mActivity.getString(com.android.internal.R.string.cut));
    362     }
    363 
    364     @Test
    365     public void testInsertionHandle() {
    366         final String text = "abcd efg hijk ";
    367         onView(withId(R.id.textview)).perform(replaceText(text));
    368 
    369         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(text.length()));
    370         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.length()));
    371 
    372         final TextView textView = mActivity.findViewById(R.id.textview);
    373 
    374         onHandleView(com.android.internal.R.id.insertion_handle)
    375                 .perform(dragHandle(textView, Handle.INSERTION, text.indexOf('a')));
    376         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.indexOf("a")));
    377 
    378         onHandleView(com.android.internal.R.id.insertion_handle)
    379                 .perform(dragHandle(textView, Handle.INSERTION, text.indexOf('f')));
    380         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.indexOf("f")));
    381     }
    382 
    383     @Test
    384     public void testInsertionHandle_multiLine() {
    385         final String text = "abcd\n" + "efg\n" + "hijk\n";
    386         onView(withId(R.id.textview)).perform(replaceText(text));
    387 
    388         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(text.length()));
    389         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.length()));
    390 
    391         final TextView textView = mActivity.findViewById(R.id.textview);
    392 
    393         onHandleView(com.android.internal.R.id.insertion_handle)
    394                 .perform(dragHandle(textView, Handle.INSERTION, text.indexOf('a')));
    395         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.indexOf("a")));
    396 
    397         onHandleView(com.android.internal.R.id.insertion_handle)
    398                 .perform(dragHandle(textView, Handle.INSERTION, text.indexOf('f')));
    399         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.indexOf("f")));
    400     }
    401 
    402     @Test
    403     public void testSelectionHandles() {
    404         final String text = "abcd efg hijk lmn";
    405         onView(withId(R.id.textview)).perform(replaceText(text));
    406 
    407         assertNoSelectionHandles();
    408 
    409         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('f')));
    410 
    411         onHandleView(com.android.internal.R.id.selection_start_handle)
    412                 .check(matches(isDisplayed()));
    413         onHandleView(com.android.internal.R.id.selection_end_handle)
    414                 .check(matches(isDisplayed()));
    415 
    416         final TextView textView = mActivity.findViewById(R.id.textview);
    417         onHandleView(com.android.internal.R.id.selection_start_handle)
    418                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('a')));
    419         onView(withId(R.id.textview)).check(hasSelection("abcd efg"));
    420 
    421         onHandleView(com.android.internal.R.id.selection_end_handle)
    422                 .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('k') + 1));
    423         onView(withId(R.id.textview)).check(hasSelection("abcd efg hijk"));
    424     }
    425 
    426     @Test
    427     public void testSelectionHandles_bidi() {
    428         final String text = "abc \u0621\u0622\u0623 def";
    429         onView(withId(R.id.textview)).perform(replaceText(text));
    430 
    431         assertNoSelectionHandles();
    432 
    433         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('\u0622')));
    434 
    435         onHandleView(com.android.internal.R.id.selection_start_handle)
    436                 .check(matches(isDisplayed()));
    437         onHandleView(com.android.internal.R.id.selection_end_handle)
    438                 .check(matches(isDisplayed()));
    439 
    440         onView(withId(R.id.textview)).check(hasSelection("\u0621\u0622\u0623"));
    441 
    442         final TextView textView = mActivity.findViewById(R.id.textview);
    443         onHandleView(com.android.internal.R.id.selection_start_handle)
    444                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('f')));
    445         onView(withId(R.id.textview)).check(hasSelection("\u0621\u0622\u0623"));
    446 
    447         onHandleView(com.android.internal.R.id.selection_end_handle)
    448                 .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('a')));
    449         onView(withId(R.id.textview)).check(hasSelection("\u0621\u0622\u0623"));
    450 
    451         onHandleView(com.android.internal.R.id.selection_start_handle)
    452                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('\u0623'),
    453                         false));
    454         onView(withId(R.id.textview)).check(hasSelection("\u0623"));
    455 
    456         onHandleView(com.android.internal.R.id.selection_start_handle)
    457                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('\u0621'),
    458                         false));
    459         onView(withId(R.id.textview)).check(hasSelection("\u0621\u0622\u0623"));
    460 
    461         onHandleView(com.android.internal.R.id.selection_start_handle)
    462                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('a')));
    463         onView(withId(R.id.textview)).check(hasSelection("abc \u0621\u0622\u0623"));
    464 
    465         onHandleView(com.android.internal.R.id.selection_end_handle)
    466                 .perform(dragHandle(textView, Handle.SELECTION_END, text.length()));
    467         onView(withId(R.id.textview)).check(hasSelection("abc \u0621\u0622\u0623 def"));
    468     }
    469 
    470     @Test
    471     public void testSelectionHandles_multiLine() {
    472         final String text = "abcd\n" + "efg\n" + "hijk\n" + "lmn\n" + "opqr";
    473         onView(withId(R.id.textview)).perform(replaceText(text));
    474         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('i')));
    475 
    476         final TextView textView = mActivity.findViewById(R.id.textview);
    477         onHandleView(com.android.internal.R.id.selection_start_handle)
    478                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('e')));
    479         onView(withId(R.id.textview)).check(hasSelection("efg\nhijk"));
    480 
    481         onHandleView(com.android.internal.R.id.selection_start_handle)
    482                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('a')));
    483         onView(withId(R.id.textview)).check(hasSelection("abcd\nefg\nhijk"));
    484 
    485         onHandleView(com.android.internal.R.id.selection_end_handle)
    486                 .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('n') + 1));
    487         onView(withId(R.id.textview)).check(hasSelection("abcd\nefg\nhijk\nlmn"));
    488 
    489         onHandleView(com.android.internal.R.id.selection_end_handle)
    490                 .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('r') + 1));
    491         onView(withId(R.id.textview)).check(hasSelection("abcd\nefg\nhijk\nlmn\nopqr"));
    492     }
    493 
    494     @Test
    495     public void testSelectionHandles_multiLine_rtl() {
    496         // Arabic text.
    497         final String text = "\u062A\u062B\u062C\n" + "\u062D\u062E\u062F\n"
    498                 + "\u0630\u0631\u0632\n" + "\u0633\u0634\u0635\n" + "\u0636\u0637\u0638\n"
    499                 + "\u0639\u063A\u063B";
    500         onView(withId(R.id.textview)).perform(replaceText(text));
    501         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('\u0634')));
    502 
    503         final TextView textView = mActivity.findViewById(R.id.textview);
    504         onHandleView(com.android.internal.R.id.selection_start_handle)
    505                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('\u062E')));
    506         onView(withId(R.id.textview)).check(hasSelection(
    507                 text.substring(text.indexOf('\u062D'), text.indexOf('\u0635') + 1)));
    508 
    509         onHandleView(com.android.internal.R.id.selection_start_handle)
    510                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('\u062A')));
    511         onView(withId(R.id.textview)).check(hasSelection(
    512                 text.substring(text.indexOf('\u062A'), text.indexOf('\u0635') + 1)));
    513 
    514         onHandleView(com.android.internal.R.id.selection_end_handle)
    515                 .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('\u0638')));
    516         onView(withId(R.id.textview)).check(hasSelection(
    517                 text.substring(text.indexOf('\u062A'), text.indexOf('\u0638') + 1)));
    518 
    519         onHandleView(com.android.internal.R.id.selection_end_handle)
    520                 .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('\u063B')));
    521         onView(withId(R.id.textview)).check(hasSelection(text));
    522     }
    523 
    524     @Test
    525     public void testSelectionHandles_doesNotPassAnotherHandle() {
    526         final String text = "abcd efg hijk lmn";
    527         onView(withId(R.id.textview)).perform(replaceText(text));
    528         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('f')));
    529 
    530         final TextView textView = mActivity.findViewById(R.id.textview);
    531         onHandleView(com.android.internal.R.id.selection_start_handle)
    532                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('l')));
    533         onView(withId(R.id.textview)).check(hasSelection("g"));
    534 
    535         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('f')));
    536         onHandleView(com.android.internal.R.id.selection_end_handle)
    537                 .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('a')));
    538         onView(withId(R.id.textview)).check(hasSelection("e"));
    539     }
    540 
    541     @Test
    542     public void testSelectionHandles_doesNotPassAnotherHandle_multiLine() {
    543         final String text = "abcd\n" + "efg\n" + "hijk\n" + "lmn\n" + "opqr";
    544         onView(withId(R.id.textview)).perform(replaceText(text));
    545         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('i')));
    546 
    547         final TextView textView = mActivity.findViewById(R.id.textview);
    548         onHandleView(com.android.internal.R.id.selection_start_handle)
    549                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('r') + 1));
    550         onView(withId(R.id.textview)).check(hasSelection("k"));
    551 
    552         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('i')));
    553         onHandleView(com.android.internal.R.id.selection_end_handle)
    554                 .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('a')));
    555         onView(withId(R.id.textview)).check(hasSelection("h"));
    556     }
    557 
    558     @Test
    559     public void testSelectionHandles_snapToWordBoundary() {
    560         final String text = "abcd efg hijk lmn opqr";
    561         onView(withId(R.id.textview)).perform(replaceText(text));
    562         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('i')));
    563 
    564         final TextView textView = mActivity.findViewById(R.id.textview);
    565 
    566         onHandleView(com.android.internal.R.id.selection_start_handle)
    567                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('f')));
    568         onView(withId(R.id.textview)).check(hasSelection("efg hijk"));
    569 
    570         onHandleView(com.android.internal.R.id.selection_start_handle)
    571                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('d') + 1));
    572         onView(withId(R.id.textview)).check(hasSelection("efg hijk"));
    573 
    574 
    575         onHandleView(com.android.internal.R.id.selection_start_handle)
    576                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('c')));
    577         onView(withId(R.id.textview)).check(hasSelection("abcd efg hijk"));
    578 
    579         onHandleView(com.android.internal.R.id.selection_start_handle)
    580                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('d')));
    581         onView(withId(R.id.textview)).check(hasSelection("d efg hijk"));
    582 
    583         onHandleView(com.android.internal.R.id.selection_start_handle)
    584                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('b')));
    585         onView(withId(R.id.textview)).check(hasSelection("bcd efg hijk"));
    586 
    587         onView(withId(R.id.textview)).perform(click());
    588         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('i')));
    589 
    590         onHandleView(com.android.internal.R.id.selection_end_handle)
    591                 .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('n')));
    592         onView(withId(R.id.textview)).check(hasSelection("hijk lmn"));
    593 
    594         onHandleView(com.android.internal.R.id.selection_end_handle)
    595                 .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('o')));
    596         onView(withId(R.id.textview)).check(hasSelection("hijk lmn"));
    597 
    598         onHandleView(com.android.internal.R.id.selection_end_handle)
    599                 .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('q')));
    600         onView(withId(R.id.textview)).check(hasSelection("hijk lmn opqr"));
    601 
    602         onHandleView(com.android.internal.R.id.selection_end_handle)
    603                 .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('p')));
    604         onView(withId(R.id.textview)).check(hasSelection("hijk lmn o"));
    605 
    606         onHandleView(com.android.internal.R.id.selection_end_handle)
    607                 .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('r')));
    608         onView(withId(R.id.textview)).check(hasSelection("hijk lmn opq"));
    609     }
    610 
    611     @Test
    612     public void testSelectionHandles_snapToWordBoundary_multiLine() {
    613         final String text = "abcd efg\n" + "hijk lmn\n" + "opqr stu";
    614         onView(withId(R.id.textview)).perform(replaceText(text));
    615         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('m')));
    616 
    617         final TextView textView = mActivity.findViewById(R.id.textview);
    618 
    619         onHandleView(com.android.internal.R.id.selection_start_handle)
    620                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('c')));
    621         onView(withId(R.id.textview)).check(hasSelection("abcd efg\nhijk lmn"));
    622 
    623         onHandleView(com.android.internal.R.id.selection_start_handle)
    624                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('g')));
    625         onView(withId(R.id.textview)).check(hasSelection("g\nhijk lmn"));
    626 
    627         onHandleView(com.android.internal.R.id.selection_start_handle)
    628                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('m')));
    629         onView(withId(R.id.textview)).check(hasSelection("lmn"));
    630 
    631         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('i')));
    632 
    633         onHandleView(com.android.internal.R.id.selection_end_handle)
    634                 .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('u')));
    635         onView(withId(R.id.textview)).check(hasSelection("hijk lmn\nopqr stu"));
    636 
    637         onHandleView(com.android.internal.R.id.selection_end_handle)
    638                 .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('p')));
    639         onView(withId(R.id.textview)).check(hasSelection("hijk lmn\no"));
    640 
    641         onHandleView(com.android.internal.R.id.selection_end_handle)
    642                 .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('i')));
    643         onView(withId(R.id.textview)).check(hasSelection("hijk"));
    644     }
    645 
    646     @Test
    647     public void testSetSelectionAndActionMode() throws Throwable {
    648         final String text = "abc def";
    649         onView(withId(R.id.textview)).perform(replaceText(text));
    650 
    651         final TextView textView = mActivity.findViewById(R.id.textview);
    652         assertFloatingToolbarIsNotDisplayed();
    653         mActivityRule.runOnUiThread(
    654                 () -> Selection.setSelection((Spannable) textView.getText(), 0, 3));
    655         mInstrumentation.waitForIdleSync();
    656         sleepForFloatingToolbarPopup();
    657         // Don't automatically start action mode.
    658         assertFloatingToolbarIsNotDisplayed();
    659         // Make sure that "Select All" is included in the selection action mode when the entire text
    660         // is not selected.
    661         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('e')));
    662         sleepForFloatingToolbarPopup();
    663         assertFloatingToolbarIsDisplayed();
    664         // Changing the selection range by API should not interrupt the selection action mode.
    665         mActivityRule.runOnUiThread(
    666                 () -> Selection.setSelection((Spannable) textView.getText(), 0, 3));
    667         mInstrumentation.waitForIdleSync();
    668         sleepForFloatingToolbarPopup();
    669         assertFloatingToolbarIsDisplayed();
    670         assertFloatingToolbarContainsItem(
    671                 mActivity.getString(com.android.internal.R.string.selectAll));
    672         // Make sure that "Select All" is no longer included when the entire text is selected by
    673         // API.
    674         mActivityRule.runOnUiThread(
    675                 () -> Selection.setSelection((Spannable) textView.getText(), 0, text.length()));
    676         mInstrumentation.waitForIdleSync();
    677 
    678         sleepForFloatingToolbarPopup();
    679         assertFloatingToolbarIsDisplayed();
    680         assertFloatingToolbarDoesNotContainItem(
    681                 mActivity.getString(com.android.internal.R.string.selectAll));
    682         // Make sure that shrinking the selection range to cursor (an empty range) by API
    683         // terminates selection action mode and does not trigger the insertion action mode.
    684         mActivityRule.runOnUiThread(
    685                 () -> Selection.setSelection((Spannable) textView.getText(), 0));
    686         mInstrumentation.waitForIdleSync();
    687 
    688         sleepForFloatingToolbarPopup();
    689         assertFloatingToolbarIsNotDisplayed();
    690         // Make sure that user click can trigger the insertion action mode.
    691         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(text.length()));
    692         onHandleView(com.android.internal.R.id.insertion_handle).perform(click());
    693         sleepForFloatingToolbarPopup();
    694         assertFloatingToolbarIsDisplayed();
    695         // Make sure that an existing insertion action mode keeps alive after the insertion point is
    696         // moved by API.
    697         mActivityRule.runOnUiThread(
    698                 () -> Selection.setSelection((Spannable) textView.getText(), 0));
    699         mInstrumentation.waitForIdleSync();
    700 
    701         sleepForFloatingToolbarPopup();
    702         assertFloatingToolbarIsDisplayed();
    703         assertFloatingToolbarDoesNotContainItem(
    704                 mActivity.getString(com.android.internal.R.string.copy));
    705         // Make sure that selection action mode is started after selection is created by API when
    706         // insertion action mode is active.
    707         mActivityRule.runOnUiThread(
    708                 () -> Selection.setSelection((Spannable) textView.getText(), 1, text.length()));
    709         mInstrumentation.waitForIdleSync();
    710 
    711         sleepForFloatingToolbarPopup();
    712         assertFloatingToolbarIsDisplayed();
    713         assertFloatingToolbarContainsItem(
    714                 mActivity.getString(com.android.internal.R.string.copy));
    715     }
    716 
    717     @Test
    718     public void testTransientState() throws Throwable {
    719         final String text = "abc def";
    720         onView(withId(R.id.textview)).perform(replaceText(text));
    721 
    722         final TextView textView = mActivity.findViewById(R.id.textview);
    723         assertFalse(textView.hasTransientState());
    724 
    725         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('b')));
    726         // hasTransientState should return true when user generated selection is active.
    727         assertTrue(textView.hasTransientState());
    728         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(text.indexOf('d')));
    729         // hasTransientState should return false as the selection has been cleared.
    730         assertFalse(textView.hasTransientState());
    731         mActivityRule.runOnUiThread(
    732                 () -> Selection.setSelection((Spannable) textView.getText(), 0, text.length()));
    733         mInstrumentation.waitForIdleSync();
    734 
    735         // hasTransientState should return false when selection is created by API.
    736         assertFalse(textView.hasTransientState());
    737     }
    738 
    739     @Test
    740     public void testResetMenuItemTitle() throws Throwable {
    741         mActivity.getSystemService(TextClassificationManager.class).setTextClassifier(null);
    742         final TextView textView = mActivity.findViewById(R.id.textview);
    743         final int itemId = 1;
    744         final String title1 = " AFIGBO";
    745         final int index = title1.indexOf('I');
    746         final String title2 = title1.substring(index);
    747         final String[] title = new String[]{title1};
    748         mActivityRule.runOnUiThread(() -> textView.setCustomSelectionActionModeCallback(
    749                 new ActionMode.Callback() {
    750                     @Override
    751                     public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
    752                         return true;
    753                     }
    754 
    755                     @Override
    756                     public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
    757                         menu.removeItem(itemId);
    758                         menu.add(Menu.NONE /* group */, itemId, 0 /* order */, title[0]);
    759                         return true;
    760                     }
    761 
    762                     @Override
    763                     public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
    764                         return false;
    765                     }
    766 
    767                     @Override
    768                     public void onDestroyActionMode(ActionMode actionMode) {
    769                     }
    770                 }));
    771         mInstrumentation.waitForIdleSync();
    772 
    773         onView(withId(R.id.textview)).perform(replaceText(title1));
    774         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(index));
    775         sleepForFloatingToolbarPopup();
    776         assertFloatingToolbarContainsItem(title1);
    777 
    778         // Change the menu item title.
    779         title[0] = title2;
    780         // Change the selection to invalidate the action mode without restarting it.
    781         onHandleView(com.android.internal.R.id.selection_start_handle)
    782                 .perform(dragHandle(textView, Handle.SELECTION_START, index));
    783         sleepForFloatingToolbarPopup();
    784         assertFloatingToolbarContainsItem(title2);
    785     }
    786 
    787     @Test
    788     public void testAssistItemIsAtIndexZero() throws Throwable {
    789         useSystemDefaultTextClassifier();
    790         final TextView textView = mActivity.findViewById(R.id.textview);
    791         mActivityRule.runOnUiThread(() -> textView.setCustomSelectionActionModeCallback(
    792                 new ActionMode.Callback() {
    793                     @Override
    794                     public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
    795                         // Create another item at order position 0 to confirm that it will never be
    796                         // placed before the textAssist item.
    797                         menu.add(Menu.NONE, 0 /* id */, 0 /* order */, "Test");
    798                         return true;
    799                     }
    800 
    801                     @Override
    802                     public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
    803                         return true;
    804                     }
    805 
    806                     @Override
    807                     public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
    808                         return false;
    809                     }
    810 
    811                     @Override
    812                     public void onDestroyActionMode(ActionMode actionMode) {
    813                     }
    814                 }));
    815         mInstrumentation.waitForIdleSync();
    816         final String text = "droid (at) android.com";
    817 
    818         onView(withId(R.id.textview)).perform(replaceText(text));
    819         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('@')));
    820         sleepForFloatingToolbarPopup();
    821         assertFloatingToolbarItemIndex(android.R.id.textAssist, 0);
    822     }
    823 
    824     @Test
    825     public void testNoAssistItemForPasswordField() throws Throwable {
    826         useSystemDefaultTextClassifier();
    827         final TextView textView = mActivity.findViewById(R.id.textview);
    828         mActivityRule.runOnUiThread(() -> {
    829             textView.setInputType(
    830                     InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
    831         });
    832         mInstrumentation.waitForIdleSync();
    833         final String password = "afigbo (at) android.com";
    834 
    835         onView(withId(R.id.textview)).perform(replaceText(password));
    836         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(password.indexOf('@')));
    837         sleepForFloatingToolbarPopup();
    838         assertFloatingToolbarDoesNotContainItem(android.R.id.textAssist);
    839     }
    840 
    841     @Test
    842     public void testPastePlainText_menuAction() {
    843         initializeClipboardWithText(TextStyle.STYLED);
    844 
    845         onView(withId(R.id.textview)).perform(replaceText(""));
    846         onView(withId(R.id.textview)).perform(longClick());
    847         sleepForFloatingToolbarPopup();
    848         clickFloatingToolbarItem(
    849                 mActivity.getString(com.android.internal.R.string.paste_as_plain_text));
    850         mInstrumentation.waitForIdleSync();
    851 
    852         onView(withId(R.id.textview)).check(matches(withText("styledtext")));
    853         onView(withId(R.id.textview)).check(doesNotHaveStyledText());
    854     }
    855 
    856     @Test
    857     public void testPastePlainText_noMenuItemForPlainText() {
    858         initializeClipboardWithText(TextStyle.PLAIN);
    859 
    860         onView(withId(R.id.textview)).perform(replaceText(""));
    861         onView(withId(R.id.textview)).perform(longClick());
    862         sleepForFloatingToolbarPopup();
    863 
    864         assertFloatingToolbarDoesNotContainItem(
    865                 mActivity.getString(com.android.internal.R.string.paste_as_plain_text));
    866     }
    867 
    868     private void useSystemDefaultTextClassifier() {
    869         mActivity.getSystemService(TextClassificationManager.class).setTextClassifier(null);
    870     }
    871 
    872     private void initializeClipboardWithText(TextStyle textStyle) {
    873         final ClipData clip;
    874         switch (textStyle) {
    875             case STYLED:
    876                 clip = ClipData.newHtmlText("html", "styledtext", "<b>styledtext</b>");
    877                 break;
    878             case PLAIN:
    879                 clip = ClipData.newPlainText("plain", "plaintext");
    880                 break;
    881             default:
    882                 throw new IllegalArgumentException("Invalid text style");
    883         }
    884         mActivity.getWindow().getDecorView().post(() ->
    885                 mActivity.getSystemService(ClipboardManager.class).setPrimaryClip(clip));
    886         mInstrumentation.waitForIdleSync();
    887     }
    888 
    889     private enum TextStyle {
    890         PLAIN, STYLED
    891     }
    892 }
    893