1 /* 2 * Copyright (C) 2008 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.text.method.cts; 18 19 import static org.junit.Assert.assertEquals; 20 import static org.junit.Assert.assertFalse; 21 import static org.junit.Assert.assertNotNull; 22 import static org.junit.Assert.assertSame; 23 import static org.junit.Assert.assertTrue; 24 import static org.mockito.Matchers.any; 25 import static org.mockito.Mockito.never; 26 import static org.mockito.Mockito.reset; 27 import static org.mockito.Mockito.spy; 28 import static org.mockito.Mockito.times; 29 import static org.mockito.Mockito.verify; 30 31 import android.app.Activity; 32 import android.os.SystemClock; 33 import android.support.test.InstrumentationRegistry; 34 import android.support.test.annotation.UiThreadTest; 35 import android.support.test.filters.MediumTest; 36 import android.support.test.rule.ActivityTestRule; 37 import android.support.test.runner.AndroidJUnit4; 38 import android.text.Selection; 39 import android.text.Spannable; 40 import android.text.SpannableString; 41 import android.text.Spanned; 42 import android.text.method.LinkMovementMethod; 43 import android.text.method.MovementMethod; 44 import android.text.style.ClickableSpan; 45 import android.util.TypedValue; 46 import android.view.KeyEvent; 47 import android.view.MotionEvent; 48 import android.view.View; 49 import android.widget.TextView; 50 import android.widget.TextView.BufferType; 51 52 import org.junit.Before; 53 import org.junit.Rule; 54 import org.junit.Test; 55 import org.junit.runner.RunWith; 56 57 /** 58 * Test {@link LinkMovementMethod}. The class is an implementation of interface 59 * {@link MovementMethod}. The typical usage of {@link MovementMethod} is tested in 60 * {@link android.widget.cts.TextViewTest} and this test case is only focused on the 61 * implementation of the methods. 62 * 63 * @see android.widget.cts.TextViewTest 64 */ 65 @MediumTest 66 @RunWith(AndroidJUnit4.class) 67 public class LinkMovementMethodTest { 68 private static final String CONTENT = "clickable\nunclickable\nclickable"; 69 70 private Activity mActivity; 71 private LinkMovementMethod mMethod; 72 private TextView mView; 73 private Spannable mSpannable; 74 private ClickableSpan mClickable0; 75 private ClickableSpan mClickable1; 76 77 @Rule 78 public ActivityTestRule<CtsActivity> mActivityRule = new ActivityTestRule<>(CtsActivity.class); 79 80 @Before 81 public void setup() throws Throwable { 82 mActivity = mActivityRule.getActivity(); 83 mMethod = new LinkMovementMethod(); 84 85 // Set the content view with a text view which contains 3 lines, 86 mActivityRule.runOnUiThread(() -> mView = new TextViewNoIme(mActivity)); 87 mView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10); 88 mView.setText(CONTENT, BufferType.SPANNABLE); 89 90 mActivityRule.runOnUiThread(() -> mActivity.setContentView(mView)); 91 InstrumentationRegistry.getInstrumentation().waitForIdleSync(); 92 93 mSpannable = (Spannable) mView.getText(); 94 // make first line clickable 95 mClickable0 = markClickable(0, CONTENT.indexOf('\n')); 96 // make last line clickable 97 mClickable1 = markClickable(CONTENT.lastIndexOf('\n'), CONTENT.length()); 98 } 99 100 @Test 101 public void testConstructor() { 102 new LinkMovementMethod(); 103 } 104 105 @Test 106 public void testGetInstance() { 107 MovementMethod method0 = LinkMovementMethod.getInstance(); 108 assertTrue(method0 instanceof LinkMovementMethod); 109 110 MovementMethod method1 = LinkMovementMethod.getInstance(); 111 assertNotNull(method1); 112 assertSame(method0, method1); 113 } 114 115 @Test 116 public void testOnTakeFocus() { 117 LinkMovementMethod method = new LinkMovementMethod(); 118 Spannable spannable = new SpannableString("test sequence"); 119 Selection.setSelection(spannable, 0, spannable.length()); 120 121 assertSelection(spannable, 0, spannable.length()); 122 assertTrue("Expected at least 2 spans", 123 2 <= spannable.getSpans(0, spannable.length(), Object.class).length); 124 method.onTakeFocus(null, spannable, View.FOCUS_UP); 125 assertSelection(spannable, -1); 126 assertEquals(1, spannable.getSpans(0, spannable.length(), Object.class).length); 127 Object span = spannable.getSpans(0, spannable.length(), Object.class)[0]; 128 assertEquals(0, spannable.getSpanStart(span)); 129 assertEquals(0, spannable.getSpanEnd(span)); 130 assertEquals(Spanned.SPAN_POINT_POINT, spannable.getSpanFlags(span)); 131 132 // focus forwards 133 Selection.setSelection(spannable, 0, spannable.length()); 134 assertSelection(spannable, 0, spannable.length()); 135 assertTrue("Expected at least 3 spans", 136 3 <= spannable.getSpans(0, spannable.length(), Object.class).length); 137 method.onTakeFocus(null, spannable, View.FOCUS_RIGHT); 138 assertSelection(spannable, -1); 139 assertEquals(0, spannable.getSpans(0, spannable.length(), Object.class).length); 140 141 // force adding span while focus backward 142 method.onTakeFocus(null, spannable, View.FOCUS_UP); 143 // param direction is unknown(0) 144 Selection.setSelection(spannable, 0, spannable.length()); 145 assertSelection(spannable, 0, spannable.length()); 146 assertTrue("Expected at least 3 spans", 147 3 <= spannable.getSpans(0, spannable.length(), Object.class).length); 148 method.onTakeFocus(null, spannable, 0); 149 assertSelection(spannable, -1); 150 assertEquals(0, spannable.getSpans(0, spannable.length(), Object.class).length); 151 } 152 153 @UiThreadTest 154 @Test(expected=NullPointerException.class) 155 public void testOnTakeFocusNullSpannable() { 156 LinkMovementMethod method = new LinkMovementMethod(); 157 method.onTakeFocus(new TextViewNoIme(mActivity), null, View.FOCUS_RIGHT); 158 } 159 160 @UiThreadTest 161 @Test 162 public void testOnKeyDown() { 163 // no selection 164 assertSelection(mSpannable, -1); 165 reset(mClickable0); 166 reset(mClickable1); 167 assertFalse(mMethod.onKeyDown(mView, mSpannable, KeyEvent.KEYCODE_ENTER, 168 new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER))); 169 verify(mClickable0, never()).onClick(any()); 170 verify(mClickable1, never()).onClick(any()); 171 172 // select clickable0 173 Selection.setSelection(mSpannable, mSpannable.getSpanStart(mClickable0), 174 mSpannable.getSpanEnd(mClickable0)); 175 reset(mClickable0); 176 reset(mClickable1); 177 assertFalse(mMethod.onKeyDown(mView, mSpannable, KeyEvent.KEYCODE_DPAD_CENTER, 178 new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_CENTER))); 179 verify(mClickable0, times(1)).onClick(any()); 180 verify(mClickable1, never()).onClick(any()); 181 182 // select unclickable 183 Selection.setSelection(mSpannable, mSpannable.getSpanEnd(mClickable0), 184 mSpannable.getSpanStart(mClickable1)); 185 reset(mClickable0); 186 reset(mClickable1); 187 assertFalse(mMethod.onKeyDown(mView, mSpannable, KeyEvent.KEYCODE_ENTER, 188 new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER))); 189 verify(mClickable0, never()).onClick(any()); 190 verify(mClickable1, never()).onClick(any()); 191 192 // select all clickables(more than one) 193 Selection.selectAll(mSpannable); 194 reset(mClickable0); 195 reset(mClickable1); 196 assertFalse(mMethod.onKeyDown(mView, mSpannable, KeyEvent.KEYCODE_DPAD_CENTER, 197 new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_CENTER))); 198 verify(mClickable0, never()).onClick(any()); 199 verify(mClickable1, never()).onClick(any()); 200 201 // part of selection is clickable 202 Selection.setSelection(mSpannable, mSpannable.getSpanEnd(mClickable0), 203 mSpannable.getSpanEnd(mClickable1)); 204 reset(mClickable0); 205 reset(mClickable1); 206 assertFalse(mMethod.onKeyDown(mView, mSpannable, KeyEvent.KEYCODE_DPAD_CENTER, 207 new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_CENTER))); 208 verify(mClickable0, never()).onClick(any()); 209 verify(mClickable1, times(1)).onClick(any()); 210 211 // selection contains only clickable1 and repeat count of the event is not 0 212 Selection.setSelection(mSpannable, mSpannable.getSpanEnd(mClickable0), 213 mSpannable.getSpanEnd(mClickable1)); 214 long now = SystemClock.uptimeMillis(); 215 KeyEvent event = new KeyEvent(now, now, KeyEvent.ACTION_DOWN, 216 KeyEvent.KEYCODE_DPAD_CENTER, 1); 217 218 reset(mClickable0); 219 reset(mClickable1); 220 assertFalse(mMethod.onKeyDown(mView, mSpannable, KeyEvent.KEYCODE_DPAD_CENTER, event)); 221 verify(mClickable0, never()).onClick(any()); 222 verify(mClickable1, never()).onClick(any()); 223 } 224 225 @UiThreadTest 226 @Test(expected=NullPointerException.class) 227 public void testOnKeyDown_nullViewParam() { 228 mMethod.onKeyDown(null, mSpannable, KeyEvent.KEYCODE_DPAD_CENTER, 229 new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_CENTER)); 230 } 231 232 @UiThreadTest 233 @Test(expected=NullPointerException.class) 234 public void testOnKeyDown_nullSpannableParam() { 235 mMethod.onKeyDown(mView, null, KeyEvent.KEYCODE_DPAD_CENTER, 236 new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_CENTER)); 237 } 238 239 @UiThreadTest 240 @Test(expected=NullPointerException.class) 241 public void testOnKeyDown_nullKeyEventParam() { 242 mMethod.onKeyDown(mView, mSpannable, KeyEvent.KEYCODE_DPAD_CENTER, null); 243 } 244 245 @UiThreadTest 246 @Test 247 public void testOnKeyUp() { 248 LinkMovementMethod method = new LinkMovementMethod(); 249 // always returns false 250 assertFalse(method.onKeyUp(null, null, 0, null)); 251 assertFalse(method.onKeyUp(new TextViewNoIme(mActivity), null, 0, null)); 252 assertFalse(method.onKeyUp(null, new SpannableString("blahblah"), 0, null)); 253 assertFalse(method.onKeyUp(null, null, KeyEvent.KEYCODE_0, 254 new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_0))); 255 } 256 257 @UiThreadTest 258 @Test 259 public void testOnTouchEvent() { 260 assertSelection(mSpannable, -1); 261 262 // press on first line (Clickable) 263 assertTrue(pressOnLine(0)); 264 assertSelectClickableLeftToRight(mSpannable, mClickable0); 265 266 // release on first line 267 verify(mClickable0, never()).onClick(any()); 268 assertTrue(releaseOnLine(0)); 269 verify(mClickable0, times(1)).onClick(any()); 270 271 // press on second line (unclickable) 272 assertSelectClickableLeftToRight(mSpannable, mClickable0); 273 // just clear selection 274 pressOnLine(1); 275 assertSelection(mSpannable, -1); 276 277 // press on last line (Clickable) 278 assertTrue(pressOnLine(2)); 279 assertSelectClickableLeftToRight(mSpannable, mClickable1); 280 281 // release on last line 282 verify(mClickable1, never()).onClick(any()); 283 assertTrue(releaseOnLine(2)); 284 verify(mClickable1, times(1)).onClick(any()); 285 286 // release on second line (unclickable) 287 assertSelectClickableLeftToRight(mSpannable, mClickable1); 288 // just clear selection 289 releaseOnLine(1); 290 assertSelection(mSpannable, -1); 291 } 292 293 @UiThreadTest 294 @Test(expected=NullPointerException.class) 295 public void testOnTouchEvent_nullViewParam() { 296 long now = SystemClock.uptimeMillis(); 297 int y = (mView.getLayout().getLineTop(1) + mView.getLayout().getLineBottom(1)) / 2; 298 mMethod.onTouchEvent(null, mSpannable, 299 MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN, 5, y, 0)); 300 } 301 302 @UiThreadTest 303 @Test(expected=NullPointerException.class) 304 public void testOnTouchEvent_nullSpannableParam() { 305 long now = SystemClock.uptimeMillis(); 306 int y = (mView.getLayout().getLineTop(1) + mView.getLayout().getLineBottom(1)) / 2; 307 mMethod.onTouchEvent(mView, null, 308 MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN, 5, y, 0)); 309 } 310 311 @UiThreadTest 312 @Test(expected=NullPointerException.class) 313 public void testOnTouchEvent_nullKeyEventParam() { 314 mMethod.onTouchEvent(mView, mSpannable, null); 315 } 316 317 @UiThreadTest 318 @Test 319 public void testUp() { 320 final MyLinkMovementMethod method = new MyLinkMovementMethod(); 321 assertSelection(mSpannable, -1); 322 323 assertTrue(method.up(mView, mSpannable)); 324 assertSelectClickableRightToLeft(mSpannable, mClickable1); 325 326 assertTrue(method.up(mView, mSpannable)); 327 assertSelectClickableRightToLeft(mSpannable, mClickable0); 328 329 assertFalse(method.up(mView, mSpannable)); 330 assertSelectClickableRightToLeft(mSpannable, mClickable0); 331 } 332 333 @UiThreadTest 334 @Test(expected=NullPointerException.class) 335 public void testUp_nullViewParam() { 336 final MyLinkMovementMethod method = new MyLinkMovementMethod(); 337 method.up(null, mSpannable); 338 } 339 340 @UiThreadTest 341 @Test(expected=NullPointerException.class) 342 public void testUp_nullSpannableParam() { 343 final MyLinkMovementMethod method = new MyLinkMovementMethod(); 344 method.up(mView, null); 345 } 346 347 @UiThreadTest 348 @Test 349 public void testDown() { 350 final MyLinkMovementMethod method = new MyLinkMovementMethod(); 351 assertSelection(mSpannable, -1); 352 353 assertTrue(method.down(mView, mSpannable)); 354 assertSelectClickableLeftToRight(mSpannable, mClickable0); 355 356 assertTrue(method.down(mView, mSpannable)); 357 assertSelectClickableLeftToRight(mSpannable, mClickable1); 358 359 assertFalse(method.down(mView, mSpannable)); 360 assertSelectClickableLeftToRight(mSpannable, mClickable1); 361 } 362 363 @UiThreadTest 364 @Test(expected=NullPointerException.class) 365 public void testDown_nullViewParam() { 366 final MyLinkMovementMethod method = new MyLinkMovementMethod(); 367 method.down(null, mSpannable); 368 } 369 370 @UiThreadTest 371 @Test(expected=NullPointerException.class) 372 public void testDown_nullSpannableParam() { 373 final MyLinkMovementMethod method = new MyLinkMovementMethod(); 374 method.down(mView, null); 375 } 376 377 @UiThreadTest 378 @Test 379 public void testLeft() { 380 final MyLinkMovementMethod method = new MyLinkMovementMethod(); 381 assertSelection(mSpannable, -1); 382 383 assertTrue(method.left(mView, mSpannable)); 384 assertSelectClickableRightToLeft(mSpannable, mClickable1); 385 386 assertTrue(method.left(mView, mSpannable)); 387 assertSelectClickableRightToLeft(mSpannable, mClickable0); 388 389 assertFalse(method.left(mView, mSpannable)); 390 assertSelectClickableRightToLeft(mSpannable, mClickable0); 391 } 392 393 @UiThreadTest 394 @Test(expected=NullPointerException.class) 395 public void testLeft_nullViewParam() { 396 final MyLinkMovementMethod method = new MyLinkMovementMethod(); 397 method.left(null, mSpannable); 398 } 399 400 @UiThreadTest 401 @Test(expected=NullPointerException.class) 402 public void testLeft_nullSpannableParam() { 403 final MyLinkMovementMethod method = new MyLinkMovementMethod(); 404 method.left(mView, null); 405 } 406 407 @UiThreadTest 408 @Test 409 public void testRight() { 410 final MyLinkMovementMethod method = new MyLinkMovementMethod(); 411 assertSelection(mSpannable, -1); 412 413 assertTrue(method.right(mView, mSpannable)); 414 assertSelectClickableLeftToRight(mSpannable, mClickable0); 415 416 assertTrue(method.right(mView, mSpannable)); 417 assertSelectClickableLeftToRight(mSpannable, mClickable1); 418 419 assertFalse(method.right(mView, mSpannable)); 420 assertSelectClickableLeftToRight(mSpannable, mClickable1); 421 } 422 423 @UiThreadTest 424 @Test(expected=NullPointerException.class) 425 public void testRight_nullViewParam() { 426 final MyLinkMovementMethod method = new MyLinkMovementMethod(); 427 method.right(null, mSpannable); 428 } 429 430 @UiThreadTest 431 @Test(expected=NullPointerException.class) 432 public void testRight_nullSpannableParam() { 433 final MyLinkMovementMethod method = new MyLinkMovementMethod(); 434 method.right(mView, null); 435 } 436 437 @UiThreadTest 438 @Test 439 public void testMoveAroundUnclickable() { 440 final MyLinkMovementMethod method = new MyLinkMovementMethod(); 441 mSpannable.removeSpan(mClickable0); 442 mSpannable.removeSpan(mClickable1); 443 assertSelection(mSpannable, -1); 444 445 assertFalse(method.up(mView, mSpannable)); 446 assertSelection(mSpannable, -1); 447 448 assertFalse(method.down(mView, mSpannable)); 449 assertSelection(mSpannable, -1); 450 451 assertFalse(method.left(mView, mSpannable)); 452 assertSelection(mSpannable, -1); 453 454 assertFalse(method.right(mView, mSpannable)); 455 assertSelection(mSpannable, -1); 456 } 457 458 @Test 459 public void testInitialize() { 460 LinkMovementMethod method = new LinkMovementMethod(); 461 Spannable spannable = new SpannableString("test sequence"); 462 method.onTakeFocus(null, spannable, View.FOCUS_UP); 463 Selection.setSelection(spannable, 0, spannable.length()); 464 465 assertSelection(spannable, 0, spannable.length()); 466 assertTrue("Expected at least 3 spans", 3 <= spannable.getSpans(0, spannable.length(), Object.class).length); 467 method.initialize(null, spannable); 468 assertSelection(spannable, -1); 469 assertEquals(0, spannable.getSpans(0, spannable.length(), Object.class).length); 470 471 } 472 473 @Test(expected=NullPointerException.class) 474 public void testInitialize_nullViewParam() { 475 final LinkMovementMethod method = new LinkMovementMethod(); 476 method.initialize(mView, null); 477 } 478 479 private ClickableSpan markClickable(final int start, final int end) throws Throwable { 480 final ClickableSpan clickableSpan = spy(new MockClickableSpan()); 481 mActivityRule.runOnUiThread(() -> mSpannable.setSpan(clickableSpan, start, end, 482 Spanned.SPAN_MARK_MARK)); 483 InstrumentationRegistry.getInstrumentation().waitForIdleSync(); 484 return clickableSpan; 485 } 486 487 private boolean performMotionOnLine(int line, int action) { 488 int x = (mView.getLayout().getLineStart(line) + mView.getLayout().getLineEnd(line)) / 2; 489 int y = (mView.getLayout().getLineTop(line) + mView.getLayout().getLineBottom(line)) / 2; 490 long now = SystemClock.uptimeMillis(); 491 492 return mMethod.onTouchEvent(mView, mSpannable, 493 MotionEvent.obtain(now, now, action, x, y, 0)); 494 } 495 496 private boolean pressOnLine(int line) { 497 return performMotionOnLine(line, MotionEvent.ACTION_DOWN); 498 } 499 500 private boolean releaseOnLine(int line) { 501 return performMotionOnLine(line, MotionEvent.ACTION_UP); 502 } 503 504 private void assertSelection(Spannable spannable, int start, int end) { 505 assertEquals(start, Selection.getSelectionStart(spannable)); 506 assertEquals(end, Selection.getSelectionEnd(spannable)); 507 } 508 509 private void assertSelection(Spannable spannable, int position) { 510 assertSelection(spannable, position, position); 511 } 512 513 private void assertSelectClickableLeftToRight(Spannable spannable, 514 ClickableSpan clickableSpan) { 515 assertSelection(spannable, spannable.getSpanStart(clickableSpan), 516 spannable.getSpanEnd(clickableSpan)); 517 } 518 519 private void assertSelectClickableRightToLeft(Spannable spannable, 520 ClickableSpan clickableSpan) { 521 assertSelection(spannable, spannable.getSpanEnd(clickableSpan), 522 spannable.getSpanStart(clickableSpan)); 523 } 524 525 private static class MyLinkMovementMethod extends LinkMovementMethod { 526 @Override 527 protected boolean down(TextView widget, Spannable buffer) { 528 return super.down(widget, buffer); 529 } 530 531 @Override 532 protected boolean left(TextView widget, Spannable buffer) { 533 return super.left(widget, buffer); 534 } 535 536 @Override 537 protected boolean right(TextView widget, Spannable buffer) { 538 return super.right(widget, buffer); 539 } 540 541 @Override 542 protected boolean up(TextView widget, Spannable buffer) { 543 return super.up(widget, buffer); 544 } 545 } 546 547 public static class MockClickableSpan extends ClickableSpan { 548 @Override 549 public void onClick(View widget) { 550 } 551 } 552 } 553