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