OSDN Git Service

RESTRICT AUTOMERGE Do not linkify text with RLO/LRO characters.
[android-x86/frameworks-base.git] / core / tests / coretests / src / android / widget / TextViewActivityTest.java
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@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 testNoAssistItemForTextFieldWithUnsupportedCharacters() throws Throwable {
843         useSystemDefaultTextClassifier();
844         final String text = "\u202Emoc.diordna.com";
845         final TextView textView = mActivity.findViewById(R.id.textview);
846         mActivityRule.runOnUiThread(() -> textView.setText(text));
847         mInstrumentation.waitForIdleSync();
848
849         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('.')));
850         sleepForFloatingToolbarPopup();
851         assertFloatingToolbarDoesNotContainItem(android.R.id.textAssist);
852     }
853
854     @Test
855     public void testPastePlainText_menuAction() {
856         initializeClipboardWithText(TextStyle.STYLED);
857
858         onView(withId(R.id.textview)).perform(replaceText(""));
859         onView(withId(R.id.textview)).perform(longClick());
860         sleepForFloatingToolbarPopup();
861         clickFloatingToolbarItem(
862                 mActivity.getString(com.android.internal.R.string.paste_as_plain_text));
863         mInstrumentation.waitForIdleSync();
864
865         onView(withId(R.id.textview)).check(matches(withText("styledtext")));
866         onView(withId(R.id.textview)).check(doesNotHaveStyledText());
867     }
868
869     @Test
870     public void testPastePlainText_noMenuItemForPlainText() {
871         initializeClipboardWithText(TextStyle.PLAIN);
872
873         onView(withId(R.id.textview)).perform(replaceText(""));
874         onView(withId(R.id.textview)).perform(longClick());
875         sleepForFloatingToolbarPopup();
876
877         assertFloatingToolbarDoesNotContainItem(
878                 mActivity.getString(com.android.internal.R.string.paste_as_plain_text));
879     }
880
881     private void useSystemDefaultTextClassifier() {
882         mActivity.getSystemService(TextClassificationManager.class).setTextClassifier(null);
883     }
884
885     private void initializeClipboardWithText(TextStyle textStyle) {
886         final ClipData clip;
887         switch (textStyle) {
888             case STYLED:
889                 clip = ClipData.newHtmlText("html", "styledtext", "<b>styledtext</b>");
890                 break;
891             case PLAIN:
892                 clip = ClipData.newPlainText("plain", "plaintext");
893                 break;
894             default:
895                 throw new IllegalArgumentException("Invalid text style");
896         }
897         mActivity.getWindow().getDecorView().post(() ->
898                 mActivity.getSystemService(ClipboardManager.class).setPrimaryClip(clip));
899         mInstrumentation.waitForIdleSync();
900     }
901
902     private enum TextStyle {
903         PLAIN, STYLED
904     }
905 }