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