TextViewActivityTest.java revision 4a056a5895e09b5e5e1950672077730313332b1b
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
17package android.widget;
18
19import static android.support.test.espresso.Espresso.onView;
20import static android.support.test.espresso.action.ViewActions.click;
21import static android.support.test.espresso.action.ViewActions.longClick;
22import static android.support.test.espresso.action.ViewActions.pressKey;
23import static android.support.test.espresso.action.ViewActions.replaceText;
24import static android.support.test.espresso.assertion.ViewAssertions.matches;
25import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
26import static android.support.test.espresso.matcher.ViewMatchers.withId;
27import static android.support.test.espresso.matcher.ViewMatchers.withText;
28import static android.widget.espresso.CustomViewActions.longPressAtRelativeCoordinates;
29import static android.widget.espresso.DragHandleUtils.assertNoSelectionHandles;
30import static android.widget.espresso.DragHandleUtils.onHandleView;
31import static android.widget.espresso.FloatingToolbarEspressoUtils.assertFloatingToolbarContainsItem;
32import static android.widget.espresso.FloatingToolbarEspressoUtils.assertFloatingToolbarDoesNotContainItem;
33import static android.widget.espresso.FloatingToolbarEspressoUtils.assertFloatingToolbarIsDisplayed;
34import static android.widget.espresso.FloatingToolbarEspressoUtils.assertFloatingToolbarIsNotDisplayed;
35import static android.widget.espresso.FloatingToolbarEspressoUtils.assertFloatingToolbarItemIndex;
36import static android.widget.espresso.FloatingToolbarEspressoUtils.clickFloatingToolbarItem;
37import static android.widget.espresso.FloatingToolbarEspressoUtils.sleepForFloatingToolbarPopup;
38import static android.widget.espresso.TextViewActions.Handle;
39import static android.widget.espresso.TextViewActions.clickOnTextAtIndex;
40import static android.widget.espresso.TextViewActions.doubleClickOnTextAtIndex;
41import static android.widget.espresso.TextViewActions.doubleTapAndDragOnText;
42import static android.widget.espresso.TextViewActions.dragHandle;
43import static android.widget.espresso.TextViewActions.longPressAndDragOnText;
44import static android.widget.espresso.TextViewActions.longPressOnTextAtIndex;
45import static android.widget.espresso.TextViewAssertions.doesNotHaveStyledText;
46import static android.widget.espresso.TextViewAssertions.hasInsertionPointerAtIndex;
47import static android.widget.espresso.TextViewAssertions.hasSelection;
48
49import static junit.framework.Assert.assertFalse;
50import static junit.framework.Assert.assertTrue;
51
52import static org.hamcrest.Matchers.anyOf;
53import static org.hamcrest.Matchers.is;
54
55import android.app.Activity;
56import android.app.Instrumentation;
57import android.content.ClipData;
58import android.content.ClipboardManager;
59import android.support.test.InstrumentationRegistry;
60import android.support.test.espresso.action.EspressoKey;
61import android.support.test.filters.MediumTest;
62import android.support.test.rule.ActivityTestRule;
63import android.support.test.runner.AndroidJUnit4;
64import android.text.InputType;
65import android.text.Selection;
66import android.text.Spannable;
67import android.view.ActionMode;
68import android.view.KeyEvent;
69import android.view.Menu;
70import android.view.MenuItem;
71import android.view.textclassifier.TextClassificationManager;
72import android.view.textclassifier.TextClassifier;
73import android.widget.espresso.CustomViewActions.RelativeCoordinatesProvider;
74
75import com.android.frameworks.coretests.R;
76
77import org.junit.Before;
78import org.junit.Rule;
79import org.junit.Test;
80import org.junit.runner.RunWith;
81
82/**
83 * Tests the TextView widget from an Activity
84 */
85@RunWith(AndroidJUnit4.class)
86@MediumTest
87public 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        assertFloatingToolbarIsNotDisplayed();
314        onView(withId(R.id.textview)).perform(replaceText(text));
315        onView(withId(R.id.textview)).perform(clickOnTextAtIndex(text.length()));
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@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@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