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 } 233 state = STATE_FINISHED; 234 break; 235 case STATE_BEFORE_VS_AND_EMOJI_MODIFIER: 236 if (Emoji.isEmojiModifierBase(codePoint)) { 237 deleteCharCount += lastSeenVSCharCount + Character.charCount(codePoint); 238 } 239 state = STATE_FINISHED; 240 break; 241 case STATE_BEFORE_VS: 242 if (Emoji.isEmoji(codePoint)) { 243 deleteCharCount += Character.charCount(codePoint); 244 state = STATE_BEFORE_EMOJI; 245 break; 246 } 247 248 if (!isVariationSelector(codePoint) && 249 UCharacter.getCombiningClass(codePoint) == 0) { 250 deleteCharCount += Character.charCount(codePoint); 251 } 252 state = STATE_FINISHED; 253 break; 254 case STATE_BEFORE_EMOJI: 255 if (codePoint == Emoji.ZERO_WIDTH_JOINER) { 256 state = STATE_BEFORE_ZWJ; 257 } else { 258 state = STATE_FINISHED; 259 } 260 break; 261 case STATE_BEFORE_ZWJ: 262 if (Emoji.isEmoji(codePoint)) { 263 deleteCharCount += Character.charCount(codePoint) + 1; // +1 for ZWJ. 264 state = Emoji.isEmojiModifier(codePoint) ? 265 STATE_BEFORE_EMOJI_MODIFIER : STATE_BEFORE_EMOJI; 266 } else if (isVariationSelector(codePoint)) { 267 lastSeenVSCharCount = Character.charCount(codePoint); 268 state = STATE_BEFORE_VS_AND_ZWJ; 269 } else { 270 state = STATE_FINISHED; 271 } 272 break; 273 case STATE_BEFORE_VS_AND_ZWJ: 274 if (Emoji.isEmoji(codePoint)) { 275 // +1 for ZWJ. 276 deleteCharCount += lastSeenVSCharCount + 1 + Character.charCount(codePoint); 277 lastSeenVSCharCount = 0; 278 state = STATE_BEFORE_EMOJI; 279 } else { 280 state = STATE_FINISHED; 281 } 282 break; 283 case STATE_IN_TAG_SEQUENCE: 284 if (Emoji.isTagSpecChar(codePoint)) { 285 deleteCharCount += 2; /* Char count of emoji tag spec character. */ 286 // Keep the same state. 287 } else if (Emoji.isEmoji(codePoint)) { 288 deleteCharCount += Character.charCount(codePoint); 289 state = STATE_FINISHED; 290 } else { 291 // Couldn't find tag_base character. Delete the last tag_term character. 292 deleteCharCount = 2; // for U+E007F 293 state = STATE_FINISHED; 294 } 295 // TODO: Need handle emoji variation selectors. Issue 35224297 296 break; 297 default: 298 throw new IllegalArgumentException("state " + state + " is unknown"); 299 } 300 } while (tmpOffset > 0 && state != STATE_FINISHED); 301 302 return adjustReplacementSpan(text, offset - deleteCharCount, true /* move to the start */); 303 } 304 305 // Returns the end offset to be deleted by a forward delete key from the given offset. getOffsetForForwardDeleteKey(CharSequence text, int offset, Paint paint)306 private static int getOffsetForForwardDeleteKey(CharSequence text, int offset, Paint paint) { 307 final int len = text.length(); 308 309 if (offset >= len - 1) { 310 return len; 311 } 312 313 offset = paint.getTextRunCursor(text, offset, len, false /* LTR, not used */, 314 offset, Paint.CURSOR_AFTER); 315 316 return adjustReplacementSpan(text, offset, false /* move to the end */); 317 } 318 backspaceOrForwardDelete(View view, Editable content, int keyCode, KeyEvent event, boolean isForwardDelete)319 private boolean backspaceOrForwardDelete(View view, Editable content, int keyCode, 320 KeyEvent event, boolean isForwardDelete) { 321 // Ensure the key event does not have modifiers except ALT or SHIFT or CTRL. 322 if (!KeyEvent.metaStateHasNoModifiers(event.getMetaState() 323 & ~(KeyEvent.META_SHIFT_MASK | KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK))) { 324 return false; 325 } 326 327 // If there is a current selection, delete it. 328 if (deleteSelection(view, content)) { 329 return true; 330 } 331 332 // MetaKeyKeyListener doesn't track control key state. Need to check the KeyEvent instead. 333 boolean isCtrlActive = ((event.getMetaState() & KeyEvent.META_CTRL_ON) != 0); 334 boolean isShiftActive = (getMetaState(content, META_SHIFT_ON, event) == 1); 335 boolean isAltActive = (getMetaState(content, META_ALT_ON, event) == 1); 336 337 if (isCtrlActive) { 338 if (isAltActive || isShiftActive) { 339 // Ctrl+Alt, Ctrl+Shift, Ctrl+Alt+Shift should not delete any characters. 340 return false; 341 } 342 return deleteUntilWordBoundary(view, content, isForwardDelete); 343 } 344 345 // Alt+Backspace or Alt+ForwardDelete deletes the current line, if possible. 346 if (isAltActive && deleteLine(view, content)) { 347 return true; 348 } 349 350 // Delete a character. 351 final int start = Selection.getSelectionEnd(content); 352 final int end; 353 if (isForwardDelete) { 354 final Paint paint; 355 if (view instanceof TextView) { 356 paint = ((TextView)view).getPaint(); 357 } else { 358 synchronized (mLock) { 359 if (sCachedPaint == null) { 360 sCachedPaint = new Paint(); 361 } 362 paint = sCachedPaint; 363 } 364 } 365 end = getOffsetForForwardDeleteKey(content, start, paint); 366 } else { 367 end = getOffsetForBackspaceKey(content, start); 368 } 369 if (start != end) { 370 content.delete(Math.min(start, end), Math.max(start, end)); 371 return true; 372 } 373 return false; 374 } 375 deleteUntilWordBoundary(View view, Editable content, boolean isForwardDelete)376 private boolean deleteUntilWordBoundary(View view, Editable content, boolean isForwardDelete) { 377 int currentCursorOffset = Selection.getSelectionStart(content); 378 379 // If there is a selection, do nothing. 380 if (currentCursorOffset != Selection.getSelectionEnd(content)) { 381 return false; 382 } 383 384 // Early exit if there is no contents to delete. 385 if ((!isForwardDelete && currentCursorOffset == 0) || 386 (isForwardDelete && currentCursorOffset == content.length())) { 387 return false; 388 } 389 390 WordIterator wordIterator = null; 391 if (view instanceof TextView) { 392 wordIterator = ((TextView)view).getWordIterator(); 393 } 394 395 if (wordIterator == null) { 396 // Default locale is used for WordIterator since the appropriate locale is not clear 397 // here. 398 // TODO: Use appropriate locale for WordIterator. 399 wordIterator = new WordIterator(); 400 } 401 402 int deleteFrom; 403 int deleteTo; 404 405 if (isForwardDelete) { 406 deleteFrom = currentCursorOffset; 407 wordIterator.setCharSequence(content, deleteFrom, content.length()); 408 deleteTo = wordIterator.following(currentCursorOffset); 409 if (deleteTo == BreakIterator.DONE) { 410 deleteTo = content.length(); 411 } 412 } else { 413 deleteTo = currentCursorOffset; 414 wordIterator.setCharSequence(content, 0, deleteTo); 415 deleteFrom = wordIterator.preceding(currentCursorOffset); 416 if (deleteFrom == BreakIterator.DONE) { 417 deleteFrom = 0; 418 } 419 } 420 content.delete(deleteFrom, deleteTo); 421 return true; 422 } 423 deleteSelection(View view, Editable content)424 private boolean deleteSelection(View view, Editable content) { 425 int selectionStart = Selection.getSelectionStart(content); 426 int selectionEnd = Selection.getSelectionEnd(content); 427 if (selectionEnd < selectionStart) { 428 int temp = selectionEnd; 429 selectionEnd = selectionStart; 430 selectionStart = temp; 431 } 432 if (selectionStart != selectionEnd) { 433 content.delete(selectionStart, selectionEnd); 434 return true; 435 } 436 return false; 437 } 438 deleteLine(View view, Editable content)439 private boolean deleteLine(View view, Editable content) { 440 if (view instanceof TextView) { 441 final Layout layout = ((TextView) view).getLayout(); 442 if (layout != null) { 443 final int line = layout.getLineForOffset(Selection.getSelectionStart(content)); 444 final int start = layout.getLineStart(line); 445 final int end = layout.getLineEnd(line); 446 if (end != start) { 447 content.delete(start, end); 448 return true; 449 } 450 } 451 } 452 return false; 453 } 454 makeTextContentType(Capitalize caps, boolean autoText)455 static int makeTextContentType(Capitalize caps, boolean autoText) { 456 int contentType = InputType.TYPE_CLASS_TEXT; 457 switch (caps) { 458 case CHARACTERS: 459 contentType |= InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS; 460 break; 461 case WORDS: 462 contentType |= InputType.TYPE_TEXT_FLAG_CAP_WORDS; 463 break; 464 case SENTENCES: 465 contentType |= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES; 466 break; 467 } 468 if (autoText) { 469 contentType |= InputType.TYPE_TEXT_FLAG_AUTO_CORRECT; 470 } 471 return contentType; 472 } 473 onKeyDown(View view, Editable content, int keyCode, KeyEvent event)474 public boolean onKeyDown(View view, Editable content, 475 int keyCode, KeyEvent event) { 476 boolean handled; 477 switch (keyCode) { 478 case KeyEvent.KEYCODE_DEL: 479 handled = backspace(view, content, keyCode, event); 480 break; 481 case KeyEvent.KEYCODE_FORWARD_DEL: 482 handled = forwardDelete(view, content, keyCode, event); 483 break; 484 default: 485 handled = false; 486 break; 487 } 488 489 if (handled) { 490 adjustMetaAfterKeypress(content); 491 return true; 492 } 493 494 return super.onKeyDown(view, content, keyCode, event); 495 } 496 497 /** 498 * Base implementation handles ACTION_MULTIPLE KEYCODE_UNKNOWN by inserting 499 * the event's text into the content. 500 */ onKeyOther(View view, Editable content, KeyEvent event)501 public boolean onKeyOther(View view, Editable content, KeyEvent event) { 502 if (event.getAction() != KeyEvent.ACTION_MULTIPLE 503 || event.getKeyCode() != KeyEvent.KEYCODE_UNKNOWN) { 504 // Not something we are interested in. 505 return false; 506 } 507 508 int selectionStart = Selection.getSelectionStart(content); 509 int selectionEnd = Selection.getSelectionEnd(content); 510 if (selectionEnd < selectionStart) { 511 int temp = selectionEnd; 512 selectionEnd = selectionStart; 513 selectionStart = temp; 514 } 515 516 CharSequence text = event.getCharacters(); 517 if (text == null) { 518 return false; 519 } 520 521 content.replace(selectionStart, selectionEnd, text); 522 return true; 523 } 524 } 525