• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 Google Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5  * use this file except in compliance with the License. You may obtain a copy of
6  * 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, WITHOUT
12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13  * License for the specific language governing permissions and limitations under
14  * the License.
15  */
16 
17 package com.android.inputmethod.latin;
18 
19 import com.android.inputmethod.latin.LatinKeyboardBaseView.OnKeyboardActionListener;
20 import com.android.inputmethod.latin.LatinKeyboardBaseView.UIHandler;
21 
22 import android.content.res.Resources;
23 import android.inputmethodservice.Keyboard;
24 import android.inputmethodservice.Keyboard.Key;
25 import android.util.Log;
26 import android.view.MotionEvent;
27 
28 public class PointerTracker {
29     private static final String TAG = "PointerTracker";
30     private static final boolean DEBUG = false;
31     private static final boolean DEBUG_MOVE = false;
32 
33     public interface UIProxy {
invalidateKey(Key key)34         public void invalidateKey(Key key);
showPreview(int keyIndex, PointerTracker tracker)35         public void showPreview(int keyIndex, PointerTracker tracker);
hasDistinctMultitouch()36         public boolean hasDistinctMultitouch();
37     }
38 
39     public final int mPointerId;
40 
41     // Timing constants
42     private final int mDelayBeforeKeyRepeatStart;
43     private final int mLongPressKeyTimeout;
44     private final int mMultiTapKeyTimeout;
45 
46     // Miscellaneous constants
47     private static final int NOT_A_KEY = LatinKeyboardBaseView.NOT_A_KEY;
48     private static final int[] KEY_DELETE = { Keyboard.KEYCODE_DELETE };
49 
50     private final UIProxy mProxy;
51     private final UIHandler mHandler;
52     private final KeyDetector mKeyDetector;
53     private OnKeyboardActionListener mListener;
54     private final KeyboardSwitcher mKeyboardSwitcher;
55     private final boolean mHasDistinctMultitouch;
56 
57     private Key[] mKeys;
58     private int mKeyHysteresisDistanceSquared = -1;
59 
60     private final KeyState mKeyState;
61 
62     // true if keyboard layout has been changed.
63     private boolean mKeyboardLayoutHasBeenChanged;
64 
65     // true if event is already translated to a key action (long press or mini-keyboard)
66     private boolean mKeyAlreadyProcessed;
67 
68     // true if this pointer is repeatable key
69     private boolean mIsRepeatableKey;
70 
71     // true if this pointer is in sliding key input
72     private boolean mIsInSlidingKeyInput;
73 
74     // For multi-tap
75     private int mLastSentIndex;
76     private int mTapCount;
77     private long mLastTapTime;
78     private boolean mInMultiTap;
79     private final StringBuilder mPreviewLabel = new StringBuilder(1);
80 
81     // pressed key
82     private int mPreviousKey = NOT_A_KEY;
83 
84     // This class keeps track of a key index and a position where this pointer is.
85     private static class KeyState {
86         private final KeyDetector mKeyDetector;
87 
88         // The position and time at which first down event occurred.
89         private int mStartX;
90         private int mStartY;
91         private long mDownTime;
92 
93         // The current key index where this pointer is.
94         private int mKeyIndex = NOT_A_KEY;
95         // The position where mKeyIndex was recognized for the first time.
96         private int mKeyX;
97         private int mKeyY;
98 
99         // Last pointer position.
100         private int mLastX;
101         private int mLastY;
102 
KeyState(KeyDetector keyDetecor)103         public KeyState(KeyDetector keyDetecor) {
104             mKeyDetector = keyDetecor;
105         }
106 
getKeyIndex()107         public int getKeyIndex() {
108             return mKeyIndex;
109         }
110 
getKeyX()111         public int getKeyX() {
112             return mKeyX;
113         }
114 
getKeyY()115         public int getKeyY() {
116             return mKeyY;
117         }
118 
getStartX()119         public int getStartX() {
120             return mStartX;
121         }
122 
getStartY()123         public int getStartY() {
124             return mStartY;
125         }
126 
getDownTime()127         public long getDownTime() {
128             return mDownTime;
129         }
130 
getLastX()131         public int getLastX() {
132             return mLastX;
133         }
134 
getLastY()135         public int getLastY() {
136             return mLastY;
137         }
138 
onDownKey(int x, int y, long eventTime)139         public int onDownKey(int x, int y, long eventTime) {
140             mStartX = x;
141             mStartY = y;
142             mDownTime = eventTime;
143 
144             return onMoveToNewKey(onMoveKeyInternal(x, y), x, y);
145         }
146 
onMoveKeyInternal(int x, int y)147         private int onMoveKeyInternal(int x, int y) {
148             mLastX = x;
149             mLastY = y;
150             return mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null);
151         }
152 
onMoveKey(int x, int y)153         public int onMoveKey(int x, int y) {
154             return onMoveKeyInternal(x, y);
155         }
156 
onMoveToNewKey(int keyIndex, int x, int y)157         public int onMoveToNewKey(int keyIndex, int x, int y) {
158             mKeyIndex = keyIndex;
159             mKeyX = x;
160             mKeyY = y;
161             return keyIndex;
162         }
163 
onUpKey(int x, int y)164         public int onUpKey(int x, int y) {
165             return onMoveKeyInternal(x, y);
166         }
167     }
168 
PointerTracker(int id, UIHandler handler, KeyDetector keyDetector, UIProxy proxy, Resources res)169     public PointerTracker(int id, UIHandler handler, KeyDetector keyDetector, UIProxy proxy,
170             Resources res) {
171         if (proxy == null || handler == null || keyDetector == null)
172             throw new NullPointerException();
173         mPointerId = id;
174         mProxy = proxy;
175         mHandler = handler;
176         mKeyDetector = keyDetector;
177         mKeyboardSwitcher = KeyboardSwitcher.getInstance();
178         mKeyState = new KeyState(keyDetector);
179         mHasDistinctMultitouch = proxy.hasDistinctMultitouch();
180         mDelayBeforeKeyRepeatStart = res.getInteger(R.integer.config_delay_before_key_repeat_start);
181         mLongPressKeyTimeout = res.getInteger(R.integer.config_long_press_key_timeout);
182         mMultiTapKeyTimeout = res.getInteger(R.integer.config_multi_tap_key_timeout);
183         resetMultiTap();
184     }
185 
setOnKeyboardActionListener(OnKeyboardActionListener listener)186     public void setOnKeyboardActionListener(OnKeyboardActionListener listener) {
187         mListener = listener;
188     }
189 
setKeyboard(Key[] keys, float keyHysteresisDistance)190     public void setKeyboard(Key[] keys, float keyHysteresisDistance) {
191         if (keys == null || keyHysteresisDistance < 0)
192             throw new IllegalArgumentException();
193         mKeys = keys;
194         mKeyHysteresisDistanceSquared = (int)(keyHysteresisDistance * keyHysteresisDistance);
195         // Mark that keyboard layout has been changed.
196         mKeyboardLayoutHasBeenChanged = true;
197     }
198 
isInSlidingKeyInput()199     public boolean isInSlidingKeyInput() {
200         return mIsInSlidingKeyInput;
201     }
202 
isValidKeyIndex(int keyIndex)203     private boolean isValidKeyIndex(int keyIndex) {
204         return keyIndex >= 0 && keyIndex < mKeys.length;
205     }
206 
getKey(int keyIndex)207     public Key getKey(int keyIndex) {
208         return isValidKeyIndex(keyIndex) ? mKeys[keyIndex] : null;
209     }
210 
isModifierInternal(int keyIndex)211     private boolean isModifierInternal(int keyIndex) {
212         Key key = getKey(keyIndex);
213         if (key == null)
214             return false;
215         int primaryCode = key.codes[0];
216         return primaryCode == Keyboard.KEYCODE_SHIFT
217                 || primaryCode == Keyboard.KEYCODE_MODE_CHANGE;
218     }
219 
isModifier()220     public boolean isModifier() {
221         return isModifierInternal(mKeyState.getKeyIndex());
222     }
223 
isOnModifierKey(int x, int y)224     public boolean isOnModifierKey(int x, int y) {
225         return isModifierInternal(mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null));
226     }
227 
isSpaceKey(int keyIndex)228     public boolean isSpaceKey(int keyIndex) {
229         Key key = getKey(keyIndex);
230         return key != null && key.codes[0] == LatinIME.KEYCODE_SPACE;
231     }
232 
updateKey(int keyIndex)233     public void updateKey(int keyIndex) {
234         if (mKeyAlreadyProcessed)
235             return;
236         int oldKeyIndex = mPreviousKey;
237         mPreviousKey = keyIndex;
238         if (keyIndex != oldKeyIndex) {
239             if (isValidKeyIndex(oldKeyIndex)) {
240                 // if new key index is not a key, old key was just released inside of the key.
241                 final boolean inside = (keyIndex == NOT_A_KEY);
242                 mKeys[oldKeyIndex].onReleased(inside);
243                 mProxy.invalidateKey(mKeys[oldKeyIndex]);
244             }
245             if (isValidKeyIndex(keyIndex)) {
246                 mKeys[keyIndex].onPressed();
247                 mProxy.invalidateKey(mKeys[keyIndex]);
248             }
249         }
250     }
251 
setAlreadyProcessed()252     public void setAlreadyProcessed() {
253         mKeyAlreadyProcessed = true;
254     }
255 
onTouchEvent(int action, int x, int y, long eventTime)256     public void onTouchEvent(int action, int x, int y, long eventTime) {
257         switch (action) {
258         case MotionEvent.ACTION_MOVE:
259             onMoveEvent(x, y, eventTime);
260             break;
261         case MotionEvent.ACTION_DOWN:
262         case MotionEvent.ACTION_POINTER_DOWN:
263             onDownEvent(x, y, eventTime);
264             break;
265         case MotionEvent.ACTION_UP:
266         case MotionEvent.ACTION_POINTER_UP:
267             onUpEvent(x, y, eventTime);
268             break;
269         case MotionEvent.ACTION_CANCEL:
270             onCancelEvent(x, y, eventTime);
271             break;
272         }
273     }
274 
onDownEvent(int x, int y, long eventTime)275     public void onDownEvent(int x, int y, long eventTime) {
276         if (DEBUG)
277             debugLog("onDownEvent:", x, y);
278         int keyIndex = mKeyState.onDownKey(x, y, eventTime);
279         mKeyboardLayoutHasBeenChanged = false;
280         mKeyAlreadyProcessed = false;
281         mIsRepeatableKey = false;
282         mIsInSlidingKeyInput = false;
283         checkMultiTap(eventTime, keyIndex);
284         if (mListener != null) {
285             if (isValidKeyIndex(keyIndex)) {
286                 mListener.onPress(mKeys[keyIndex].codes[0]);
287                 // This onPress call may have changed keyboard layout. Those cases are detected at
288                 // {@link #setKeyboard}. In those cases, we should update keyIndex according to the
289                 // new keyboard layout.
290                 if (mKeyboardLayoutHasBeenChanged) {
291                     mKeyboardLayoutHasBeenChanged = false;
292                     keyIndex = mKeyState.onDownKey(x, y, eventTime);
293                 }
294             }
295         }
296         if (isValidKeyIndex(keyIndex)) {
297             if (mKeys[keyIndex].repeatable) {
298                 repeatKey(keyIndex);
299                 mHandler.startKeyRepeatTimer(mDelayBeforeKeyRepeatStart, keyIndex, this);
300                 mIsRepeatableKey = true;
301             }
302             startLongPressTimer(keyIndex);
303         }
304         showKeyPreviewAndUpdateKey(keyIndex);
305     }
306 
onMoveEvent(int x, int y, long eventTime)307     public void onMoveEvent(int x, int y, long eventTime) {
308         if (DEBUG_MOVE)
309             debugLog("onMoveEvent:", x, y);
310         if (mKeyAlreadyProcessed)
311             return;
312         final KeyState keyState = mKeyState;
313         int keyIndex = keyState.onMoveKey(x, y);
314         final Key oldKey = getKey(keyState.getKeyIndex());
315         if (isValidKeyIndex(keyIndex)) {
316             if (oldKey == null) {
317                 // The pointer has been slid in to the new key, but the finger was not on any keys.
318                 // In this case, we must call onPress() to notify that the new key is being pressed.
319                 if (mListener != null) {
320                     mListener.onPress(getKey(keyIndex).codes[0]);
321                     // This onPress call may have changed keyboard layout. Those cases are detected
322                     // at {@link #setKeyboard}. In those cases, we should update keyIndex according
323                     // to the new keyboard layout.
324                     if (mKeyboardLayoutHasBeenChanged) {
325                         mKeyboardLayoutHasBeenChanged = false;
326                         keyIndex = keyState.onMoveKey(x, y);
327                     }
328                 }
329                 keyState.onMoveToNewKey(keyIndex, x, y);
330                 startLongPressTimer(keyIndex);
331             } else if (!isMinorMoveBounce(x, y, keyIndex)) {
332                 // The pointer has been slid in to the new key from the previous key, we must call
333                 // onRelease() first to notify that the previous key has been released, then call
334                 // onPress() to notify that the new key is being pressed.
335                 mIsInSlidingKeyInput = true;
336                 if (mListener != null)
337                     mListener.onRelease(oldKey.codes[0]);
338                 resetMultiTap();
339                 if (mListener != null) {
340                     mListener.onPress(getKey(keyIndex).codes[0]);
341                     // This onPress call may have changed keyboard layout. Those cases are detected
342                     // at {@link #setKeyboard}. In those cases, we should update keyIndex according
343                     // to the new keyboard layout.
344                     if (mKeyboardLayoutHasBeenChanged) {
345                         mKeyboardLayoutHasBeenChanged = false;
346                         keyIndex = keyState.onMoveKey(x, y);
347                     }
348                 }
349                 keyState.onMoveToNewKey(keyIndex, x, y);
350                 startLongPressTimer(keyIndex);
351             }
352         } else {
353             if (oldKey != null && !isMinorMoveBounce(x, y, keyIndex)) {
354                 // The pointer has been slid out from the previous key, we must call onRelease() to
355                 // notify that the previous key has been released.
356                 mIsInSlidingKeyInput = true;
357                 if (mListener != null)
358                     mListener.onRelease(oldKey.codes[0]);
359                 resetMultiTap();
360                 keyState.onMoveToNewKey(keyIndex, x ,y);
361                 mHandler.cancelLongPressTimer();
362             }
363         }
364         showKeyPreviewAndUpdateKey(keyState.getKeyIndex());
365     }
366 
onUpEvent(int x, int y, long eventTime)367     public void onUpEvent(int x, int y, long eventTime) {
368         if (DEBUG)
369             debugLog("onUpEvent  :", x, y);
370         mHandler.cancelKeyTimers();
371         mHandler.cancelPopupPreview();
372         showKeyPreviewAndUpdateKey(NOT_A_KEY);
373         mIsInSlidingKeyInput = false;
374         if (mKeyAlreadyProcessed)
375             return;
376         int keyIndex = mKeyState.onUpKey(x, y);
377         if (isMinorMoveBounce(x, y, keyIndex)) {
378             // Use previous fixed key index and coordinates.
379             keyIndex = mKeyState.getKeyIndex();
380             x = mKeyState.getKeyX();
381             y = mKeyState.getKeyY();
382         }
383         if (!mIsRepeatableKey) {
384             detectAndSendKey(keyIndex, x, y, eventTime);
385         }
386 
387         if (isValidKeyIndex(keyIndex))
388             mProxy.invalidateKey(mKeys[keyIndex]);
389     }
390 
onCancelEvent(int x, int y, long eventTime)391     public void onCancelEvent(int x, int y, long eventTime) {
392         if (DEBUG)
393             debugLog("onCancelEvt:", x, y);
394         mHandler.cancelKeyTimers();
395         mHandler.cancelPopupPreview();
396         showKeyPreviewAndUpdateKey(NOT_A_KEY);
397         mIsInSlidingKeyInput = false;
398         int keyIndex = mKeyState.getKeyIndex();
399         if (isValidKeyIndex(keyIndex))
400            mProxy.invalidateKey(mKeys[keyIndex]);
401     }
402 
repeatKey(int keyIndex)403     public void repeatKey(int keyIndex) {
404         Key key = getKey(keyIndex);
405         if (key != null) {
406             // While key is repeating, because there is no need to handle multi-tap key, we can
407             // pass -1 as eventTime argument.
408             detectAndSendKey(keyIndex, key.x, key.y, -1);
409         }
410     }
411 
getLastX()412     public int getLastX() {
413         return mKeyState.getLastX();
414     }
415 
getLastY()416     public int getLastY() {
417         return mKeyState.getLastY();
418     }
419 
getDownTime()420     public long getDownTime() {
421         return mKeyState.getDownTime();
422     }
423 
424     // These package scope methods are only for debugging purpose.
getStartX()425     /* package */ int getStartX() {
426         return mKeyState.getStartX();
427     }
428 
getStartY()429     /* package */ int getStartY() {
430         return mKeyState.getStartY();
431     }
432 
isMinorMoveBounce(int x, int y, int newKey)433     private boolean isMinorMoveBounce(int x, int y, int newKey) {
434         if (mKeys == null || mKeyHysteresisDistanceSquared < 0)
435             throw new IllegalStateException("keyboard and/or hysteresis not set");
436         int curKey = mKeyState.getKeyIndex();
437         if (newKey == curKey) {
438             return true;
439         } else if (isValidKeyIndex(curKey)) {
440             return getSquareDistanceToKeyEdge(x, y, mKeys[curKey]) < mKeyHysteresisDistanceSquared;
441         } else {
442             return false;
443         }
444     }
445 
getSquareDistanceToKeyEdge(int x, int y, Key key)446     private static int getSquareDistanceToKeyEdge(int x, int y, Key key) {
447         final int left = key.x;
448         final int right = key.x + key.width;
449         final int top = key.y;
450         final int bottom = key.y + key.height;
451         final int edgeX = x < left ? left : (x > right ? right : x);
452         final int edgeY = y < top ? top : (y > bottom ? bottom : y);
453         final int dx = x - edgeX;
454         final int dy = y - edgeY;
455         return dx * dx + dy * dy;
456     }
457 
showKeyPreviewAndUpdateKey(int keyIndex)458     private void showKeyPreviewAndUpdateKey(int keyIndex) {
459         updateKey(keyIndex);
460         // The modifier key, such as shift key, should not be shown as preview when multi-touch is
461         // supported. On the other hand, if multi-touch is not supported, the modifier key should
462         // be shown as preview.
463         if (mHasDistinctMultitouch && isModifier()) {
464             mProxy.showPreview(NOT_A_KEY, this);
465         } else {
466             mProxy.showPreview(keyIndex, this);
467         }
468     }
469 
startLongPressTimer(int keyIndex)470     private void startLongPressTimer(int keyIndex) {
471         if (mKeyboardSwitcher.isInMomentaryAutoModeSwitchState()) {
472             // We use longer timeout for sliding finger input started from the symbols mode key.
473             mHandler.startLongPressTimer(mLongPressKeyTimeout * 3, keyIndex, this);
474         } else {
475             mHandler.startLongPressTimer(mLongPressKeyTimeout, keyIndex, this);
476         }
477     }
478 
detectAndSendKey(int index, int x, int y, long eventTime)479     private void detectAndSendKey(int index, int x, int y, long eventTime) {
480         final OnKeyboardActionListener listener = mListener;
481         final Key key = getKey(index);
482 
483         if (key == null) {
484             if (listener != null)
485                 listener.onCancel();
486         } else {
487             if (key.text != null) {
488                 if (listener != null) {
489                     listener.onText(key.text);
490                     listener.onRelease(0); // dummy key code
491                 }
492             } else {
493                 int code = key.codes[0];
494                 int[] codes = mKeyDetector.newCodeArray();
495                 mKeyDetector.getKeyIndexAndNearbyCodes(x, y, codes);
496                 // Multi-tap
497                 if (mInMultiTap) {
498                     if (mTapCount != -1) {
499                         mListener.onKey(Keyboard.KEYCODE_DELETE, KEY_DELETE, x, y);
500                     } else {
501                         mTapCount = 0;
502                     }
503                     code = key.codes[mTapCount];
504                 }
505                 /*
506                  * Swap the first and second values in the codes array if the primary code is not
507                  * the first value but the second value in the array. This happens when key
508                  * debouncing is in effect.
509                  */
510                 if (codes.length >= 2 && codes[0] != code && codes[1] == code) {
511                     codes[1] = codes[0];
512                     codes[0] = code;
513                 }
514                 if (listener != null) {
515                     listener.onKey(code, codes, x, y);
516                     listener.onRelease(code);
517                 }
518             }
519             mLastSentIndex = index;
520             mLastTapTime = eventTime;
521         }
522     }
523 
524     /**
525      * Handle multi-tap keys by producing the key label for the current multi-tap state.
526      */
getPreviewText(Key key)527     public CharSequence getPreviewText(Key key) {
528         if (mInMultiTap) {
529             // Multi-tap
530             mPreviewLabel.setLength(0);
531             mPreviewLabel.append((char) key.codes[mTapCount < 0 ? 0 : mTapCount]);
532             return mPreviewLabel;
533         } else {
534             return key.label;
535         }
536     }
537 
resetMultiTap()538     private void resetMultiTap() {
539         mLastSentIndex = NOT_A_KEY;
540         mTapCount = 0;
541         mLastTapTime = -1;
542         mInMultiTap = false;
543     }
544 
checkMultiTap(long eventTime, int keyIndex)545     private void checkMultiTap(long eventTime, int keyIndex) {
546         Key key = getKey(keyIndex);
547         if (key == null)
548             return;
549 
550         final boolean isMultiTap =
551                 (eventTime < mLastTapTime + mMultiTapKeyTimeout && keyIndex == mLastSentIndex);
552         if (key.codes.length > 1) {
553             mInMultiTap = true;
554             if (isMultiTap) {
555                 mTapCount = (mTapCount + 1) % key.codes.length;
556                 return;
557             } else {
558                 mTapCount = -1;
559                 return;
560             }
561         }
562         if (!isMultiTap) {
563             resetMultiTap();
564         }
565     }
566 
debugLog(String title, int x, int y)567     private void debugLog(String title, int x, int y) {
568         int keyIndex = mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null);
569         Key key = getKey(keyIndex);
570         final String code;
571         if (key == null) {
572             code = "----";
573         } else {
574             int primaryCode = key.codes[0];
575             code = String.format((primaryCode < 0) ? "%4d" : "0x%02x", primaryCode);
576         }
577         Log.d(TAG, String.format("%s%s[%d] %3d,%3d %3d(%s) %s", title,
578                 (mKeyAlreadyProcessed ? "-" : " "), mPointerId, x, y, keyIndex, code,
579                 (isModifier() ? "modifier" : "")));
580     }
581 }
582