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