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