• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.app.PendingIntent.FLAG_IMMUTABLE;
20 import static android.widget.espresso.CustomViewActions.longPressAtRelativeCoordinates;
21 import static android.widget.espresso.DragHandleUtils.assertNoSelectionHandles;
22 import static android.widget.espresso.DragHandleUtils.onHandleView;
23 import static android.widget.espresso.TextViewActions.Handle;
24 import static android.widget.espresso.TextViewActions.clickOnTextAtIndex;
25 import static android.widget.espresso.TextViewActions.doubleClickOnTextAtIndex;
26 import static android.widget.espresso.TextViewActions.doubleTapAndDragHandle;
27 import static android.widget.espresso.TextViewActions.doubleTapAndDragOnText;
28 import static android.widget.espresso.TextViewActions.doubleTapHandle;
29 import static android.widget.espresso.TextViewActions.dragHandle;
30 import static android.widget.espresso.TextViewActions.longPressAndDragHandle;
31 import static android.widget.espresso.TextViewActions.longPressAndDragOnText;
32 import static android.widget.espresso.TextViewActions.longPressHandle;
33 import static android.widget.espresso.TextViewActions.longPressOnTextAtIndex;
34 import static android.widget.espresso.TextViewAssertions.doesNotHaveStyledText;
35 import static android.widget.espresso.TextViewAssertions.hasInsertionPointerAtIndex;
36 import static android.widget.espresso.TextViewAssertions.hasSelection;
37 
38 import static androidx.test.espresso.Espresso.onView;
39 import static androidx.test.espresso.action.ViewActions.click;
40 import static androidx.test.espresso.action.ViewActions.longClick;
41 import static androidx.test.espresso.action.ViewActions.pressKey;
42 import static androidx.test.espresso.action.ViewActions.replaceText;
43 import static androidx.test.espresso.assertion.ViewAssertions.matches;
44 import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
45 import static androidx.test.espresso.matcher.ViewMatchers.withId;
46 import static androidx.test.espresso.matcher.ViewMatchers.withText;
47 
48 import static junit.framework.Assert.assertEquals;
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 import static org.mockito.Matchers.any;
55 import static org.mockito.Mockito.mock;
56 import static org.mockito.Mockito.never;
57 import static org.mockito.Mockito.verify;
58 import static org.mockito.Mockito.when;
59 
60 import android.app.Activity;
61 import android.app.Instrumentation;
62 import android.app.PendingIntent;
63 import android.app.RemoteAction;
64 import android.content.ClipData;
65 import android.content.ClipboardManager;
66 import android.content.Context;
67 import android.content.Intent;
68 import android.graphics.drawable.Icon;
69 import android.os.Bundle;
70 import android.support.test.uiautomator.By;
71 import android.support.test.uiautomator.UiDevice;
72 import android.support.test.uiautomator.Until;
73 import android.text.InputType;
74 import android.text.Selection;
75 import android.text.Spannable;
76 import android.text.SpannableString;
77 import android.text.method.LinkMovementMethod;
78 import android.view.ActionMode;
79 import android.view.KeyEvent;
80 import android.view.Menu;
81 import android.view.MenuItem;
82 import android.view.accessibility.AccessibilityNodeInfo;
83 import android.view.textclassifier.SelectionEvent;
84 import android.view.textclassifier.TextClassification;
85 import android.view.textclassifier.TextClassificationManager;
86 import android.view.textclassifier.TextClassifier;
87 import android.view.textclassifier.TextLinks;
88 import android.view.textclassifier.TextLinksParams;
89 import android.view.textclassifier.TextSelection;
90 import android.widget.espresso.CustomViewActions.RelativeCoordinatesProvider;
91 
92 import androidx.test.InstrumentationRegistry;
93 import androidx.test.espresso.action.EspressoKey;
94 import androidx.test.filters.MediumTest;
95 import androidx.test.filters.Suppress;
96 import androidx.test.rule.ActivityTestRule;
97 import androidx.test.runner.AndroidJUnit4;
98 
99 import com.android.frameworks.coretests.R;
100 
101 import org.junit.Before;
102 import org.junit.Rule;
103 import org.junit.Test;
104 import org.junit.runner.RunWith;
105 
106 import java.util.ArrayList;
107 import java.util.List;
108 import java.util.Objects;
109 
110 /**
111  * Tests the TextView widget from an Activity
112  */
113 @RunWith(AndroidJUnit4.class)
114 @MediumTest
115 public class TextViewActivityTest {
116 
117     @Rule
118     public ActivityTestRule<TextViewActivity> mActivityRule = new ActivityTestRule<>(
119             TextViewActivity.class);
120 
121     private Activity mActivity;
122     private Instrumentation mInstrumentation;
123     private UiDevice mDevice;
124     private FloatingToolbarUtils mToolbar;
125 
126     @Before
setUp()127     public void setUp() throws Exception {
128         mActivity = mActivityRule.getActivity();
129         mInstrumentation = InstrumentationRegistry.getInstrumentation();
130         mDevice = UiDevice.getInstance(mInstrumentation);
131         mDevice.wakeUp();
132         mToolbar = new FloatingToolbarUtils();
133         TextClassificationManager tcm = mActivity.getSystemService(
134                 TextClassificationManager.class);
135         tcm.setTextClassifier(TextClassifier.NO_OP);
136         tcm.setTextClassificationSessionFactory(null);
137     }
138 
139     @Test
testTypedTextIsOnScreen()140     public void testTypedTextIsOnScreen() {
141         final String helloWorld = "Hello world!";
142         // We use replaceText instead of typeTextIntoFocusedView to input text to avoid
143         // unintentional interactions with software keyboard.
144         setText(helloWorld);
145 
146         onView(withId(R.id.textview)).check(matches(withText(helloWorld)));
147     }
148     @Test
testPositionCursorAtTextAtIndex()149     public void testPositionCursorAtTextAtIndex() {
150         final String helloWorld = "Hello world!";
151         setText(helloWorld);
152         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(helloWorld.indexOf("world")));
153 
154         // Delete text at specified index and see if we got the right one.
155         onView(withId(R.id.textview)).perform(pressKey(KeyEvent.KEYCODE_FORWARD_DEL));
156         onView(withId(R.id.textview)).check(matches(withText("Hello orld!")));
157     }
158 
159     @Test
testPositionCursorAtTextAtIndex_arabic()160     public void testPositionCursorAtTextAtIndex_arabic() {
161         // Arabic text. The expected cursorable boundary is
162         // | \u0623 \u064F | \u067A | \u0633 \u0652 |
163         final String text = "\u0623\u064F\u067A\u0633\u0652";
164         setText(text);
165 
166         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(0));
167         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(0));
168         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(1));
169         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(anyOf(is(0), is(2))));
170         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(2));
171         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(2));
172         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(3));
173         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(3));
174         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(4));
175         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(anyOf(is(3), is(5))));
176         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(5));
177         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(5));
178     }
179 
180     @Test
testPositionCursorAtTextAtIndex_devanagari()181     public void testPositionCursorAtTextAtIndex_devanagari() {
182         // Devanagari text. The expected cursorable boundary is | \u0915 \u093E |
183         final String text = "\u0915\u093E";
184         setText(text);
185 
186         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(0));
187         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(0));
188         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(1));
189         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(anyOf(is(0), is(2))));
190         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(2));
191         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(2));
192     }
193 
194     @Test
testLongPressToSelect()195     public void testLongPressToSelect() {
196         final String helloWorld = "Hello Kirk!";
197         onView(withId(R.id.textview)).perform(click());
198         setText(helloWorld);
199         onView(withId(R.id.textview)).perform(
200                 longPressOnTextAtIndex(helloWorld.indexOf("Kirk")));
201 
202         onView(withId(R.id.textview)).check(hasSelection("Kirk"));
203     }
204 
205     @Test
testLongPressEmptySpace()206     public void testLongPressEmptySpace() {
207         final String helloWorld = "Hello big round sun!";
208         setText(helloWorld);
209         // Move cursor somewhere else
210         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(helloWorld.indexOf("big")));
211         // Long-press at end of line.
212         onView(withId(R.id.textview)).perform(longPressAtRelativeCoordinates(
213                 RelativeCoordinatesProvider.HorizontalReference.RIGHT, -5,
214                 RelativeCoordinatesProvider.VerticalReference.CENTER, 0));
215 
216         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(helloWorld.length()));
217     }
218 
219     @Test
testLongPressAndDragToSelect()220     public void testLongPressAndDragToSelect() {
221         final String helloWorld = "Hello little handsome boy!";
222         setText(helloWorld);
223         onView(withId(R.id.textview)).perform(
224                 longPressAndDragOnText(helloWorld.indexOf("little"), helloWorld.indexOf(" boy!")));
225 
226         onView(withId(R.id.textview)).check(hasSelection("little handsome"));
227     }
228 
229     @Test
testLongPressAndDragToSelect_emoji()230     public void testLongPressAndDragToSelect_emoji() {
231         final String text = "\uD83D\uDE00\uD83D\uDE01\uD83D\uDE02\uD83D\uDE03";
232         setText(text);
233 
234         onView(withId(R.id.textview)).perform(longPressAndDragOnText(4, 6));
235         onView(withId(R.id.textview)).check(hasSelection("\uD83D\uDE02"));
236 
237         onView(withId(R.id.textview)).perform(click());
238 
239         onView(withId(R.id.textview)).perform(longPressAndDragOnText(4, 2));
240         onView(withId(R.id.textview)).check(hasSelection("\uD83D\uDE01"));
241     }
242 
243     @Test
testDragAndDrop()244     public void testDragAndDrop() {
245         final String text = "abc def ghi.";
246         setText(text);
247         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf("e")));
248 
249         onView(withId(R.id.textview)).perform(
250                 longPressAndDragOnText(text.indexOf("e"), text.length()));
251 
252         onView(withId(R.id.textview)).check(matches(withText("abc ghi.def")));
253         onView(withId(R.id.textview)).check(hasSelection(""));
254         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex("abc ghi.def".length()));
255 
256         // Test undo returns to the original state.
257         onView(withId(R.id.textview)).perform(pressKey(
258                 (new EspressoKey.Builder()).withCtrlPressed(true).withKeyCode(KeyEvent.KEYCODE_Z)
259                         .build()));
260         onView(withId(R.id.textview)).check(matches(withText(text)));
261     }
262 
263     @Test
testDoubleTapToSelect()264     public void testDoubleTapToSelect() {
265         final String helloWorld = "Hello SuetYi!";
266         setText(helloWorld);
267 
268         onView(withId(R.id.textview)).perform(
269                 doubleClickOnTextAtIndex(helloWorld.indexOf("SuetYi")));
270 
271         onView(withId(R.id.textview)).check(hasSelection("SuetYi"));
272     }
273 
274     @Test
testDoubleTapAndDragToSelect()275     public void testDoubleTapAndDragToSelect() {
276         final String helloWorld = "Hello young beautiful person!";
277         setText(helloWorld);
278         onView(withId(R.id.textview)).perform(doubleTapAndDragOnText(helloWorld.indexOf("young"),
279                         helloWorld.indexOf(" person!")));
280 
281         onView(withId(R.id.textview)).check(hasSelection("young beautiful"));
282     }
283 
284     @Test
testDoubleTapAndDragToSelect_multiLine()285     public void testDoubleTapAndDragToSelect_multiLine() {
286         final String helloWorld = "abcd\n" + "efg\n" + "hijklm\n" + "nop";
287         setText(helloWorld);
288         onView(withId(R.id.textview)).perform(
289                 doubleTapAndDragOnText(helloWorld.indexOf("m"), helloWorld.indexOf("a")));
290         onView(withId(R.id.textview)).check(hasSelection("abcd\nefg\nhijklm"));
291     }
292 
293     @Test
testSelectBackwordsByTouch()294     public void testSelectBackwordsByTouch() {
295         final String helloWorld = "Hello king of the Jungle!";
296         setText(helloWorld);
297         onView(withId(R.id.textview)).perform(
298                 doubleTapAndDragOnText(helloWorld.indexOf(" Jungle!"), helloWorld.indexOf("king")));
299 
300         onView(withId(R.id.textview)).check(hasSelection("king of the"));
301     }
302 
303     @Test
testToolbarAppearsAfterSelection()304     public void testToolbarAppearsAfterSelection() {
305         final String text = "Toolbar appears after selection.";
306         setText(text);
307         onView(withId(R.id.textview)).perform(
308                 longPressOnTextAtIndex(text.indexOf("appears")));
309 
310         mToolbar.assertFloatingToolbarIsDisplayed();
311     }
312 
313     @Test
testToolbarAppearsAfterSelection_withFirstStringLtrAlgorithmAndRtlHint()314     public void testToolbarAppearsAfterSelection_withFirstStringLtrAlgorithmAndRtlHint()
315             throws Throwable {
316         // after the hint layout change, the floating toolbar was not visible in the case below
317         // this test tests that the floating toolbar is displayed on the screen and is visible to
318         // user.
319         mActivityRule.runOnUiThread(() -> {
320             final TextView textView = mActivity.findViewById(R.id.textview);
321             textView.setTextDirection(TextView.TEXT_DIRECTION_FIRST_STRONG_LTR);
322             textView.setInputType(InputType.TYPE_CLASS_TEXT);
323             textView.setSingleLine(true);
324             textView.setHint("الروبوت");
325         });
326         mInstrumentation.waitForIdleSync();
327 
328         setText("test");
329         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(1));
330         mToolbar.clickFloatingToolbarItem(mActivity.getString(com.android.internal.R.string.cut));
331         onView(withId(R.id.textview)).perform(longClick());
332 
333         mToolbar.assertFloatingToolbarIsDisplayed();
334     }
335 
336     @Test
testToolbarAppearsAfterLinkClicked()337     public void testToolbarAppearsAfterLinkClicked() throws Throwable {
338         TextLinks.TextLink textLink = addLinkifiedTextToTextView(R.id.textview);
339         int position = (textLink.getStart() + textLink.getEnd()) / 2;
340         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(position));
341         mToolbar.assertFloatingToolbarIsDisplayed();
342     }
343 
344     @Test
testToolbarAppearsAfterLinkClickedNonselectable()345     public void testToolbarAppearsAfterLinkClickedNonselectable() throws Throwable {
346         final TextView textView = mActivity.findViewById(R.id.nonselectable_textview);
347         final TextLinks.TextLink textLink = addLinkifiedTextToTextView(R.id.nonselectable_textview);
348         final int position = (textLink.getStart() + textLink.getEnd()) / 2;
349 
350         onView(withId(R.id.nonselectable_textview)).perform(clickOnTextAtIndex(position));
351         mToolbar.assertFloatingToolbarIsDisplayed();
352         assertTrue(textView.hasSelection());
353 
354         // toggle
355         onView(withId(R.id.nonselectable_textview)).perform(clickOnTextAtIndex(position));
356         mToolbar.waitForFloatingToolbarPopup();
357         assertFalse(textView.hasSelection());
358 
359         onView(withId(R.id.nonselectable_textview)).perform(clickOnTextAtIndex(position));
360         mToolbar.assertFloatingToolbarIsDisplayed();
361         assertTrue(textView.hasSelection());
362 
363         // click outside
364         onView(withId(R.id.nonselectable_textview)).perform(clickOnTextAtIndex(0));
365         assertFalse(textView.hasSelection());
366     }
367 
368     @Test
testToolbarAppearsAccessibilityLongClick()369     public void testToolbarAppearsAccessibilityLongClick() throws Throwable {
370         final String text = "Toolbar appears after performing accessibility's ACTION_LONG_CLICK.";
371         mActivityRule.runOnUiThread(() -> {
372             final TextView textView = mActivity.findViewById(R.id.textview);
373             final Bundle args = new Bundle();
374             textView.performAccessibilityAction(AccessibilityNodeInfo.ACTION_LONG_CLICK, args);
375         });
376         mInstrumentation.waitForIdleSync();
377 
378         mToolbar.assertFloatingToolbarIsDisplayed();
379     }
380 
381     @Test
testToolbarMenuItemClickAfterSelectionChange()382     public void testToolbarMenuItemClickAfterSelectionChange() throws Throwable {
383         final MenuItem[] latestItem = new MenuItem[1];
384         final MenuItem[] clickedItem = new MenuItem[1];
385         final String text = "abcd efg hijk";
386         mActivityRule.runOnUiThread(() -> {
387             final TextView textView = mActivity.findViewById(R.id.textview);
388             textView.setText(text);
389             textView.setCustomSelectionActionModeCallback(
390                     new ActionModeCallbackAdapter() {
391                         @Override
392                         public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
393                             menu.clear();
394                             latestItem[0] = menu.add("Item");
395                             return true;
396                         }
397 
398                         @Override
399                         public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
400                             clickedItem[0] = item;
401                             return true;
402                         }
403                     });
404         });
405         mInstrumentation.waitForIdleSync();
406 
407         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf("f")));
408 
409         // Change the selection so that the menu items are refreshed.
410         final TextView textView = mActivity.findViewById(R.id.textview);
411         onHandleView(com.android.internal.R.id.selection_start_handle)
412                 .perform(dragHandle(textView, Handle.SELECTION_START, 0));
413         mToolbar.assertFloatingToolbarIsDisplayed();
414 
415         mToolbar.clickFloatingToolbarItem("Item");
416         mInstrumentation.waitForIdleSync();
417 
418         assertEquals(latestItem[0], clickedItem[0]);
419     }
420 
421     @Test
testSelectionOnCreateActionModeReturnsFalse()422     public void testSelectionOnCreateActionModeReturnsFalse() throws Throwable {
423         final String text = "hello world";
424         mActivityRule.runOnUiThread(() -> {
425             final TextView textView = mActivity.findViewById(R.id.textview);
426             textView.setText(text);
427             textView.setCustomSelectionActionModeCallback(
428                     new ActionMode.Callback() {
429                         @Override
430                         public boolean onCreateActionMode(ActionMode mode, Menu menu) {
431                             return false;
432                         }
433 
434                         @Override
435                         public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
436                             return false;
437                         }
438 
439                         @Override
440                         public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
441                             return false;
442                         }
443 
444 
445                         @Override
446                         public void onDestroyActionMode(ActionMode mode) {
447                         }
448                     });
449         });
450         mInstrumentation.waitForIdleSync();
451         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf("d")));
452         mInstrumentation.waitForIdleSync();
453         assertNoSelectionHandles();
454     }
455 
456     @Test
testSelectionRemovedWhenNonselectableTextLosesFocus()457     public void testSelectionRemovedWhenNonselectableTextLosesFocus() throws Throwable {
458         final TextLinks.TextLink textLink = addLinkifiedTextToTextView(R.id.nonselectable_textview);
459         final int position = (textLink.getStart() + textLink.getEnd()) / 2;
460         final TextView textView = mActivity.findViewById(R.id.nonselectable_textview);
461         mActivityRule.runOnUiThread(() -> textView.setFocusableInTouchMode(true));
462 
463         onView(withId(R.id.nonselectable_textview)).perform(clickOnTextAtIndex(position));
464         mToolbar.assertFloatingToolbarIsDisplayed();
465         assertTrue(textView.hasSelection());
466 
467         mActivityRule.runOnUiThread(() -> textView.clearFocus());
468         mInstrumentation.waitForIdleSync();
469 
470         assertFalse(textView.hasSelection());
471     }
472 
473     @Test
testSelectionRemovedFromNonselectableTextWhenWindowLosesFocus()474     public void testSelectionRemovedFromNonselectableTextWhenWindowLosesFocus() throws Throwable {
475         TextLinks.TextLink textLink = addLinkifiedTextToTextView(R.id.nonselectable_textview);
476         int nonselectablePosition = (textLink.getStart() + textLink.getEnd()) / 2;
477         TextView nonselectableTextView = mActivity.findViewById(R.id.nonselectable_textview);
478 
479         onView(withId(R.id.nonselectable_textview))
480                 .perform(clickOnTextAtIndex(nonselectablePosition));
481         mToolbar.assertFloatingToolbarIsDisplayed();
482         assertTrue(nonselectableTextView.hasSelection());
483 
484         mDevice.openNotification();
485         Thread.sleep(2000);
486         mDevice.pressBack();
487         Thread.sleep(2000);
488 
489         assertFalse(nonselectableTextView.hasSelection());
490     }
491 
addLinkifiedTextToTextView(int id)492     private TextLinks.TextLink addLinkifiedTextToTextView(int id) throws Throwable {
493         TextView textView = mActivity.findViewById(id);
494         useSystemDefaultTextClassifier();
495         TextClassificationManager textClassificationManager =
496                 mActivity.getSystemService(TextClassificationManager.class);
497         TextClassifier textClassifier = textClassificationManager.getTextClassifier();
498         Spannable content = new SpannableString("Call me at +19148277737");
499         TextLinks.Request request = new TextLinks.Request.Builder(content).build();
500         TextLinks links = textClassifier.generateLinks(request);
501         TextLinksParams applyParams = new TextLinksParams.Builder()
502                 .setApplyStrategy(TextLinks.APPLY_STRATEGY_REPLACE)
503                 .build();
504         applyParams.apply(content, links);
505 
506         mActivityRule.runOnUiThread(() -> {
507             textView.setText(content);
508             textView.setMovementMethod(LinkMovementMethod.getInstance());
509         });
510         mInstrumentation.waitForIdleSync();
511 
512         // Wait for the UI thread to refresh
513         Thread.sleep(1000);
514 
515         return links.getLinks().iterator().next();
516     }
517 
518     @Test
testToolbarAndInsertionHandle()519     public void testToolbarAndInsertionHandle() throws Throwable {
520         final String text = "text";
521         setText(text);
522         Thread.sleep(500);
523         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(text.length()));
524 
525         onHandleView(com.android.internal.R.id.insertion_handle).perform(click());
526 
527         mToolbar.assertFloatingToolbarContainsItem(
528                 mActivity.getString(com.android.internal.R.string.selectAll));
529         mToolbar.assertFloatingToolbarDoesNotContainItem(
530                 mActivity.getString(com.android.internal.R.string.copy));
531         mToolbar.assertFloatingToolbarDoesNotContainItem(
532                 mActivity.getString(com.android.internal.R.string.cut));
533     }
534 
535     @Test
testToolbarAndSelectionHandle()536     public void testToolbarAndSelectionHandle() {
537         final String text = "abcd efg hijk";
538         setText(text);
539 
540         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf("f")));
541         mToolbar.assertFloatingToolbarIsDisplayed();
542 
543         mToolbar.assertFloatingToolbarContainsItem(
544                 mActivity.getString(com.android.internal.R.string.selectAll));
545         mToolbar.assertFloatingToolbarContainsItem(
546                 mActivity.getString(com.android.internal.R.string.copy));
547         mToolbar.assertFloatingToolbarContainsItem(
548                 mActivity.getString(com.android.internal.R.string.cut));
549 
550         final TextView textView = mActivity.findViewById(R.id.textview);
551         onHandleView(com.android.internal.R.id.selection_start_handle)
552                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('a')));
553         mToolbar.assertFloatingToolbarIsDisplayed();
554 
555         onHandleView(com.android.internal.R.id.selection_end_handle)
556                 .perform(dragHandle(textView, Handle.SELECTION_END, text.length()));
557         mToolbar.assertFloatingToolbarIsDisplayed();
558 
559         mToolbar.assertFloatingToolbarDoesNotContainItem(
560                 mActivity.getString(com.android.internal.R.string.selectAll));
561         mToolbar.assertFloatingToolbarContainsItem(
562                 mActivity.getString(com.android.internal.R.string.copy));
563         mToolbar.assertFloatingToolbarContainsItem(
564                 mActivity.getString(com.android.internal.R.string.cut));
565     }
566 
567     @Test
testInsertionHandle()568     public void testInsertionHandle() {
569         final String text = "abcd efg hijk ";
570         setText(text);
571 
572         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(text.length()));
573         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.length()));
574 
575         final TextView textView = mActivity.findViewById(R.id.textview);
576 
577         onHandleView(com.android.internal.R.id.insertion_handle)
578                 .perform(dragHandle(textView, Handle.INSERTION, text.indexOf('a')));
579         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.indexOf("a")));
580 
581         onHandleView(com.android.internal.R.id.insertion_handle)
582                 .perform(dragHandle(textView, Handle.INSERTION, text.indexOf('f')));
583         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.indexOf("f")));
584     }
585 
586     @Test
testInsertionHandle_multiLine()587     public void testInsertionHandle_multiLine() {
588         final String text = "abcd\n" + "efg\n" + "hijk\n" + "lmn\n";
589         setText(text);
590 
591         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(text.length()));
592         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.length()));
593 
594         final TextView textView = mActivity.findViewById(R.id.textview);
595 
596         onHandleView(com.android.internal.R.id.insertion_handle)
597                 .perform(dragHandle(textView, Handle.INSERTION, text.indexOf('f')));
598         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.indexOf("f")));
599 
600         onHandleView(com.android.internal.R.id.insertion_handle)
601                 .perform(dragHandle(textView, Handle.INSERTION, text.indexOf('i')));
602         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.indexOf("i")));
603     }
604 
enableFlagsForInsertionHandleGestures()605     private void enableFlagsForInsertionHandleGestures() {
606         final TextView textView = mActivity.findViewById(R.id.textview);
607         final Editor editor = textView.getEditorForTesting();
608         editor.setFlagCursorDragFromAnywhereEnabled(true);
609         editor.setFlagInsertionHandleGesturesEnabled(true);
610         // Note: We don't need to reset these flags explicitly at the end of each test, because a
611         // fresh TextView and Editor will be created for each test.
612     }
613 
614     @Test
testInsertionHandle_touchThrough()615     public void testInsertionHandle_touchThrough() {
616         enableFlagsForInsertionHandleGestures();
617         testInsertionHandle();
618         testInsertionHandle_multiLine();
619     }
620 
621     @Test
testInsertionHandle_longPressToSelect()622     public void testInsertionHandle_longPressToSelect() {
623         enableFlagsForInsertionHandleGestures();
624         final TextView textView = mActivity.findViewById(R.id.textview);
625 
626         final String text = "hello the world";
627         setText(text);
628 
629         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(text.length()));
630         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.length()));
631 
632         onHandleView(com.android.internal.R.id.insertion_handle).perform(longPressHandle(textView));
633         onView(withId(R.id.textview)).check(hasSelection("world"));
634     }
635 
636     @Test
testInsertionHandle_longPressAndDragToSelect()637     public void testInsertionHandle_longPressAndDragToSelect() {
638         enableFlagsForInsertionHandleGestures();
639         final TextView textView = mActivity.findViewById(R.id.textview);
640         final String text = "hello the world";
641         setText(text);
642 
643         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(text.length()));
644         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.length()));
645 
646         onHandleView(com.android.internal.R.id.insertion_handle)
647                 .perform(longPressAndDragHandle(textView, Handle.INSERTION, text.indexOf('t')));
648         onView(withId(R.id.textview)).check(hasSelection("the world"));
649     }
650 
651     @Test
testInsertionHandle_doubleTapToSelect()652     public void testInsertionHandle_doubleTapToSelect() {
653         enableFlagsForInsertionHandleGestures();
654         final TextView textView = mActivity.findViewById(R.id.textview);
655 
656         final String text = "hello the world";
657         setText(text);
658 
659         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(text.length()));
660         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.length()));
661 
662         onHandleView(com.android.internal.R.id.insertion_handle).perform(doubleTapHandle(textView));
663         onView(withId(R.id.textview)).check(hasSelection("world"));
664     }
665 
666     @Test
testInsertionHandle_doubleTapAndDragToSelect()667     public void testInsertionHandle_doubleTapAndDragToSelect() {
668         enableFlagsForInsertionHandleGestures();
669         final TextView textView = mActivity.findViewById(R.id.textview);
670 
671         final String text = "hello the world";
672         setText(text);
673 
674         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(text.length()));
675         onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.length()));
676 
677         onHandleView(com.android.internal.R.id.insertion_handle)
678                 .perform(doubleTapAndDragHandle(textView, Handle.INSERTION, text.indexOf('t')));
679         onView(withId(R.id.textview)).check(hasSelection("the world"));
680     }
681 
682     @Test
testSelectionHandles()683     public void testSelectionHandles() {
684         final String text = "abcd efg hijk lmn";
685         setText(text);
686 
687         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('f')));
688 
689         onHandleView(com.android.internal.R.id.selection_start_handle)
690                 .check(matches(isDisplayed()));
691         onHandleView(com.android.internal.R.id.selection_end_handle)
692                 .check(matches(isDisplayed()));
693 
694         final TextView textView = mActivity.findViewById(R.id.textview);
695         onHandleView(com.android.internal.R.id.selection_start_handle)
696                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('a')));
697         onView(withId(R.id.textview)).check(hasSelection("abcd efg"));
698 
699         onHandleView(com.android.internal.R.id.selection_end_handle)
700                 .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('k') + 1));
701         onView(withId(R.id.textview)).check(hasSelection("abcd efg hijk"));
702     }
703 
704     @Test
testSelectionHandles_bidi()705     public void testSelectionHandles_bidi() {
706         final String text = "abc \u0621\u0622\u0623 def";
707         setText(text);
708 
709         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('\u0622')));
710 
711         onHandleView(com.android.internal.R.id.selection_start_handle)
712                 .check(matches(isDisplayed()));
713         onHandleView(com.android.internal.R.id.selection_end_handle)
714                 .check(matches(isDisplayed()));
715 
716         onView(withId(R.id.textview)).check(hasSelection("\u0621\u0622\u0623"));
717 
718         final TextView textView = mActivity.findViewById(R.id.textview);
719         onHandleView(com.android.internal.R.id.selection_start_handle)
720                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('f')));
721         onView(withId(R.id.textview)).check(hasSelection("\u0621\u0622\u0623"));
722 
723         onHandleView(com.android.internal.R.id.selection_end_handle)
724                 .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('a')));
725         onView(withId(R.id.textview)).check(hasSelection("\u0621\u0622\u0623"));
726 
727         onHandleView(com.android.internal.R.id.selection_start_handle)
728                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('\u0623'),
729                         false));
730         onView(withId(R.id.textview)).check(hasSelection("\u0623"));
731 
732         onHandleView(com.android.internal.R.id.selection_start_handle)
733                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('\u0621'),
734                         false));
735         onView(withId(R.id.textview)).check(hasSelection("\u0621\u0622\u0623"));
736 
737         onHandleView(com.android.internal.R.id.selection_start_handle)
738                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('a')));
739         onView(withId(R.id.textview)).check(hasSelection("abc \u0621\u0622\u0623"));
740 
741         onHandleView(com.android.internal.R.id.selection_end_handle)
742                 .perform(dragHandle(textView, Handle.SELECTION_END, text.length()));
743         onView(withId(R.id.textview)).check(hasSelection("abc \u0621\u0622\u0623 def"));
744     }
745 
746     @Test
testSelectionHandles_multiLine()747     public void testSelectionHandles_multiLine() {
748         final String text = "abcd\n" + "efg\n" + "hijk\n" + "lmn\n" + "opqr";
749         setText(text);
750         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('i')));
751 
752         final TextView textView = mActivity.findViewById(R.id.textview);
753         onHandleView(com.android.internal.R.id.selection_start_handle)
754                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('e')));
755         onView(withId(R.id.textview)).check(hasSelection("efg\nhijk"));
756 
757         onHandleView(com.android.internal.R.id.selection_start_handle)
758                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('a')));
759         onView(withId(R.id.textview)).check(hasSelection("abcd\nefg\nhijk"));
760 
761         onHandleView(com.android.internal.R.id.selection_end_handle)
762                 .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('n') + 1));
763         onView(withId(R.id.textview)).check(hasSelection("abcd\nefg\nhijk\nlmn"));
764 
765         onHandleView(com.android.internal.R.id.selection_end_handle)
766                 .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('r') + 1));
767         onView(withId(R.id.textview)).check(hasSelection("abcd\nefg\nhijk\nlmn\nopqr"));
768     }
769 
770     @Suppress // Consistently failing.
771     @Test
testSelectionHandles_multiLine_rtl()772     public void testSelectionHandles_multiLine_rtl() {
773         // Arabic text.
774         final String text = "\u062A\u062B\u062C\n" + "\u062D\u062E\u062F\n"
775                 + "\u0630\u0631\u0632\n" + "\u0633\u0634\u0635\n" + "\u0636\u0637\u0638\n"
776                 + "\u0639\u063A\u063B";
777         setText(text);
778         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('\u0634')));
779 
780         final TextView textView = mActivity.findViewById(R.id.textview);
781         onHandleView(com.android.internal.R.id.selection_start_handle)
782                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('\u062E')));
783         onView(withId(R.id.textview)).check(hasSelection(
784                 text.substring(text.indexOf('\u062D'), text.indexOf('\u0635') + 1)));
785 
786         onHandleView(com.android.internal.R.id.selection_start_handle)
787                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('\u062A')));
788         onView(withId(R.id.textview)).check(hasSelection(
789                 text.substring(text.indexOf('\u062A'), text.indexOf('\u0635') + 1)));
790 
791         onHandleView(com.android.internal.R.id.selection_end_handle)
792                 .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('\u0638')));
793         onView(withId(R.id.textview)).check(hasSelection(
794                 text.substring(text.indexOf('\u062A'), text.indexOf('\u0638') + 1)));
795 
796         onHandleView(com.android.internal.R.id.selection_end_handle)
797                 .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('\u063B')));
798         onView(withId(R.id.textview)).check(hasSelection(text));
799     }
800 
801     @Test
testSelectionHandles_doesNotPassAnotherHandle()802     public void testSelectionHandles_doesNotPassAnotherHandle() {
803         final String text = "abcd efg hijk lmn";
804         setText(text);
805         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('f')));
806 
807         final TextView textView = mActivity.findViewById(R.id.textview);
808         onHandleView(com.android.internal.R.id.selection_start_handle)
809                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('l')));
810         onView(withId(R.id.textview)).check(hasSelection("g"));
811 
812         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('f')));
813         onHandleView(com.android.internal.R.id.selection_end_handle)
814                 .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('a')));
815         onView(withId(R.id.textview)).check(hasSelection("e"));
816     }
817 
818     @Test
testSelectionHandles_doesNotPassAnotherHandle_multiLine()819     public void testSelectionHandles_doesNotPassAnotherHandle_multiLine() {
820         final String text = "abcd\n" + "efg\n" + "hijk\n" + "lmn\n" + "opqr";
821         setText(text);
822         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('i')));
823 
824         final TextView textView = mActivity.findViewById(R.id.textview);
825         onHandleView(com.android.internal.R.id.selection_start_handle)
826                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('r') + 1));
827         onView(withId(R.id.textview)).check(hasSelection("k"));
828 
829         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('i')));
830         onHandleView(com.android.internal.R.id.selection_end_handle)
831                 .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('a')));
832         onView(withId(R.id.textview)).check(hasSelection("h"));
833     }
834 
835     @Test
testSelectionHandles_snapToWordBoundary()836     public void testSelectionHandles_snapToWordBoundary() {
837         final String text = "abcd efg hijk lmn opqr";
838         setText(text);
839         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('i')));
840 
841         final TextView textView = mActivity.findViewById(R.id.textview);
842 
843         onHandleView(com.android.internal.R.id.selection_start_handle)
844                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('f')));
845         onView(withId(R.id.textview)).check(hasSelection("efg hijk"));
846 
847         onHandleView(com.android.internal.R.id.selection_start_handle)
848                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('d') + 1));
849         onView(withId(R.id.textview)).check(hasSelection("efg hijk"));
850 
851 
852         onHandleView(com.android.internal.R.id.selection_start_handle)
853                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('c')));
854         onView(withId(R.id.textview)).check(hasSelection("abcd efg hijk"));
855 
856         onHandleView(com.android.internal.R.id.selection_start_handle)
857                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('d')));
858         onView(withId(R.id.textview)).check(hasSelection("d efg hijk"));
859 
860         onHandleView(com.android.internal.R.id.selection_start_handle)
861                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('b')));
862         onView(withId(R.id.textview)).check(hasSelection("bcd efg hijk"));
863 
864         onView(withId(R.id.textview)).perform(click());
865         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('i')));
866 
867         onHandleView(com.android.internal.R.id.selection_end_handle)
868                 .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('n')));
869         onView(withId(R.id.textview)).check(hasSelection("hijk lmn"));
870 
871         onHandleView(com.android.internal.R.id.selection_end_handle)
872                 .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('o')));
873         onView(withId(R.id.textview)).check(hasSelection("hijk lmn"));
874 
875         onHandleView(com.android.internal.R.id.selection_end_handle)
876                 .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('q')));
877         onView(withId(R.id.textview)).check(hasSelection("hijk lmn opqr"));
878 
879         onHandleView(com.android.internal.R.id.selection_end_handle)
880                 .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('p')));
881         onView(withId(R.id.textview)).check(hasSelection("hijk lmn o"));
882 
883         onHandleView(com.android.internal.R.id.selection_end_handle)
884                 .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('r')));
885         onView(withId(R.id.textview)).check(hasSelection("hijk lmn opq"));
886     }
887 
888     @Test
testSelectionHandles_snapToWordBoundary_multiLine()889     public void testSelectionHandles_snapToWordBoundary_multiLine() {
890         final String text = "abcd efg\n" + "hijk lmn\n" + "opqr stu";
891         setText(text);
892         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('m')));
893 
894         final TextView textView = mActivity.findViewById(R.id.textview);
895 
896         onHandleView(com.android.internal.R.id.selection_start_handle)
897                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('c')));
898         onView(withId(R.id.textview)).check(hasSelection("abcd efg\nhijk lmn"));
899 
900         onHandleView(com.android.internal.R.id.selection_start_handle)
901                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('g')));
902         onView(withId(R.id.textview)).check(hasSelection("g\nhijk lmn"));
903 
904         onHandleView(com.android.internal.R.id.selection_start_handle)
905                 .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('m')));
906         onView(withId(R.id.textview)).check(hasSelection("lmn"));
907 
908         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('i')));
909 
910         onHandleView(com.android.internal.R.id.selection_end_handle)
911                 .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('u')));
912         onView(withId(R.id.textview)).check(hasSelection("hijk lmn\nopqr stu"));
913 
914         onHandleView(com.android.internal.R.id.selection_end_handle)
915                 .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('p')));
916         onView(withId(R.id.textview)).check(hasSelection("hijk lmn\no"));
917 
918         onHandleView(com.android.internal.R.id.selection_end_handle)
919                 .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('i')));
920         onView(withId(R.id.textview)).check(hasSelection("hijk"));
921     }
922 
923     @Test
testSelectionHandles_visibleEvenWithEmptyMenu()924     public void testSelectionHandles_visibleEvenWithEmptyMenu() {
925         ((TextView) mActivity.findViewById(R.id.textview)).setCustomSelectionActionModeCallback(
926                 new ActionModeCallbackAdapter() {
927                     @Override
928                     public boolean onCreateActionMode(ActionMode mode, Menu menu) {
929                         menu.clear();
930                         return true;
931                     }
932 
933                     @Override
934                     public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
935                         menu.clear();
936                         return true;
937                     }
938                 });
939         final String text = "abcd efg hijk lmn";
940         setText(text);
941 
942         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('f')));
943 
944         onHandleView(com.android.internal.R.id.selection_start_handle)
945                 .check(matches(isDisplayed()));
946         onHandleView(com.android.internal.R.id.selection_end_handle)
947                 .check(matches(isDisplayed()));
948     }
949 
950     @Test
testSetSelectionAndActionMode()951     public void testSetSelectionAndActionMode() throws Throwable {
952         final TextView textView = mActivity.findViewById(R.id.textview);
953         final ActionMode.Callback amCallback = mock(ActionMode.Callback.class);
954         when(amCallback.onCreateActionMode(any(ActionMode.class), any(Menu.class)))
955                 .thenReturn(true);
956         when(amCallback.onPrepareActionMode(any(ActionMode.class), any(Menu.class)))
957                 .thenReturn(true);
958         textView.setCustomSelectionActionModeCallback(amCallback);
959 
960         final String text = "abc def";
961         setText(text);
962         mActivityRule.runOnUiThread(
963                 () -> Selection.setSelection((Spannable) textView.getText(), 0, 3));
964         mInstrumentation.waitForIdleSync();
965         // Don't automatically start action mode.
966         verify(amCallback, never()).onCreateActionMode(any(ActionMode.class), any(Menu.class));
967         // Make sure that "Select All" is included in the selection action mode when the entire text
968         // is not selected.
969         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('e')));
970         mToolbar.assertFloatingToolbarIsDisplayed();
971         // Changing the selection range by API should not interrupt the selection action mode.
972         mActivityRule.runOnUiThread(
973                 () -> Selection.setSelection((Spannable) textView.getText(), 0, 3));
974         mInstrumentation.waitForIdleSync();
975         mToolbar.assertFloatingToolbarIsDisplayed();
976         mToolbar.assertFloatingToolbarContainsItem(
977                 mActivity.getString(com.android.internal.R.string.selectAll));
978         // Make sure that "Select All" is no longer included when the entire text is selected by
979         // API.
980         mActivityRule.runOnUiThread(
981                 () -> Selection.setSelection((Spannable) textView.getText(), 0, text.length()));
982         mInstrumentation.waitForIdleSync();
983 
984         mToolbar.assertFloatingToolbarIsDisplayed();
985         mToolbar.assertFloatingToolbarDoesNotContainItem(
986                 mActivity.getString(com.android.internal.R.string.selectAll));
987         // Make sure that shrinking the selection range to cursor (an empty range) by API
988         // terminates selection action mode and does not trigger the insertion action mode.
989         mActivityRule.runOnUiThread(
990                 () -> Selection.setSelection((Spannable) textView.getText(), 0));
991         mInstrumentation.waitForIdleSync();
992 
993         // Make sure that user click can trigger the insertion action mode.
994         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(text.length()));
995         onHandleView(com.android.internal.R.id.insertion_handle).perform(click());
996         mToolbar.assertFloatingToolbarIsDisplayed();
997         // Make sure that an existing insertion action mode keeps alive after the insertion point is
998         // moved by API.
999         mActivityRule.runOnUiThread(
1000                 () -> Selection.setSelection((Spannable) textView.getText(), 0));
1001         mInstrumentation.waitForIdleSync();
1002 
1003         mToolbar.assertFloatingToolbarIsDisplayed();
1004         mToolbar.assertFloatingToolbarDoesNotContainItem(
1005                 mActivity.getString(com.android.internal.R.string.copy));
1006         // Make sure that selection action mode is started after selection is created by API when
1007         // insertion action mode is active.
1008         mActivityRule.runOnUiThread(
1009                 () -> Selection.setSelection((Spannable) textView.getText(), 1, text.length()));
1010         mInstrumentation.waitForIdleSync();
1011 
1012         mToolbar.assertFloatingToolbarIsDisplayed();
1013         mToolbar.assertFloatingToolbarContainsItem(
1014                 mActivity.getString(com.android.internal.R.string.copy));
1015     }
1016 
1017     @Test
testTransientState()1018     public void testTransientState() throws Throwable {
1019         final String text = "abc def";
1020         setText(text);
1021 
1022         final TextView textView = mActivity.findViewById(R.id.textview);
1023         assertFalse(textView.hasTransientState());
1024 
1025         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('b')));
1026         // hasTransientState should return true when user generated selection is active.
1027         assertTrue(textView.hasTransientState());
1028         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(text.indexOf('d')));
1029         // hasTransientState should return false as the selection has been cleared.
1030         assertFalse(textView.hasTransientState());
1031         mActivityRule.runOnUiThread(
1032                 () -> Selection.setSelection((Spannable) textView.getText(), 0, text.length()));
1033         mInstrumentation.waitForIdleSync();
1034 
1035         // hasTransientState should return false when selection is created by API.
1036         assertFalse(textView.hasTransientState());
1037     }
1038 
1039     @Test
testResetMenuItemTitle()1040     public void testResetMenuItemTitle() throws Throwable {
1041         mActivity.getSystemService(TextClassificationManager.class)
1042                 .setTextClassifier(TextClassifier.NO_OP);
1043         final TextView textView = mActivity.findViewById(R.id.textview);
1044         final int itemId = 1;
1045         final String title1 = "@AFIGBO";
1046         final int index = 3;
1047         final String title2 = "IGBO";
1048         final String[] title = new String[]{title1};
1049         mActivityRule.runOnUiThread(() -> textView.setCustomSelectionActionModeCallback(
1050                 new ActionModeCallbackAdapter() {
1051                     @Override
1052                     public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
1053                         menu.clear();
1054                         menu.add(Menu.NONE /* group */, itemId, 0 /* order */, title[0]);
1055                         return true;
1056                     }
1057                 }));
1058         mInstrumentation.waitForIdleSync();
1059 
1060         setText(title1);
1061         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(index));
1062         mToolbar.assertFloatingToolbarContainsItem(title1);
1063 
1064         // Change the menu item title.
1065         title[0] = title2;
1066         // Change the selection to invalidate the action mode without restarting it.
1067         onHandleView(com.android.internal.R.id.selection_start_handle)
1068                 .perform(dragHandle(textView, Handle.SELECTION_START, index));
1069         mToolbar.assertFloatingToolbarContainsItem(title2);
1070     }
1071 
1072     @Test
testAssistItemIsAtIndexZero()1073     public void testAssistItemIsAtIndexZero() throws Throwable {
1074         final SingleActionTextClassifier tc = useSingleActionTextClassifier();
1075         final TextView textView = mActivity.findViewById(R.id.textview);
1076         mActivityRule.runOnUiThread(() -> textView.setCustomSelectionActionModeCallback(
1077                 new ActionModeCallbackAdapter() {
1078                     @Override
1079                     public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
1080                         // Create another item at order position 0 to confirm that it will never be
1081                         // placed before the textAssist item.
1082                         menu.add(Menu.NONE, 0 /* id */, 0 /* order */, "Test");
1083                         return true;
1084                     }
1085                 }));
1086         mInstrumentation.waitForIdleSync();
1087         final String text = "droid@android.com";
1088 
1089         setText(text);
1090         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('@')));
1091         mToolbar.assertFloatingToolbarContainsItemAtIndex(tc.getActionLabel(), 0);
1092     }
1093 
1094     @Test
testNoAssistItemForPasswordField()1095     public void testNoAssistItemForPasswordField() throws Throwable {
1096         final SingleActionTextClassifier tc = useSingleActionTextClassifier();
1097 
1098         final TextView textView = mActivity.findViewById(R.id.textview);
1099         mActivityRule.runOnUiThread(() -> {
1100             textView.setInputType(
1101                     InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
1102         });
1103         mInstrumentation.waitForIdleSync();
1104         final String password = "afigbo@android.com";
1105 
1106         setText(password);
1107         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(password.indexOf('@')));
1108         mToolbar.assertFloatingToolbarDoesNotContainItem(tc.getActionLabel());
1109     }
1110 
1111     @Test
testNoAssistItemForTextFieldWithUnsupportedCharacters()1112     public void testNoAssistItemForTextFieldWithUnsupportedCharacters() throws Throwable {
1113         // NOTE: This test addresses a security bug.
1114         final SingleActionTextClassifier tc = useSingleActionTextClassifier();
1115         final String text = "\u202Emoc.diordna.com";
1116         final TextView textView = mActivity.findViewById(R.id.textview);
1117         mActivityRule.runOnUiThread(() -> textView.setText(text));
1118         mInstrumentation.waitForIdleSync();
1119 
1120         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('.')));
1121         mToolbar.assertFloatingToolbarDoesNotContainItem(tc.getActionLabel());
1122     }
1123 
1124     @Test
testSelectionMetricsLogger_noAbandonAfterCopy()1125     public void testSelectionMetricsLogger_noAbandonAfterCopy() throws Throwable {
1126         final List<SelectionEvent> selectionEvents = new ArrayList<>();
1127         final TextClassifier classifier = new TextClassifier() {
1128             @Override
1129             public void onSelectionEvent(SelectionEvent event) {
1130                 selectionEvents.add(event);
1131             }
1132         };
1133         final TextView textView = mActivity.findViewById(R.id.textview);
1134         mActivityRule.runOnUiThread(() -> textView.setTextClassifier(classifier));
1135         mInstrumentation.waitForIdleSync();
1136         final String text = "andyroid@android.com";
1137 
1138         setText(text);
1139         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('@')));
1140         mToolbar.clickFloatingToolbarItem(mActivity.getString(com.android.internal.R.string.copy));
1141         mInstrumentation.waitForIdleSync();
1142 
1143         final SelectionEvent lastEvent = selectionEvents.get(selectionEvents.size() - 1);
1144         assertEquals(SelectionEvent.ACTION_COPY, lastEvent.getEventType());
1145     }
1146 
1147     @Test
testSelectionMetricsLogger_abandonEventIncludesEntityType()1148     public void testSelectionMetricsLogger_abandonEventIncludesEntityType() throws Throwable {
1149         final TestableTextClassifier classifier = new TestableTextClassifier();
1150         final TextView textView = mActivity.findViewById(R.id.textview);
1151         mActivityRule.runOnUiThread(() -> textView.setTextClassifier(classifier));
1152         mInstrumentation.waitForIdleSync();
1153 
1154         final String text = "My number is 987654321";
1155 
1156         setText(text);
1157         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('9')));
1158         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(0));
1159         mInstrumentation.waitForIdleSync();
1160 
1161         // Abandon event is logged 100ms later. See SelectionActionModeHelper.SelectionTracker
1162         final long abandonDelay = 100;
1163         final long pollInterval = 10;
1164         long waitTime = 0;
1165         SelectionEvent lastEvent;
1166         do {
1167             final List<SelectionEvent> selectionEvents = classifier.getSelectionEvents();
1168             lastEvent = selectionEvents.get(selectionEvents.size() - 1);
1169             if (lastEvent.getEventType() == SelectionEvent.ACTION_ABANDON) {
1170                 break;
1171             }
1172             Thread.sleep(pollInterval);
1173             waitTime += pollInterval;
1174         } while (waitTime < abandonDelay * 10);
1175         assertEquals(SelectionEvent.ACTION_ABANDON, lastEvent.getEventType());
1176     }
1177 
1178     @Test
testSelectionMetricsLogger_overtypeEventIncludesEntityType()1179     public void testSelectionMetricsLogger_overtypeEventIncludesEntityType() throws Throwable {
1180         final TestableTextClassifier classifier = new TestableTextClassifier();
1181         final TextView textView = mActivity.findViewById(R.id.textview);
1182         mActivityRule.runOnUiThread(() -> textView.setTextClassifier(classifier));
1183         mInstrumentation.waitForIdleSync();
1184 
1185         final String text = "My number is 987654321";
1186 
1187         // Long press to trigger selection
1188         setText(text);
1189         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('9')));
1190 
1191         // Type over the selection
1192         onView(withId(R.id.textview)).perform(pressKey(KeyEvent.KEYCODE_A));
1193         mInstrumentation.waitForIdleSync();
1194 
1195         final List<SelectionEvent> selectionEvents = classifier.getSelectionEvents();
1196         final SelectionEvent lastEvent = selectionEvents.get(selectionEvents.size() - 1);
1197         assertEquals(SelectionEvent.ACTION_OVERTYPE, lastEvent.getEventType());
1198         assertEquals(TextClassifier.TYPE_PHONE, lastEvent.getEntityType());
1199     }
1200 
1201     @Test
testTextClassifierSession()1202     public void testTextClassifierSession() throws Throwable {
1203         useSystemDefaultTextClassifier();
1204         TextClassificationManager tcm =
1205                 mActivity.getSystemService(TextClassificationManager.class);
1206         List<TestableTextClassifier> testableTextClassifiers = new ArrayList<>();
1207         tcm.setTextClassificationSessionFactory(classificationContext -> {
1208             TestableTextClassifier textClassifier = new TestableTextClassifier();
1209             testableTextClassifiers.add(textClassifier);
1210             return new TextClassifier() {
1211                 private boolean mIsDestroyed = false;
1212 
1213                 @Override
1214                 public TextSelection suggestSelection(TextSelection.Request request) {
1215                     return textClassifier.suggestSelection(request);
1216                 }
1217 
1218                 @Override
1219                 public void destroy() {
1220                     mIsDestroyed = true;
1221                 }
1222 
1223                 @Override
1224                 public boolean isDestroyed() {
1225                     return mIsDestroyed;
1226                 }
1227             };
1228         });
1229 
1230         // Long press to trigger selection
1231         setText("android.com");
1232         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(0));
1233         // Click "Copy" to dismiss the selection.
1234         mToolbar.clickFloatingToolbarItem(mActivity.getString(com.android.internal.R.string.copy));
1235 
1236         // Long press to trigger another selection
1237         setText("android@android.com");
1238         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(0));
1239 
1240         // suggestSelection should be called in two different TextClassifier sessions.
1241         assertEquals(2, testableTextClassifiers.size());
1242         assertEquals(1, testableTextClassifiers.get(0).getTextSelectionRequests().size());
1243         assertEquals(1, testableTextClassifiers.get(1).getTextSelectionRequests().size());
1244     }
1245 
1246     @Test
testPastePlainText_menuAction()1247     public void testPastePlainText_menuAction() {
1248         initializeClipboardWithText(TextStyle.STYLED);
1249 
1250         setText("");
1251         onView(withId(R.id.textview)).perform(longClick());
1252         mToolbar.clickFloatingToolbarItem(
1253                 mActivity.getString(com.android.internal.R.string.paste_as_plain_text));
1254         mInstrumentation.waitForIdleSync();
1255 
1256         onView(withId(R.id.textview)).check(matches(withText("styledtext")));
1257         onView(withId(R.id.textview)).check(doesNotHaveStyledText());
1258     }
1259 
1260     @Test
testPastePlainText_noMenuItemForPlainText()1261     public void testPastePlainText_noMenuItemForPlainText() {
1262         initializeClipboardWithText(TextStyle.PLAIN);
1263 
1264         setText("");
1265         onView(withId(R.id.textview)).perform(longClick());
1266 
1267         mToolbar.assertFloatingToolbarDoesNotContainItem(
1268                 mActivity.getString(com.android.internal.R.string.paste_as_plain_text));
1269     }
1270 
setText(String text)1271     private void setText(String text) {
1272         onView(withId(R.id.textview)).perform(replaceText(text));
1273         mDevice.wait(Until.findObject(By.text(text)), 1000);
1274         mInstrumentation.waitForIdleSync();
1275     }
1276 
useSystemDefaultTextClassifier()1277     private void useSystemDefaultTextClassifier() {
1278         mActivity.getSystemService(TextClassificationManager.class).setTextClassifier(null);
1279     }
1280 
useSingleActionTextClassifier()1281     private SingleActionTextClassifier useSingleActionTextClassifier() {
1282         useSystemDefaultTextClassifier();
1283         final TextClassificationManager tcm =
1284                 mActivity.getSystemService(TextClassificationManager.class);
1285         final SingleActionTextClassifier oneActionTC =
1286                 new SingleActionTextClassifier(mActivity, tcm.getTextClassifier());
1287         tcm.setTextClassifier(oneActionTC);
1288         return oneActionTC;
1289     }
1290 
initializeClipboardWithText(TextStyle textStyle)1291     private void initializeClipboardWithText(TextStyle textStyle) {
1292         final ClipData clip;
1293         switch (textStyle) {
1294             case STYLED:
1295                 clip = ClipData.newHtmlText("html", "styledtext", "<b>styledtext</b>");
1296                 break;
1297             case PLAIN:
1298                 clip = ClipData.newPlainText("plain", "plaintext");
1299                 break;
1300             default:
1301                 throw new IllegalArgumentException("Invalid text style");
1302         }
1303         mActivity.getWindow().getDecorView().post(() ->
1304                 mActivity.getSystemService(ClipboardManager.class).setPrimaryClip(clip));
1305         mInstrumentation.waitForIdleSync();
1306     }
1307 
1308     private enum TextStyle {
1309         PLAIN, STYLED
1310     }
1311 
1312     private static final class TestableTextClassifier implements TextClassifier {
1313         final List<SelectionEvent> mSelectionEvents = new ArrayList<>();
1314         final List<TextSelection.Request> mTextSelectionRequests = new ArrayList<>();
1315 
1316         @Override
onSelectionEvent(SelectionEvent event)1317         public void onSelectionEvent(SelectionEvent event) {
1318             mSelectionEvents.add(event);
1319         }
1320 
1321         @Override
suggestSelection(TextSelection.Request request)1322         public TextSelection suggestSelection(TextSelection.Request request) {
1323             mTextSelectionRequests.add(request);
1324             return new TextSelection.Builder(request.getStartIndex(), request.getEndIndex())
1325                     .setEntityType(TextClassifier.TYPE_PHONE, 1)
1326                     .build();
1327         }
1328 
getSelectionEvents()1329         List<SelectionEvent> getSelectionEvents() {
1330             return mSelectionEvents;
1331         }
1332 
getTextSelectionRequests()1333         List<TextSelection.Request> getTextSelectionRequests() {
1334             return mTextSelectionRequests;
1335         }
1336     }
1337 
1338     private static final class SingleActionTextClassifier implements TextClassifier {
1339 
1340         private final RemoteAction mAction;
1341         private final TextClassifier mOriginal;
1342         private final TextClassification mClassificationResult;
1343 
SingleActionTextClassifier(Context context, TextClassifier original)1344         SingleActionTextClassifier(Context context, TextClassifier original) {
1345             mAction = new RemoteAction(
1346                     Icon.createWithResource(context, android.R.drawable.btn_star),
1347                     "assist",
1348                     "assist",
1349                     PendingIntent.getActivity(context, 0, new Intent(), FLAG_IMMUTABLE));
1350             mClassificationResult = new TextClassification.Builder().addAction(mAction).build();
1351             mOriginal = Objects.requireNonNull(original);
1352         }
1353 
getActionLabel()1354         public String getActionLabel() {
1355             return mAction.getTitle().toString();
1356         }
1357 
1358         @Override
suggestSelection(TextSelection.Request request)1359         public TextSelection suggestSelection(TextSelection.Request request) {
1360             final TextSelection sel = mOriginal.suggestSelection(request);
1361             return new TextSelection.Builder(
1362                     sel.getSelectionStartIndex(), sel.getSelectionEndIndex())
1363                     .setTextClassification(mClassificationResult)
1364                     .build();
1365         }
1366     }
1367 
1368     private static class ActionModeCallbackAdapter implements ActionMode.Callback {
1369         @Override
onCreateActionMode(ActionMode actionMode, Menu menu)1370         public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
1371             return true;
1372         }
1373 
1374         @Override
onPrepareActionMode(ActionMode actionMode, Menu menu)1375         public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
1376             return true;
1377         }
1378 
1379         @Override
onActionItemClicked(ActionMode actionMode, MenuItem menuItem)1380         public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
1381             return true;
1382         }
1383 
1384         @Override
onDestroyActionMode(ActionMode actionMode)1385         public void onDestroyActionMode(ActionMode actionMode) {}
1386     }
1387 }
1388