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.os.Handler;
20import android.os.SystemClock;
21import android.text.Editable;
22import android.text.Selection;
23import android.text.SpanWatcher;
24import android.text.Spannable;
25import android.text.method.TextKeyListener.Capitalize;
26import android.util.SparseArray;
27import android.view.KeyEvent;
28import android.view.View;
29
30/**
31 * This is the standard key listener for alphabetic input on 12-key
32 * keyboards.  You should generally not need to instantiate this yourself;
33 * TextKeyListener will do it for you.
34 * <p></p>
35 * As for all implementations of {@link KeyListener}, this class is only concerned
36 * with hardware keyboards.  Software input methods have no obligation to trigger
37 * the methods in this class.
38 */
39public class MultiTapKeyListener extends BaseKeyListener
40        implements SpanWatcher {
41    private static MultiTapKeyListener[] sInstance =
42        new MultiTapKeyListener[Capitalize.values().length * 2];
43
44    private static final SparseArray<String> sRecs = new SparseArray<String>();
45
46    private Capitalize mCapitalize;
47    private boolean mAutoText;
48
49    static {
50        sRecs.put(KeyEvent.KEYCODE_1,     ".,1!@#$%^&*:/?'=()");
51        sRecs.put(KeyEvent.KEYCODE_2,     "abc2ABC");
52        sRecs.put(KeyEvent.KEYCODE_3,     "def3DEF");
53        sRecs.put(KeyEvent.KEYCODE_4,     "ghi4GHI");
54        sRecs.put(KeyEvent.KEYCODE_5,     "jkl5JKL");
55        sRecs.put(KeyEvent.KEYCODE_6,     "mno6MNO");
56        sRecs.put(KeyEvent.KEYCODE_7,     "pqrs7PQRS");
57        sRecs.put(KeyEvent.KEYCODE_8,     "tuv8TUV");
58        sRecs.put(KeyEvent.KEYCODE_9,     "wxyz9WXYZ");
59        sRecs.put(KeyEvent.KEYCODE_0,     "0+");
60        sRecs.put(KeyEvent.KEYCODE_POUND, " ");
61    };
62
63    public MultiTapKeyListener(Capitalize cap,
64                               boolean autotext) {
65        mCapitalize = cap;
66        mAutoText = autotext;
67    }
68
69    /**
70     * Returns a new or existing instance with the specified capitalization
71     * and correction properties.
72     */
73    public static MultiTapKeyListener getInstance(boolean autotext,
74                                                  Capitalize cap) {
75        int off = cap.ordinal() * 2 + (autotext ? 1 : 0);
76
77        if (sInstance[off] == null) {
78            sInstance[off] = new MultiTapKeyListener(cap, autotext);
79        }
80
81        return sInstance[off];
82    }
83
84    public int getInputType() {
85        return makeTextContentType(mCapitalize, mAutoText);
86    }
87
88    public boolean onKeyDown(View view, Editable content,
89                             int keyCode, KeyEvent event) {
90        int selStart, selEnd;
91        int pref = 0;
92
93        if (view != null) {
94            pref = TextKeyListener.getInstance().getPrefs(view.getContext());
95        }
96
97        {
98            int a = Selection.getSelectionStart(content);
99            int b = Selection.getSelectionEnd(content);
100
101            selStart = Math.min(a, b);
102            selEnd = Math.max(a, b);
103        }
104
105        int activeStart = content.getSpanStart(TextKeyListener.ACTIVE);
106        int activeEnd = content.getSpanEnd(TextKeyListener.ACTIVE);
107
108        // now for the multitap cases...
109
110        // Try to increment the character we were working on before
111        // if we have one and it's still the same key.
112
113        int rec = (content.getSpanFlags(TextKeyListener.ACTIVE)
114                    & Spannable.SPAN_USER) >>> Spannable.SPAN_USER_SHIFT;
115
116        if (activeStart == selStart && activeEnd == selEnd &&
117            selEnd - selStart == 1 &&
118            rec >= 0 && rec < sRecs.size()) {
119            if (keyCode == KeyEvent.KEYCODE_STAR) {
120                char current = content.charAt(selStart);
121
122                if (Character.isLowerCase(current)) {
123                    content.replace(selStart, selEnd,
124                                    String.valueOf(current).toUpperCase());
125                    removeTimeouts(content);
126                    new Timeout(content); // for its side effects
127
128                    return true;
129                }
130                if (Character.isUpperCase(current)) {
131                    content.replace(selStart, selEnd,
132                                    String.valueOf(current).toLowerCase());
133                    removeTimeouts(content);
134                    new Timeout(content); // for its side effects
135
136                    return true;
137                }
138            }
139
140            if (sRecs.indexOfKey(keyCode) == rec) {
141                String val = sRecs.valueAt(rec);
142                char ch = content.charAt(selStart);
143                int ix = val.indexOf(ch);
144
145                if (ix >= 0) {
146                    ix = (ix + 1) % (val.length());
147
148                    content.replace(selStart, selEnd, val, ix, ix + 1);
149                    removeTimeouts(content);
150                    new Timeout(content); // for its side effects
151
152                    return true;
153                }
154            }
155
156            // Is this key one we know about at all?  If so, acknowledge
157            // that the selection is our fault but the key has changed
158            // or the text no longer matches, so move the selection over
159            // so that it inserts instead of replaces.
160
161            rec = sRecs.indexOfKey(keyCode);
162
163            if (rec >= 0) {
164                Selection.setSelection(content, selEnd, selEnd);
165                selStart = selEnd;
166            }
167        } else {
168            rec = sRecs.indexOfKey(keyCode);
169        }
170
171        if (rec >= 0) {
172            // We have a valid key.  Replace the selection or insertion point
173            // with the first character for that key, and remember what
174            // record it came from for next time.
175
176            String val = sRecs.valueAt(rec);
177
178            int off = 0;
179            if ((pref & TextKeyListener.AUTO_CAP) != 0 &&
180                TextKeyListener.shouldCap(mCapitalize, content, selStart)) {
181                for (int i = 0; i < val.length(); i++) {
182                    if (Character.isUpperCase(val.charAt(i))) {
183                        off = i;
184                        break;
185                    }
186                }
187            }
188
189            if (selStart != selEnd) {
190                Selection.setSelection(content, selEnd);
191            }
192
193            content.setSpan(OLD_SEL_START, selStart, selStart,
194                            Spannable.SPAN_MARK_MARK);
195
196            content.replace(selStart, selEnd, val, off, off + 1);
197
198            int oldStart = content.getSpanStart(OLD_SEL_START);
199            selEnd = Selection.getSelectionEnd(content);
200
201            if (selEnd != oldStart) {
202                Selection.setSelection(content, oldStart, selEnd);
203
204                content.setSpan(TextKeyListener.LAST_TYPED,
205                                oldStart, selEnd,
206                                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
207
208                content.setSpan(TextKeyListener.ACTIVE,
209                            oldStart, selEnd,
210                            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE |
211                            (rec << Spannable.SPAN_USER_SHIFT));
212
213            }
214
215            removeTimeouts(content);
216            new Timeout(content); // for its side effects
217
218            // Set up the callback so we can remove the timeout if the
219            // cursor moves.
220
221            if (content.getSpanStart(this) < 0) {
222                KeyListener[] methods = content.getSpans(0, content.length(),
223                                                    KeyListener.class);
224                for (Object method : methods) {
225                    content.removeSpan(method);
226                }
227                content.setSpan(this, 0, content.length(),
228                                Spannable.SPAN_INCLUSIVE_INCLUSIVE);
229            }
230
231            return true;
232        }
233
234        return super.onKeyDown(view, content, keyCode, event);
235    }
236
237    public void onSpanChanged(Spannable buf,
238                              Object what, int s, int e, int start, int stop) {
239        if (what == Selection.SELECTION_END) {
240            buf.removeSpan(TextKeyListener.ACTIVE);
241            removeTimeouts(buf);
242        }
243    }
244
245    private static void removeTimeouts(Spannable buf) {
246        Timeout[] timeout = buf.getSpans(0, buf.length(), Timeout.class);
247
248        for (int i = 0; i < timeout.length; i++) {
249            Timeout t = timeout[i];
250
251            t.removeCallbacks(t);
252            t.mBuffer = null;
253            buf.removeSpan(t);
254        }
255    }
256
257    private class Timeout
258    extends Handler
259    implements Runnable
260    {
261        public Timeout(Editable buffer) {
262            mBuffer = buffer;
263            mBuffer.setSpan(Timeout.this, 0, mBuffer.length(),
264                            Spannable.SPAN_INCLUSIVE_INCLUSIVE);
265
266            postAtTime(this, SystemClock.uptimeMillis() + 2000);
267        }
268
269        public void run() {
270            Spannable buf = mBuffer;
271
272            if (buf != null) {
273                int st = Selection.getSelectionStart(buf);
274                int en = Selection.getSelectionEnd(buf);
275
276                int start = buf.getSpanStart(TextKeyListener.ACTIVE);
277                int end = buf.getSpanEnd(TextKeyListener.ACTIVE);
278
279                if (st == start && en == end) {
280                    Selection.setSelection(buf, Selection.getSelectionEnd(buf));
281                }
282
283                buf.removeSpan(Timeout.this);
284            }
285        }
286
287        private Editable mBuffer;
288    }
289
290    public void onSpanAdded(Spannable s, Object what, int start, int end) { }
291    public void onSpanRemoved(Spannable s, Object what, int start, int end) { }
292}
293
294