• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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