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