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