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