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