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