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.text.AutoText; 20 import android.text.Editable; 21 import android.text.NoCopySpan; 22 import android.text.Selection; 23 import android.text.Spannable; 24 import android.text.TextUtils; 25 import android.text.method.TextKeyListener.Capitalize; 26 import android.util.SparseArray; 27 import android.view.KeyCharacterMap; 28 import android.view.KeyEvent; 29 import android.view.View; 30 31 /** 32 * This is the standard key listener for alphabetic input on qwerty 33 * keyboards. You should generally not need to instantiate this yourself; 34 * TextKeyListener will do it for you. 35 * <p></p> 36 * As for all implementations of {@link KeyListener}, this class is only concerned 37 * with hardware keyboards. Software input methods have no obligation to trigger 38 * the methods in this class. 39 */ 40 @android.ravenwood.annotation.RavenwoodKeepWholeClass 41 public class QwertyKeyListener extends BaseKeyListener { 42 private static QwertyKeyListener[] sInstance = 43 new QwertyKeyListener[Capitalize.values().length * 2]; 44 private static QwertyKeyListener sFullKeyboardInstance; 45 46 private Capitalize mAutoCap; 47 private boolean mAutoText; 48 private boolean mFullKeyboard; 49 QwertyKeyListener(Capitalize cap, boolean autoText, boolean fullKeyboard)50 private QwertyKeyListener(Capitalize cap, boolean autoText, boolean fullKeyboard) { 51 mAutoCap = cap; 52 mAutoText = autoText; 53 mFullKeyboard = fullKeyboard; 54 } 55 QwertyKeyListener(Capitalize cap, boolean autoText)56 public QwertyKeyListener(Capitalize cap, boolean autoText) { 57 this(cap, autoText, false); 58 } 59 60 /** 61 * Returns a new or existing instance with the specified capitalization 62 * and correction properties. 63 */ getInstance(boolean autoText, Capitalize cap)64 public static QwertyKeyListener getInstance(boolean autoText, Capitalize cap) { 65 int off = cap.ordinal() * 2 + (autoText ? 1 : 0); 66 67 if (sInstance[off] == null) { 68 sInstance[off] = new QwertyKeyListener(cap, autoText); 69 } 70 71 return sInstance[off]; 72 } 73 74 /** 75 * Gets an instance of the listener suitable for use with full keyboards. 76 * Disables auto-capitalization, auto-text and long-press initiated on-screen 77 * character pickers. 78 */ getInstanceForFullKeyboard()79 public static QwertyKeyListener getInstanceForFullKeyboard() { 80 if (sFullKeyboardInstance == null) { 81 sFullKeyboardInstance = new QwertyKeyListener(Capitalize.NONE, false, true); 82 } 83 return sFullKeyboardInstance; 84 } 85 getInputType()86 public int getInputType() { 87 return makeTextContentType(mAutoCap, mAutoText); 88 } 89 onKeyDown(View view, Editable content, int keyCode, KeyEvent event)90 public boolean onKeyDown(View view, Editable content, 91 int keyCode, KeyEvent event) { 92 int selStart, selEnd; 93 int pref = 0; 94 95 if (view != null) { 96 pref = TextKeyListener.getInstance().getPrefs(view.getContext()); 97 } 98 99 { 100 int a = Selection.getSelectionStart(content); 101 int b = Selection.getSelectionEnd(content); 102 103 selStart = Math.min(a, b); 104 selEnd = Math.max(a, b); 105 106 if (selStart < 0 || selEnd < 0) { 107 selStart = selEnd = 0; 108 Selection.setSelection(content, 0, 0); 109 } 110 } 111 112 int activeStart = content.getSpanStart(TextKeyListener.ACTIVE); 113 int activeEnd = content.getSpanEnd(TextKeyListener.ACTIVE); 114 115 // QWERTY keyboard normal case 116 117 int i = event.getUnicodeChar(getMetaState(content, event)); 118 119 if (!mFullKeyboard) { 120 int count = event.getRepeatCount(); 121 if (count > 0 && selStart == selEnd && selStart > 0) { 122 char c = content.charAt(selStart - 1); 123 124 if ((c == i || c == Character.toUpperCase(i)) && view != null) { 125 if (showCharacterPicker(view, content, c, false, count)) { 126 resetMetaState(content); 127 return true; 128 } 129 } 130 } 131 } 132 133 if (i == KeyCharacterMap.PICKER_DIALOG_INPUT) { 134 if (view != null) { 135 showCharacterPicker(view, content, 136 KeyCharacterMap.PICKER_DIALOG_INPUT, true, 1); 137 } 138 resetMetaState(content); 139 return true; 140 } 141 142 if (i == KeyCharacterMap.HEX_INPUT) { 143 int start; 144 145 if (selStart == selEnd) { 146 start = selEnd; 147 148 while (start > 0 && selEnd - start < 4 && 149 Character.digit(content.charAt(start - 1), 16) >= 0) { 150 start--; 151 } 152 } else { 153 start = selStart; 154 } 155 156 int ch = -1; 157 try { 158 String hex = TextUtils.substring(content, start, selEnd); 159 ch = Integer.parseInt(hex, 16); 160 } catch (NumberFormatException nfe) { } 161 162 if (ch >= 0) { 163 selStart = start; 164 Selection.setSelection(content, selStart, selEnd); 165 i = ch; 166 } else { 167 i = 0; 168 } 169 } 170 171 if (i != 0) { 172 boolean dead = false; 173 174 if ((i & KeyCharacterMap.COMBINING_ACCENT) != 0) { 175 dead = true; 176 i = i & KeyCharacterMap.COMBINING_ACCENT_MASK; 177 } 178 179 if (activeStart == selStart && activeEnd == selEnd) { 180 boolean replace = false; 181 182 if (selEnd - selStart - 1 == 0) { 183 char accent = content.charAt(selStart); 184 int composed = event.getDeadChar(accent, i); 185 186 // Prevent a dead key repetition from inserting 187 if (i == composed && event.getRepeatCount() > 0) { 188 return true; 189 } 190 191 if (composed != 0) { 192 i = composed; 193 replace = true; 194 dead = false; 195 } 196 } 197 198 if (!replace) { 199 Selection.setSelection(content, selEnd); 200 content.removeSpan(TextKeyListener.ACTIVE); 201 selStart = selEnd; 202 } 203 } 204 205 if ((pref & TextKeyListener.AUTO_CAP) != 0 206 && Character.isLowerCase(i) 207 && TextKeyListener.shouldCap(mAutoCap, content, selStart)) { 208 int where = content.getSpanEnd(TextKeyListener.CAPPED); 209 int flags = content.getSpanFlags(TextKeyListener.CAPPED); 210 211 if (where == selStart && (((flags >> 16) & 0xFFFF) == i)) { 212 content.removeSpan(TextKeyListener.CAPPED); 213 } else { 214 flags = i << 16; 215 i = Character.toUpperCase(i); 216 217 if (selStart == 0) 218 content.setSpan(TextKeyListener.CAPPED, 0, 0, 219 Spannable.SPAN_MARK_MARK | flags); 220 else 221 content.setSpan(TextKeyListener.CAPPED, 222 selStart - 1, selStart, 223 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE | 224 flags); 225 } 226 } 227 228 if (selStart != selEnd) { 229 Selection.setSelection(content, selEnd); 230 } 231 content.setSpan(OLD_SEL_START, selStart, selStart, 232 Spannable.SPAN_MARK_MARK); 233 234 content.replace(selStart, selEnd, String.valueOf((char) i)); 235 236 int oldStart = content.getSpanStart(OLD_SEL_START); 237 selEnd = Selection.getSelectionEnd(content); 238 239 if (oldStart < selEnd) { 240 content.setSpan(TextKeyListener.LAST_TYPED, 241 oldStart, selEnd, 242 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 243 244 if (dead) { 245 Selection.setSelection(content, oldStart, selEnd); 246 content.setSpan(TextKeyListener.ACTIVE, oldStart, selEnd, 247 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 248 } 249 } 250 251 adjustMetaAfterKeypress(content); 252 253 // potentially do autotext replacement if the character 254 // that was typed was an autotext terminator 255 256 if ((pref & TextKeyListener.AUTO_TEXT) != 0 && mAutoText && 257 (i == ' ' || i == '\t' || i == '\n' || 258 i == ',' || i == '.' || i == '!' || i == '?' || 259 i == '"' || Character.getType(i) == Character.END_PUNCTUATION) && 260 content.getSpanEnd(TextKeyListener.INHIBIT_REPLACEMENT) 261 != oldStart) { 262 int x; 263 264 for (x = oldStart; x > 0; x--) { 265 char c = content.charAt(x - 1); 266 if (c != '\'' && !Character.isLetter(c)) { 267 break; 268 } 269 } 270 271 String rep = getReplacement(content, x, oldStart, view); 272 273 if (rep != null) { 274 Replaced[] repl = content.getSpans(0, content.length(), 275 Replaced.class); 276 for (int a = 0; a < repl.length; a++) 277 content.removeSpan(repl[a]); 278 279 char[] orig = new char[oldStart - x]; 280 TextUtils.getChars(content, x, oldStart, orig, 0); 281 282 content.setSpan(new Replaced(orig), x, oldStart, 283 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 284 content.replace(x, oldStart, rep); 285 } 286 } 287 288 // Replace two spaces by a period and a space. 289 290 if ((pref & TextKeyListener.AUTO_PERIOD) != 0 && mAutoText) { 291 selEnd = Selection.getSelectionEnd(content); 292 if (selEnd - 3 >= 0) { 293 if (content.charAt(selEnd - 1) == ' ' && 294 content.charAt(selEnd - 2) == ' ') { 295 char c = content.charAt(selEnd - 3); 296 297 for (int j = selEnd - 3; j > 0; j--) { 298 if (c == '"' || 299 Character.getType(c) == Character.END_PUNCTUATION) { 300 c = content.charAt(j - 1); 301 } else { 302 break; 303 } 304 } 305 306 if (Character.isLetter(c) || Character.isDigit(c)) { 307 content.replace(selEnd - 2, selEnd - 1, "."); 308 } 309 } 310 } 311 } 312 313 return true; 314 } else if (keyCode == KeyEvent.KEYCODE_DEL 315 && (event.hasNoModifiers() || event.hasModifiers(KeyEvent.META_ALT_ON)) 316 && selStart == selEnd) { 317 // special backspace case for undoing autotext 318 319 int consider = 1; 320 321 // if backspacing over the last typed character, 322 // it undoes the autotext prior to that character 323 // (unless the character typed was newline, in which 324 // case this behavior would be confusing) 325 326 if (content.getSpanEnd(TextKeyListener.LAST_TYPED) == selStart) { 327 if (content.charAt(selStart - 1) != '\n') 328 consider = 2; 329 } 330 331 Replaced[] repl = content.getSpans(selStart - consider, selStart, 332 Replaced.class); 333 334 if (repl.length > 0) { 335 int st = content.getSpanStart(repl[0]); 336 int en = content.getSpanEnd(repl[0]); 337 String old = new String(repl[0].mText); 338 339 content.removeSpan(repl[0]); 340 341 // only cancel the autocomplete if the cursor is at the end of 342 // the replaced span (or after it, because the user is 343 // backspacing over the space after the word, not the word 344 // itself). 345 if (selStart >= en) { 346 content.setSpan(TextKeyListener.INHIBIT_REPLACEMENT, 347 en, en, Spannable.SPAN_POINT_POINT); 348 content.replace(st, en, old); 349 350 en = content.getSpanStart(TextKeyListener.INHIBIT_REPLACEMENT); 351 if (en - 1 >= 0) { 352 content.setSpan(TextKeyListener.INHIBIT_REPLACEMENT, 353 en - 1, en, 354 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 355 } else { 356 content.removeSpan(TextKeyListener.INHIBIT_REPLACEMENT); 357 } 358 adjustMetaAfterKeypress(content); 359 } else { 360 adjustMetaAfterKeypress(content); 361 return super.onKeyDown(view, content, keyCode, event); 362 } 363 364 return true; 365 } 366 } else if (keyCode == KeyEvent.KEYCODE_ESCAPE && event.hasNoModifiers()) { 367 // If user is in the process of composing with a dead key, and 368 // presses Escape, cancel it. We need special handling because 369 // the Escape key will not produce a Unicode character 370 if (activeStart == selStart && activeEnd == selEnd) { 371 Selection.setSelection(content, selEnd); 372 content.removeSpan(TextKeyListener.ACTIVE); 373 return true; 374 } 375 } 376 377 return super.onKeyDown(view, content, keyCode, event); 378 } 379 getReplacement(CharSequence src, int start, int end, View view)380 private String getReplacement(CharSequence src, int start, int end, 381 View view) { 382 int len = end - start; 383 boolean changecase = false; 384 385 String replacement = AutoText.get(src, start, end, view); 386 387 if (replacement == null) { 388 String key = TextUtils.substring(src, start, end).toLowerCase(); 389 replacement = AutoText.get(key, 0, end - start, view); 390 changecase = true; 391 392 if (replacement == null) 393 return null; 394 } 395 396 int caps = 0; 397 398 if (changecase) { 399 for (int j = start; j < end; j++) { 400 if (Character.isUpperCase(src.charAt(j))) 401 caps++; 402 } 403 } 404 405 String out; 406 407 if (caps == 0) 408 out = replacement; 409 else if (caps == 1) 410 out = toTitleCase(replacement); 411 else if (caps == len) 412 out = replacement.toUpperCase(); 413 else 414 out = toTitleCase(replacement); 415 416 if (out.length() == len && 417 TextUtils.regionMatches(src, start, out, 0, len)) 418 return null; 419 420 return out; 421 } 422 423 /** 424 * Marks the specified region of <code>content</code> as having 425 * contained <code>original</code> prior to AutoText replacement. 426 * Call this method when you have done or are about to do an 427 * AutoText-style replacement on a region of text and want to let 428 * the same mechanism (the user pressing DEL immediately after the 429 * change) undo the replacement. 430 * 431 * @param content the Editable text where the replacement was made 432 * @param start the start of the replaced region 433 * @param end the end of the replaced region; the location of the cursor 434 * @param original the text to be restored if the user presses DEL 435 */ markAsReplaced(Spannable content, int start, int end, String original)436 public static void markAsReplaced(Spannable content, int start, int end, 437 String original) { 438 Replaced[] repl = content.getSpans(0, content.length(), Replaced.class); 439 for (int a = 0; a < repl.length; a++) { 440 content.removeSpan(repl[a]); 441 } 442 443 int len = original.length(); 444 char[] orig = new char[len]; 445 original.getChars(0, len, orig, 0); 446 447 content.setSpan(new Replaced(orig), start, end, 448 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 449 } 450 451 private static SparseArray<String> PICKER_SETS = 452 new SparseArray<String>(); 453 static { 454 PICKER_SETS.put('A', "\u00C0\u00C1\u00C2\u00C4\u00C6\u00C3\u00C5\u0104\u0100"); 455 PICKER_SETS.put('C', "\u00C7\u0106\u010C"); 456 PICKER_SETS.put('D', "\u010E"); 457 PICKER_SETS.put('E', "\u00C8\u00C9\u00CA\u00CB\u0118\u011A\u0112"); 458 PICKER_SETS.put('G', "\u011E"); 459 PICKER_SETS.put('L', "\u0141"); 460 PICKER_SETS.put('I', "\u00CC\u00CD\u00CE\u00CF\u012A\u0130"); 461 PICKER_SETS.put('N', "\u00D1\u0143\u0147"); 462 PICKER_SETS.put('O', "\u00D8\u0152\u00D5\u00D2\u00D3\u00D4\u00D6\u014C"); 463 PICKER_SETS.put('R', "\u0158"); 464 PICKER_SETS.put('S', "\u015A\u0160\u015E"); 465 PICKER_SETS.put('T', "\u0164"); 466 PICKER_SETS.put('U', "\u00D9\u00DA\u00DB\u00DC\u016E\u016A"); 467 PICKER_SETS.put('Y', "\u00DD\u0178"); 468 PICKER_SETS.put('Z', "\u0179\u017B\u017D"); 469 PICKER_SETS.put('a', "\u00E0\u00E1\u00E2\u00E4\u00E6\u00E3\u00E5\u0105\u0101"); 470 PICKER_SETS.put('c', "\u00E7\u0107\u010D"); 471 PICKER_SETS.put('d', "\u010F"); 472 PICKER_SETS.put('e', "\u00E8\u00E9\u00EA\u00EB\u0119\u011B\u0113"); 473 PICKER_SETS.put('g', "\u011F"); 474 PICKER_SETS.put('i', "\u00EC\u00ED\u00EE\u00EF\u012B\u0131"); 475 PICKER_SETS.put('l', "\u0142"); 476 PICKER_SETS.put('n', "\u00F1\u0144\u0148"); 477 PICKER_SETS.put('o', "\u00F8\u0153\u00F5\u00F2\u00F3\u00F4\u00F6\u014D"); 478 PICKER_SETS.put('r', "\u0159"); 479 PICKER_SETS.put('s', "\u00A7\u00DF\u015B\u0161\u015F"); 480 PICKER_SETS.put('t', "\u0165"); 481 PICKER_SETS.put('u', "\u00F9\u00FA\u00FB\u00FC\u016F\u016B"); 482 PICKER_SETS.put('y', "\u00FD\u00FF"); 483 PICKER_SETS.put('z', "\u017A\u017C\u017E"); PICKER_SETS.put(KeyCharacterMap.PICKER_DIALOG_INPUT, "\\u2026\\u00A5\\u2022\\u00AE\\u00A9\\u00B1[]{}\\\\|")484 PICKER_SETS.put(KeyCharacterMap.PICKER_DIALOG_INPUT, 485 "\u2026\u00A5\u2022\u00AE\u00A9\u00B1[]{}\\|"); 486 PICKER_SETS.put('/', "\\"); 487 488 // From packages/inputmethods/LatinIME/res/xml/kbd_symbols.xml 489 490 PICKER_SETS.put('1', "\u00b9\u00bd\u2153\u00bc\u215b"); 491 PICKER_SETS.put('2', "\u00b2\u2154"); 492 PICKER_SETS.put('3', "\u00b3\u00be\u215c"); 493 PICKER_SETS.put('4', "\u2074"); 494 PICKER_SETS.put('5', "\u215d"); 495 PICKER_SETS.put('7', "\u215e"); 496 PICKER_SETS.put('0', "\u207f\u2205"); 497 PICKER_SETS.put('$', "\u00a2\u00a3\u20ac\u00a5\u20a3\u20a4\u20b1"); 498 PICKER_SETS.put('%', "\u2030"); 499 PICKER_SETS.put('*', "\u2020\u2021"); 500 PICKER_SETS.put('-', "\u2013\u2014"); 501 PICKER_SETS.put('+', "\u00b1"); 502 PICKER_SETS.put('(', "[{<"); 503 PICKER_SETS.put(')', "]}>"); 504 PICKER_SETS.put('!', "\u00a1"); 505 PICKER_SETS.put('"', "\u201c\u201d\u00ab\u00bb\u02dd"); 506 PICKER_SETS.put('?', "\u00bf"); 507 PICKER_SETS.put(',', "\u201a\u201e"); 508 509 // From packages/inputmethods/LatinIME/res/xml/kbd_symbols_shift.xml 510 511 PICKER_SETS.put('=', "\u2260\u2248\u221e"); 512 PICKER_SETS.put('<', "\u2264\u00ab\u2039"); 513 PICKER_SETS.put('>', "\u2265\u00bb\u203a"); 514 }; 515 showCharacterPicker(View view, Editable content, char c, boolean insert, int count)516 private boolean showCharacterPicker(View view, Editable content, char c, 517 boolean insert, int count) { 518 String set = PICKER_SETS.get(c); 519 if (set == null) { 520 return false; 521 } 522 523 if (count == 1) { 524 new CharacterPickerDialog(view.getContext(), 525 view, content, set, insert).show(); 526 } 527 528 return true; 529 } 530 toTitleCase(String src)531 private static String toTitleCase(String src) { 532 return Character.toUpperCase(src.charAt(0)) + src.substring(1); 533 } 534 535 /* package */ static class Replaced implements NoCopySpan 536 { Replaced(char[] text)537 public Replaced(char[] text) { 538 mText = text; 539 } 540 541 private char[] mText; 542 } 543 } 544 545