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