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