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