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