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.os.Handler;
20 import android.os.SystemClock;
21 import android.text.Editable;
22 import android.text.Selection;
23 import android.text.SpanWatcher;
24 import android.text.Spannable;
25 import android.text.method.TextKeyListener.Capitalize;
26 import android.util.SparseArray;
27 import android.view.KeyEvent;
28 import 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  */
39 public 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 {
sRecs.put(KeyEvent.KEYCODE_1, R)50         sRecs.put(KeyEvent.KEYCODE_1,     ".,1!@#$%^&*:/?'=()");
sRecs.put(KeyEvent.KEYCODE_2, R)51         sRecs.put(KeyEvent.KEYCODE_2,     "abc2ABC");
sRecs.put(KeyEvent.KEYCODE_3, R)52         sRecs.put(KeyEvent.KEYCODE_3,     "def3DEF");
sRecs.put(KeyEvent.KEYCODE_4, R)53         sRecs.put(KeyEvent.KEYCODE_4,     "ghi4GHI");
sRecs.put(KeyEvent.KEYCODE_5, R)54         sRecs.put(KeyEvent.KEYCODE_5,     "jkl5JKL");
sRecs.put(KeyEvent.KEYCODE_6, R)55         sRecs.put(KeyEvent.KEYCODE_6,     "mno6MNO");
sRecs.put(KeyEvent.KEYCODE_7, R)56         sRecs.put(KeyEvent.KEYCODE_7,     "pqrs7PQRS");
sRecs.put(KeyEvent.KEYCODE_8, R)57         sRecs.put(KeyEvent.KEYCODE_8,     "tuv8TUV");
sRecs.put(KeyEvent.KEYCODE_9, R)58         sRecs.put(KeyEvent.KEYCODE_9,     "wxyz9WXYZ");
sRecs.put(KeyEvent.KEYCODE_0, R)59         sRecs.put(KeyEvent.KEYCODE_0,     "0+");
sRecs.put(KeyEvent.KEYCODE_POUND, R)60         sRecs.put(KeyEvent.KEYCODE_POUND, " ");
61     };
62 
MultiTapKeyListener(Capitalize cap, boolean autotext)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      */
getInstance(boolean autotext, Capitalize cap)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 
getInputType()84     public int getInputType() {
85         return makeTextContentType(mCapitalize, mAutoText);
86     }
87 
onKeyDown(View view, Editable content, int keyCode, KeyEvent event)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 
onSpanChanged(Spannable buf, Object what, int s, int e, int start, int stop)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 
removeTimeouts(Spannable buf)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     {
Timeout(Editable buffer)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 
run()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 
onSpanAdded(Spannable s, Object what, int start, int end)290     public void onSpanAdded(Spannable s, Object what, int start, int end) { }
onSpanRemoved(Spannable s, Object what, int start, int end)291     public void onSpanRemoved(Spannable s, Object what, int start, int end) { }
292 }
293 
294