1 /* 2 * Copyright (C) 2006 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; 18 19 import android.graphics.Paint; 20 import android.icu.lang.UCharacter; 21 import android.icu.lang.UProperty; 22 import android.text.Editable; 23 import android.text.Emoji; 24 import android.text.InputType; 25 import android.text.Layout; 26 import android.text.NoCopySpan; 27 import android.text.Selection; 28 import android.text.Spanned; 29 import android.text.method.TextKeyListener.Capitalize; 30 import android.text.style.ReplacementSpan; 31 import android.view.KeyEvent; 32 import android.view.View; 33 import android.widget.TextView; 34 35 import com.android.internal.annotations.GuardedBy; 36 37 import java.text.BreakIterator; 38 39 /** 40 * Abstract base class for key listeners. 41 * 42 * Provides a basic foundation for entering and editing text. 43 * Subclasses should override {@link #onKeyDown} and {@link #onKeyUp} to insert 44 * characters as keys are pressed. 45 * <p></p> 46 * As for all implementations of {@link KeyListener}, this class is only concerned 47 * with hardware keyboards. Software input methods have no obligation to trigger 48 * the methods in this class. 49 */ 50 public abstract class BaseKeyListener extends MetaKeyKeyListener 51 implements KeyListener { 52 /* package */ static final Object OLD_SEL_START = new NoCopySpan.Concrete(); 53 54 private static final int LINE_FEED = 0x0A; 55 private static final int CARRIAGE_RETURN = 0x0D; 56 57 private final Object mLock = new Object(); 58 59 @GuardedBy("mLock") 60 static Paint sCachedPaint = null; 61 62 /** 63 * Performs the action that happens when you press the {@link KeyEvent#KEYCODE_DEL} key in 64 * a {@link TextView}. If there is a selection, deletes the selection; otherwise, 65 * deletes the character before the cursor, if any; ALT+DEL deletes everything on 66 * the line the cursor is on. 67 * 68 * @return true if anything was deleted; false otherwise. 69 */ backspace(View view, Editable content, int keyCode, KeyEvent event)70 public boolean backspace(View view, Editable content, int keyCode, KeyEvent event) { 71 return backspaceOrForwardDelete(view, content, keyCode, event, false); 72 } 73 74 /** 75 * Performs the action that happens when you press the {@link KeyEvent#KEYCODE_FORWARD_DEL} 76 * key in a {@link TextView}. If there is a selection, deletes the selection; otherwise, 77 * deletes the character before the cursor, if any; ALT+FORWARD_DEL deletes everything on 78 * the line the cursor is on. 79 * 80 * @return true if anything was deleted; false otherwise. 81 */ forwardDelete(View view, Editable content, int keyCode, KeyEvent event)82 public boolean forwardDelete(View view, Editable content, int keyCode, KeyEvent event) { 83 return backspaceOrForwardDelete(view, content, keyCode, event, true); 84 } 85 86 // Returns true if the given code point is a variation selector. isVariationSelector(int codepoint)87 private static boolean isVariationSelector(int codepoint) { 88 return UCharacter.hasBinaryProperty(codepoint, UProperty.VARIATION_SELECTOR); 89 } 90 91 // Returns the offset of the replacement span edge if the offset is inside of the replacement 92 // span. Otherwise, does nothing and returns the input offset value. adjustReplacementSpan(CharSequence text, int offset, boolean moveToStart)93 private static int adjustReplacementSpan(CharSequence text, int offset, boolean moveToStart) { 94 if (!(text instanceof Spanned)) { 95 return offset; 96 } 97 98 ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset, ReplacementSpan.class); 99 for (int i = 0; i < spans.length; i++) { 100 final int start = ((Spanned) text).getSpanStart(spans[i]); 101 final int end = ((Spanned) text).getSpanEnd(spans[i]); 102 103 if (start < offset && end > offset) { 104 offset = moveToStart ? start : end; 105 } 106 } 107 return offset; 108 } 109 110 // Returns the start offset to be deleted by a backspace key from the given offset. getOffsetForBackspaceKey(CharSequence text, int offset)111 private static int getOffsetForBackspaceKey(CharSequence text, int offset) { 112 if (offset <= 1) { 113 return 0; 114 } 115 116 // Initial state 117 final int STATE_START = 0; 118 119 // The offset is immediately before line feed. 120 final int STATE_LF = 1; 121 122 // The offset is immediately before a KEYCAP. 123 final int STATE_BEFORE_KEYCAP = 2; 124 // The offset is immediately before a variation selector and a KEYCAP. 125 final int STATE_BEFORE_VS_AND_KEYCAP = 3; 126 127 // The offset is immediately before an emoji modifier. 128 final int STATE_BEFORE_EMOJI_MODIFIER = 4; 129 // The offset is immediately before a variation selector and an emoji modifier. 130 final int STATE_BEFORE_VS_AND_EMOJI_MODIFIER = 5; 131 132 // The offset is immediately before a variation selector. 133 final int STATE_BEFORE_VS = 6; 134 135 // The offset is immediately before an emoji. 136 final int STATE_BEFORE_EMOJI = 7; 137 // The offset is immediately before a ZWJ that were seen before a ZWJ emoji. 138 final int STATE_BEFORE_ZWJ = 8; 139 // The offset is immediately before a variation selector and a ZWJ that were seen before a 140 // ZWJ emoji. 141 final int STATE_BEFORE_VS_AND_ZWJ = 9; 142 143 // The number of following RIS code points is odd. 144 final int STATE_ODD_NUMBERED_RIS = 10; 145 // The number of following RIS code points is even. 146 final int STATE_EVEN_NUMBERED_RIS = 11; 147 148 // The offset is in emoji tag sequence. 149 final int STATE_IN_TAG_SEQUENCE = 12; 150 151 // The state machine has been stopped. 152 final int STATE_FINISHED = 13; 153 154 int deleteCharCount = 0; // Char count to be deleted by backspace. 155 int lastSeenVSCharCount = 0; // Char count of previous variation selector. 156 157 int state = STATE_START; 158 159 int tmpOffset = offset; 160 do { 161 final int codePoint = Character.codePointBefore(text, tmpOffset); 162 tmpOffset -= Character.charCount(codePoint); 163 164 switch (state) { 165 case STATE_START: 166 deleteCharCount = Character.charCount(codePoint); 167 if (codePoint == LINE_FEED) { 168 state = STATE_LF; 169 } else if (isVariationSelector(codePoint)) { 170 state = STATE_BEFORE_VS; 171 } else if (Emoji.isRegionalIndicatorSymbol(codePoint)) { 172 state = STATE_ODD_NUMBERED_RIS; 173 } else if (Emoji.isEmojiModifier(codePoint)) { 174 state = STATE_BEFORE_EMOJI_MODIFIER; 175 } else if (codePoint == Emoji.COMBINING_ENCLOSING_KEYCAP) { 176 state = STATE_BEFORE_KEYCAP; 177 } else if (Emoji.isEmoji(codePoint)) { 178 state = STATE_BEFORE_EMOJI; 179 } else if (codePoint == Emoji.CANCEL_TAG) { 180 state = STATE_IN_TAG_SEQUENCE; 181 } else { 182 state = STATE_FINISHED; 183 } 184 break; 185 case STATE_LF: 186 if (codePoint == CARRIAGE_RETURN) { 187 ++deleteCharCount; 188 } 189 state = STATE_FINISHED; 190 break; 191 case STATE_ODD_NUMBERED_RIS: 192 if (Emoji.isRegionalIndicatorSymbol(codePoint)) { 193 deleteCharCount += 2; /* Char count of RIS */ 194 state = STATE_EVEN_NUMBERED_RIS; 195 } else { 196 state = STATE_FINISHED; 197 } 198 break; 199 case STATE_EVEN_NUMBERED_RIS: 200 if (Emoji.isRegionalIndicatorSymbol(codePoint)) { 201 deleteCharCount -= 2; /* Char count of RIS */ 202 state = STATE_ODD_NUMBERED_RIS; 203 } else { 204 state = STATE_FINISHED; 205 } 206 break; 207 case STATE_BEFORE_KEYCAP: 208 if (isVariationSelector(codePoint)) { 209 lastSeenVSCharCount = Character.charCount(codePoint); 210 state = STATE_BEFORE_VS_AND_KEYCAP; 211 break; 212 } 213 214 if (Emoji.isKeycapBase(codePoint)) { 215 deleteCharCount += Character.charCount(codePoint); 216 } 217 state = STATE_FINISHED; 218 break; 219 case STATE_BEFORE_VS_AND_KEYCAP: 220 if (Emoji.isKeycapBase(codePoint)) { 221 deleteCharCount += lastSeenVSCharCount + Character.charCount(codePoint); 222 } 223 state = STATE_FINISHED; 224 break; 225 case STATE_BEFORE_EMOJI_MODIFIER: 226 if (isVariationSelector(codePoint)) { 227 lastSeenVSCharCount = Character.charCount(codePoint); 228 state = STATE_BEFORE_VS_AND_EMOJI_MODIFIER; 229 break; 230 } else if (Emoji.isEmojiModifierBase(codePoint)) { 231 deleteCharCount += Character.charCount(codePoint); 232 state = STATE_BEFORE_EMOJI; 233 break; 234 } 235 state = STATE_FINISHED; 236 break; 237 case STATE_BEFORE_VS_AND_EMOJI_MODIFIER: 238 if (Emoji.isEmojiModifierBase(codePoint)) { 239 deleteCharCount += lastSeenVSCharCount + Character.charCount(codePoint); 240 } 241 state = STATE_FINISHED; 242 break; 243 case STATE_BEFORE_VS: 244 if (Emoji.isEmoji(codePoint)) { 245 deleteCharCount += Character.charCount(codePoint); 246 state = STATE_BEFORE_EMOJI; 247 break; 248 } 249 250 if (!isVariationSelector(codePoint) && 251 UCharacter.getCombiningClass(codePoint) == 0) { 252 deleteCharCount += Character.charCount(codePoint); 253 } 254 state = STATE_FINISHED; 255 break; 256 case STATE_BEFORE_EMOJI: 257 if (codePoint == Emoji.ZERO_WIDTH_JOINER) { 258 state = STATE_BEFORE_ZWJ; 259 } else { 260 state = STATE_FINISHED; 261 } 262 break; 263 case STATE_BEFORE_ZWJ: 264 if (Emoji.isEmoji(codePoint)) { 265 deleteCharCount += Character.charCount(codePoint) + 1; // +1 for ZWJ. 266 state = Emoji.isEmojiModifier(codePoint) ? 267 STATE_BEFORE_EMOJI_MODIFIER : STATE_BEFORE_EMOJI; 268 } else if (isVariationSelector(codePoint)) { 269 lastSeenVSCharCount = Character.charCount(codePoint); 270 state = STATE_BEFORE_VS_AND_ZWJ; 271 } else { 272 state = STATE_FINISHED; 273 } 274 break; 275 case STATE_BEFORE_VS_AND_ZWJ: 276 if (Emoji.isEmoji(codePoint)) { 277 // +1 for ZWJ. 278 deleteCharCount += lastSeenVSCharCount + 1 + Character.charCount(codePoint); 279 lastSeenVSCharCount = 0; 280 state = STATE_BEFORE_EMOJI; 281 } else { 282 state = STATE_FINISHED; 283 } 284 break; 285 case STATE_IN_TAG_SEQUENCE: 286 if (Emoji.isTagSpecChar(codePoint)) { 287 deleteCharCount += 2; /* Char count of emoji tag spec character. */ 288 // Keep the same state. 289 } else if (Emoji.isEmoji(codePoint)) { 290 deleteCharCount += Character.charCount(codePoint); 291 state = STATE_FINISHED; 292 } else { 293 // Couldn't find tag_base character. Delete the last tag_term character. 294 deleteCharCount = 2; // for U+E007F 295 state = STATE_FINISHED; 296 } 297 // TODO: Need handle emoji variation selectors. Issue 35224297 298 break; 299 default: 300 throw new IllegalArgumentException("state " + state + " is unknown"); 301 } 302 } while (tmpOffset > 0 && state != STATE_FINISHED); 303 304 return adjustReplacementSpan(text, offset - deleteCharCount, true /* move to the start */); 305 } 306 307 // Returns the end offset to be deleted by a forward delete key from the given offset. getOffsetForForwardDeleteKey(CharSequence text, int offset, Paint paint)308 private static int getOffsetForForwardDeleteKey(CharSequence text, int offset, Paint paint) { 309 final int len = text.length(); 310 311 if (offset >= len - 1) { 312 return len; 313 } 314 315 offset = paint.getTextRunCursor(text, offset, len, false /* LTR, not used */, 316 offset, Paint.CURSOR_AFTER); 317 318 return adjustReplacementSpan(text, offset, false /* move to the end */); 319 } 320 backspaceOrForwardDelete(View view, Editable content, int keyCode, KeyEvent event, boolean isForwardDelete)321 private boolean backspaceOrForwardDelete(View view, Editable content, int keyCode, 322 KeyEvent event, boolean isForwardDelete) { 323 // Ensure the key event does not have modifiers except ALT or SHIFT or CTRL. 324 if (!KeyEvent.metaStateHasNoModifiers(event.getMetaState() 325 & ~(KeyEvent.META_SHIFT_MASK | KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK))) { 326 return false; 327 } 328 329 // If there is a current selection, delete it. 330 if (deleteSelection(view, content)) { 331 return true; 332 } 333 334 // MetaKeyKeyListener doesn't track control key state. Need to check the KeyEvent instead. 335 boolean isCtrlActive = ((event.getMetaState() & KeyEvent.META_CTRL_ON) != 0); 336 boolean isShiftActive = (getMetaState(content, META_SHIFT_ON, event) == 1); 337 boolean isAltActive = (getMetaState(content, META_ALT_ON, event) == 1); 338 339 if (isCtrlActive) { 340 if (isAltActive || isShiftActive) { 341 // Ctrl+Alt, Ctrl+Shift, Ctrl+Alt+Shift should not delete any characters. 342 return false; 343 } 344 return deleteUntilWordBoundary(view, content, isForwardDelete); 345 } 346 347 // Alt+Backspace or Alt+ForwardDelete deletes the current line, if possible. 348 if (isAltActive && deleteLineFromCursor(view, content, isForwardDelete)) { 349 return true; 350 } 351 352 // Delete a character. 353 final int start = Selection.getSelectionEnd(content); 354 final int end; 355 if (isForwardDelete) { 356 final Paint paint; 357 if (view instanceof TextView) { 358 paint = ((TextView)view).getPaint(); 359 } else { 360 synchronized (mLock) { 361 if (sCachedPaint == null) { 362 sCachedPaint = new Paint(); 363 } 364 paint = sCachedPaint; 365 } 366 } 367 end = getOffsetForForwardDeleteKey(content, start, paint); 368 } else { 369 end = getOffsetForBackspaceKey(content, start); 370 } 371 if (start != end) { 372 content.delete(Math.min(start, end), Math.max(start, end)); 373 return true; 374 } 375 return false; 376 } 377 deleteUntilWordBoundary(View view, Editable content, boolean isForwardDelete)378 private boolean deleteUntilWordBoundary(View view, Editable content, boolean isForwardDelete) { 379 int currentCursorOffset = Selection.getSelectionStart(content); 380 381 // If there is a selection, do nothing. 382 if (currentCursorOffset != Selection.getSelectionEnd(content)) { 383 return false; 384 } 385 386 // Early exit if there is no contents to delete. 387 if ((!isForwardDelete && currentCursorOffset == 0) || 388 (isForwardDelete && currentCursorOffset == content.length())) { 389 return false; 390 } 391 392 WordIterator wordIterator = null; 393 if (view instanceof TextView) { 394 wordIterator = ((TextView)view).getWordIterator(); 395 } 396 397 if (wordIterator == null) { 398 // Default locale is used for WordIterator since the appropriate locale is not clear 399 // here. 400 // TODO: Use appropriate locale for WordIterator. 401 wordIterator = new WordIterator(); 402 } 403 404 int deleteFrom; 405 int deleteTo; 406 407 if (isForwardDelete) { 408 deleteFrom = currentCursorOffset; 409 wordIterator.setCharSequence(content, deleteFrom, content.length()); 410 deleteTo = wordIterator.following(currentCursorOffset); 411 if (deleteTo == BreakIterator.DONE) { 412 deleteTo = content.length(); 413 } 414 } else { 415 deleteTo = currentCursorOffset; 416 wordIterator.setCharSequence(content, 0, deleteTo); 417 deleteFrom = wordIterator.preceding(currentCursorOffset); 418 if (deleteFrom == BreakIterator.DONE) { 419 deleteFrom = 0; 420 } 421 } 422 content.delete(deleteFrom, deleteTo); 423 return true; 424 } 425 deleteSelection(View view, Editable content)426 private boolean deleteSelection(View view, Editable content) { 427 int selectionStart = Selection.getSelectionStart(content); 428 int selectionEnd = Selection.getSelectionEnd(content); 429 if (selectionEnd < selectionStart) { 430 int temp = selectionEnd; 431 selectionEnd = selectionStart; 432 selectionStart = temp; 433 } 434 if (selectionStart != selectionEnd) { 435 content.delete(selectionStart, selectionEnd); 436 return true; 437 } 438 return false; 439 } 440 deleteLineFromCursor(View view, Editable content, boolean forward)441 private boolean deleteLineFromCursor(View view, Editable content, boolean forward) { 442 if (view instanceof TextView) { 443 final int selectionStart = Selection.getSelectionStart(content); 444 final int selectionEnd = Selection.getSelectionEnd(content); 445 final int selectionMin; 446 final int selectionMax; 447 if (selectionStart < selectionEnd) { 448 selectionMin = selectionStart; 449 selectionMax = selectionEnd; 450 } else { 451 selectionMin = selectionEnd; 452 selectionMax = selectionStart; 453 } 454 455 final TextView textView = (TextView) view; 456 final Layout layout = textView.getLayout(); 457 if (layout != null && !textView.isOffsetMappingAvailable()) { 458 final int line = layout.getLineForOffset(Selection.getSelectionStart(content)); 459 final int start = layout.getLineStart(line); 460 final int end = layout.getLineEnd(line); 461 462 if (forward) { 463 content.delete(selectionMin, end); 464 } else { 465 content.delete(start, selectionMax); 466 } 467 468 return true; 469 } 470 } 471 return false; 472 } 473 makeTextContentType(Capitalize caps, boolean autoText)474 static int makeTextContentType(Capitalize caps, boolean autoText) { 475 int contentType = InputType.TYPE_CLASS_TEXT; 476 switch (caps) { 477 case CHARACTERS: 478 contentType |= InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS; 479 break; 480 case WORDS: 481 contentType |= InputType.TYPE_TEXT_FLAG_CAP_WORDS; 482 break; 483 case SENTENCES: 484 contentType |= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES; 485 break; 486 } 487 if (autoText) { 488 contentType |= InputType.TYPE_TEXT_FLAG_AUTO_CORRECT; 489 } 490 return contentType; 491 } 492 onKeyDown(View view, Editable content, int keyCode, KeyEvent event)493 public boolean onKeyDown(View view, Editable content, 494 int keyCode, KeyEvent event) { 495 boolean handled; 496 switch (keyCode) { 497 case KeyEvent.KEYCODE_DEL: 498 handled = backspace(view, content, keyCode, event); 499 break; 500 case KeyEvent.KEYCODE_FORWARD_DEL: 501 handled = forwardDelete(view, content, keyCode, event); 502 break; 503 default: 504 handled = false; 505 break; 506 } 507 508 if (handled) { 509 adjustMetaAfterKeypress(content); 510 return true; 511 } 512 513 return super.onKeyDown(view, content, keyCode, event); 514 } 515 516 /** 517 * Base implementation handles ACTION_MULTIPLE KEYCODE_UNKNOWN by inserting 518 * the event's text into the content. 519 */ onKeyOther(View view, Editable content, KeyEvent event)520 public boolean onKeyOther(View view, Editable content, KeyEvent event) { 521 if (event.getAction() != KeyEvent.ACTION_MULTIPLE 522 || event.getKeyCode() != KeyEvent.KEYCODE_UNKNOWN) { 523 // Not something we are interested in. 524 return false; 525 } 526 527 int selectionStart = Selection.getSelectionStart(content); 528 int selectionEnd = Selection.getSelectionEnd(content); 529 if (selectionEnd < selectionStart) { 530 int temp = selectionEnd; 531 selectionEnd = selectionStart; 532 selectionStart = temp; 533 } 534 535 CharSequence text = event.getCharacters(); 536 if (text == null) { 537 return false; 538 } 539 540 content.replace(selectionStart, selectionEnd, text); 541 return true; 542 } 543 } 544