PointerTracker.java revision c5c57b506e97b334a394d23ed73c9597cb55707a
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.LatinImeLogger;
21import com.android.inputmethod.latin.R;
22import com.android.inputmethod.latin.SubtypeSwitcher;
23
24import android.content.res.Resources;
25import android.util.Log;
26import android.view.MotionEvent;
27
28import java.util.Arrays;
29import java.util.List;
30
31public class PointerTracker {
32    private static final String TAG = PointerTracker.class.getSimpleName();
33    private static final boolean ENABLE_ASSERTION = false;
34    private static final boolean DEBUG_EVENT = false;
35    private static final boolean DEBUG_MOVE_EVENT = false;
36    private static final boolean DEBUG_LISTENER = false;
37    private static boolean DEBUG_MODE = LatinImeLogger.sDBG;
38
39    public interface UIProxy {
40        public void invalidateKey(Key key);
41        public void showKeyPreview(int keyIndex, PointerTracker tracker);
42        public void dismissKeyPreview(PointerTracker tracker);
43        public boolean hasDistinctMultitouch();
44        public boolean isAccessibilityEnabled();
45    }
46
47    public final int mPointerId;
48
49    // Timing constants
50    private final int mDelayBeforeKeyRepeatStart;
51    private final int mLongPressKeyTimeout;
52    private final int mLongPressShiftKeyTimeout;
53
54    private final KeyboardView mKeyboardView;
55    private final UIProxy mProxy;
56    private final UIHandler mHandler;
57    private final KeyDetector mKeyDetector;
58    private KeyboardActionListener mListener = EMPTY_LISTENER;
59    private final KeyboardSwitcher mKeyboardSwitcher;
60    private final boolean mHasDistinctMultitouch;
61    private final boolean mConfigSlidingKeyInputEnabled;
62
63    private final int mTouchNoiseThresholdMillis;
64    private final int mTouchNoiseThresholdDistanceSquared;
65
66    private Keyboard mKeyboard;
67    private List<Key> mKeys;
68    private int mKeyHysteresisDistanceSquared = -1;
69    private int mKeyQuarterWidthSquared;
70
71    private final PointerTrackerKeyState mKeyState;
72
73    // true if accessibility is enabled in the parent keyboard
74    private boolean mIsAccessibilityEnabled;
75
76    // true if keyboard layout has been changed.
77    private boolean mKeyboardLayoutHasBeenChanged;
78
79    // true if event is already translated to a key action (long press or mini-keyboard)
80    private boolean mKeyAlreadyProcessed;
81
82    // true if this pointer is repeatable key
83    private boolean mIsRepeatableKey;
84
85    // true if this pointer is in sliding key input
86    private boolean mIsInSlidingKeyInput;
87
88    // true if sliding key is allowed.
89    private boolean mIsAllowedSlidingKeyInput;
90
91    // ignore modifier key if true
92    private boolean mIgnoreModifierKey;
93
94    // TODO: Remove these hacking variables
95    // true if this pointer is in sliding language switch
96    private boolean mIsInSlidingLanguageSwitch;
97    private int mSpaceKeyIndex;
98    private final SubtypeSwitcher mSubtypeSwitcher;
99
100    // Empty {@link KeyboardActionListener}
101    private static final KeyboardActionListener EMPTY_LISTENER = new KeyboardActionListener() {
102        @Override
103        public void onPress(int primaryCode, boolean withSliding) {}
104        @Override
105        public void onRelease(int primaryCode, boolean withSliding) {}
106        @Override
107        public void onCodeInput(int primaryCode, int[] keyCodes, int x, int y) {}
108        @Override
109        public void onTextInput(CharSequence text) {}
110        @Override
111        public void onCancelInput() {}
112        @Override
113        public void onSwipeDown() {}
114    };
115
116    public PointerTracker(int id, KeyboardView keyboardView, UIHandler handler,
117            KeyDetector keyDetector, UIProxy proxy) {
118        if (proxy == null || handler == null || keyDetector == null)
119            throw new NullPointerException();
120        mPointerId = id;
121        mKeyboardView = keyboardView;
122        mProxy = proxy;
123        mHandler = handler;
124        mKeyDetector = keyDetector;
125        mKeyboardSwitcher = KeyboardSwitcher.getInstance();
126        mKeyState = new PointerTrackerKeyState(keyDetector);
127        mIsAccessibilityEnabled = proxy.isAccessibilityEnabled();
128        mHasDistinctMultitouch = proxy.hasDistinctMultitouch();
129        final Resources res = mKeyboardView.getResources();
130        mConfigSlidingKeyInputEnabled = res.getBoolean(R.bool.config_sliding_key_input_enabled);
131        mDelayBeforeKeyRepeatStart = res.getInteger(R.integer.config_delay_before_key_repeat_start);
132        mLongPressKeyTimeout = res.getInteger(R.integer.config_long_press_key_timeout);
133        mLongPressShiftKeyTimeout = res.getInteger(R.integer.config_long_press_shift_key_timeout);
134        mTouchNoiseThresholdMillis = res.getInteger(R.integer.config_touch_noise_threshold_millis);
135        final float touchNoiseThresholdDistance = res.getDimension(
136                R.dimen.config_touch_noise_threshold_distance);
137        mTouchNoiseThresholdDistanceSquared = (int)(
138                touchNoiseThresholdDistance * touchNoiseThresholdDistance);
139        mSubtypeSwitcher = SubtypeSwitcher.getInstance();
140    }
141
142    public void setOnKeyboardActionListener(KeyboardActionListener listener) {
143        mListener = listener;
144    }
145
146    public void setAccessibilityEnabled(boolean accessibilityEnabled) {
147        mIsAccessibilityEnabled = accessibilityEnabled;
148    }
149
150    // Returns true if keyboard has been changed by this callback.
151    private boolean callListenerOnPressAndCheckKeyboardLayoutChange(Key key, boolean withSliding) {
152        final boolean ignoreModifierKey = mIgnoreModifierKey && isModifierCode(key.mCode);
153        if (DEBUG_LISTENER)
154            Log.d(TAG, "onPress    : " + keyCodePrintable(key.mCode) + " sliding=" + withSliding
155                    + " ignoreModifier=" + ignoreModifierKey);
156        if (ignoreModifierKey)
157            return false;
158        if (key.mEnabled) {
159            mListener.onPress(key.mCode, withSliding);
160            final boolean keyboardLayoutHasBeenChanged = mKeyboardLayoutHasBeenChanged;
161            mKeyboardLayoutHasBeenChanged = false;
162            return keyboardLayoutHasBeenChanged;
163        }
164        return false;
165    }
166
167    // Note that we need primaryCode argument because the keyboard may in shifted state and the
168    // primaryCode is different from {@link Key#mCode}.
169    private void callListenerOnCodeInput(Key key, int primaryCode, int[] keyCodes, int x, int y) {
170        final boolean ignoreModifierKey = mIgnoreModifierKey && isModifierCode(key.mCode);
171        if (DEBUG_LISTENER)
172            Log.d(TAG, "onCodeInput: " + keyCodePrintable(primaryCode)
173                    + " codes="+ Arrays.toString(keyCodes) + " x=" + x + " y=" + y
174                    + " ignoreModifier=" + ignoreModifierKey);
175        if (ignoreModifierKey)
176            return;
177        if (key.mEnabled)
178            mListener.onCodeInput(primaryCode, keyCodes, x, y);
179    }
180
181    private void callListenerOnTextInput(Key key) {
182        if (DEBUG_LISTENER)
183            Log.d(TAG, "onTextInput: text=" + key.mOutputText);
184        if (key.mEnabled)
185            mListener.onTextInput(key.mOutputText);
186    }
187
188    // Note that we need primaryCode argument because the keyboard may in shifted state and the
189    // primaryCode is different from {@link Key#mCode}.
190    private void callListenerOnRelease(Key key, int primaryCode, boolean withSliding) {
191        final boolean ignoreModifierKey = mIgnoreModifierKey && isModifierCode(key.mCode);
192        if (DEBUG_LISTENER)
193            Log.d(TAG, "onRelease  : " + keyCodePrintable(primaryCode) + " sliding="
194                    + withSliding + " ignoreModifier=" + ignoreModifierKey);
195        if (ignoreModifierKey)
196            return;
197        if (key.mEnabled)
198            mListener.onRelease(primaryCode, withSliding);
199    }
200
201    private void callListenerOnCancelInput() {
202        if (DEBUG_LISTENER)
203            Log.d(TAG, "onCancelInput");
204        mListener.onCancelInput();
205    }
206
207    public void setKeyboard(Keyboard keyboard, float keyHysteresisDistance) {
208        if (keyboard == null || keyHysteresisDistance < 0)
209            throw new IllegalArgumentException();
210        mKeyboard = keyboard;
211        mKeys = keyboard.getKeys();
212        mKeyHysteresisDistanceSquared = (int)(keyHysteresisDistance * keyHysteresisDistance);
213        final int keyQuarterWidth = keyboard.getKeyWidth() / 4;
214        mKeyQuarterWidthSquared = keyQuarterWidth * keyQuarterWidth;
215        // Mark that keyboard layout has been changed.
216        mKeyboardLayoutHasBeenChanged = true;
217    }
218
219    public boolean isInSlidingKeyInput() {
220        return mIsInSlidingKeyInput;
221    }
222
223    private boolean isValidKeyIndex(int keyIndex) {
224        return keyIndex >= 0 && keyIndex < mKeys.size();
225    }
226
227    public Key getKey(int keyIndex) {
228        return isValidKeyIndex(keyIndex) ? mKeys.get(keyIndex) : null;
229    }
230
231    private static boolean isModifierCode(int primaryCode) {
232        return primaryCode == Keyboard.CODE_SHIFT
233                || primaryCode == Keyboard.CODE_SWITCH_ALPHA_SYMBOL;
234    }
235
236    private boolean isModifierInternal(int keyIndex) {
237        final Key key = getKey(keyIndex);
238        return key == null ? false : isModifierCode(key.mCode);
239    }
240
241    public boolean isModifier() {
242        return isModifierInternal(mKeyState.getKeyIndex());
243    }
244
245    private boolean isOnModifierKey(int x, int y) {
246        return isModifierInternal(mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null));
247    }
248
249    public boolean isOnShiftKey(int x, int y) {
250        final Key key = getKey(mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null));
251        return key != null && key.mCode == Keyboard.CODE_SHIFT;
252    }
253
254    public boolean isSpaceKey(int keyIndex) {
255        Key key = getKey(keyIndex);
256        return key != null && key.mCode == Keyboard.CODE_SPACE;
257    }
258
259    public void setReleasedKeyGraphics() {
260        setReleasedKeyGraphics(mKeyState.getKeyIndex());
261    }
262
263    private void setReleasedKeyGraphics(int keyIndex) {
264        final Key key = getKey(keyIndex);
265        if (key != null) {
266            key.onReleased();
267            mProxy.invalidateKey(key);
268        }
269    }
270
271    private void setPressedKeyGraphics(int keyIndex) {
272        final Key key = getKey(keyIndex);
273        if (key != null && key.mEnabled) {
274            key.onPressed();
275            mProxy.invalidateKey(key);
276        }
277    }
278
279    private void checkAssertion(PointerTrackerQueue queue) {
280        if (mHasDistinctMultitouch && queue == null)
281            throw new RuntimeException(
282                    "PointerTrackerQueue must be passed on distinct multi touch device");
283        if (!mHasDistinctMultitouch && queue != null)
284            throw new RuntimeException(
285                    "PointerTrackerQueue must be null on non-distinct multi touch device");
286    }
287
288    public void onTouchEvent(int action, int x, int y, long eventTime, PointerTrackerQueue queue) {
289        switch (action) {
290        case MotionEvent.ACTION_MOVE:
291            onMoveEvent(x, y, eventTime, queue);
292            break;
293        case MotionEvent.ACTION_DOWN:
294        case MotionEvent.ACTION_POINTER_DOWN:
295            onDownEvent(x, y, eventTime, queue);
296            break;
297        case MotionEvent.ACTION_UP:
298        case MotionEvent.ACTION_POINTER_UP:
299            onUpEvent(x, y, eventTime, queue);
300            break;
301        case MotionEvent.ACTION_CANCEL:
302            onCancelEvent(x, y, eventTime, queue);
303            break;
304        }
305    }
306
307    public void onDownEvent(int x, int y, long eventTime, PointerTrackerQueue queue) {
308        if (ENABLE_ASSERTION) checkAssertion(queue);
309        if (DEBUG_EVENT)
310            printTouchEvent("onDownEvent:", x, y, eventTime);
311
312        // Naive up-to-down noise filter.
313        final long deltaT = eventTime - mKeyState.getUpTime();
314        if (deltaT < mTouchNoiseThresholdMillis) {
315            final int dx = x - mKeyState.getLastX();
316            final int dy = y - mKeyState.getLastY();
317            final int distanceSquared = (dx * dx + dy * dy);
318            if (distanceSquared < mTouchNoiseThresholdDistanceSquared) {
319                if (DEBUG_MODE)
320                    Log.w(TAG, "onDownEvent: ignore potential noise: time=" + deltaT
321                            + " distance=" + distanceSquared);
322                mKeyAlreadyProcessed = true;
323                return;
324            }
325        }
326
327        if (queue != null) {
328            if (isOnModifierKey(x, y)) {
329                // Before processing a down event of modifier key, all pointers already being
330                // tracked should be released.
331                queue.releaseAllPointers(eventTime);
332            }
333            queue.add(this);
334        }
335        onDownEventInternal(x, y, eventTime);
336    }
337
338    private void onDownEventInternal(int x, int y, long eventTime) {
339        int keyIndex = mKeyState.onDownKey(x, y, eventTime);
340        // Sliding key is allowed when 1) enabled by configuration, 2) this pointer starts sliding
341        // from modifier key, 3) this pointer is on mini-keyboard, or 4) accessibility is enabled.
342        mIsAllowedSlidingKeyInput = mConfigSlidingKeyInputEnabled || isModifierInternal(keyIndex)
343                || mKeyDetector instanceof MiniKeyboardKeyDetector
344                || mIsAccessibilityEnabled;
345        mKeyboardLayoutHasBeenChanged = false;
346        mKeyAlreadyProcessed = false;
347        mIsRepeatableKey = false;
348        mIsInSlidingKeyInput = false;
349        mIsInSlidingLanguageSwitch = false;
350        mIgnoreModifierKey = false;
351        if (isValidKeyIndex(keyIndex)) {
352            // This onPress call may have changed keyboard layout. Those cases are detected at
353            // {@link #setKeyboard}. In those cases, we should update keyIndex according to the new
354            // keyboard layout.
355            if (callListenerOnPressAndCheckKeyboardLayoutChange(getKey(keyIndex), false))
356                keyIndex = mKeyState.onDownKey(x, y, eventTime);
357
358            // Accessibility disables key repeat because users may need to pause on a key to hear
359            // its spoken description.
360            final Key key = getKey(keyIndex);
361            if (key != null && key.mRepeatable && !mIsAccessibilityEnabled) {
362                repeatKey(keyIndex);
363                mHandler.startKeyRepeatTimer(mDelayBeforeKeyRepeatStart, keyIndex, this);
364                mIsRepeatableKey = true;
365            }
366            startLongPressTimer(keyIndex);
367            showKeyPreview(keyIndex);
368            setPressedKeyGraphics(keyIndex);
369        }
370    }
371
372    private void startSlidingKeyInput(Key key) {
373        if (!mIsInSlidingKeyInput)
374            mIgnoreModifierKey = isModifierCode(key.mCode);
375        mIsInSlidingKeyInput = true;
376    }
377
378    public void onMoveEvent(int x, int y, long eventTime, PointerTrackerQueue queue) {
379        if (ENABLE_ASSERTION) checkAssertion(queue);
380        if (DEBUG_MOVE_EVENT)
381            printTouchEvent("onMoveEvent:", x, y, eventTime);
382        if (mKeyAlreadyProcessed)
383            return;
384        final PointerTrackerKeyState keyState = mKeyState;
385
386        // TODO: Remove this hacking code
387        if (mIsInSlidingLanguageSwitch) {
388            ((LatinKeyboard)mKeyboard).updateSpacebarPreviewIcon(x - keyState.getKeyX());
389            showKeyPreview(mSpaceKeyIndex);
390            return;
391        }
392        final int lastX = keyState.getLastX();
393        final int lastY = keyState.getLastY();
394        final int oldKeyIndex = keyState.getKeyIndex();
395        final Key oldKey = getKey(oldKeyIndex);
396        int keyIndex = keyState.onMoveKey(x, y);
397        if (isValidKeyIndex(keyIndex)) {
398            if (oldKey == null) {
399                // The pointer has been slid in to the new key, but the finger was not on any keys.
400                // In this case, we must call onPress() to notify that the new key is being pressed.
401                // This onPress call may have changed keyboard layout. Those cases are detected at
402                // {@link #setKeyboard}. In those cases, we should update keyIndex according to the
403                // new keyboard layout.
404                if (callListenerOnPressAndCheckKeyboardLayoutChange(getKey(keyIndex), true))
405                    keyIndex = keyState.onMoveKey(x, y);
406                keyState.onMoveToNewKey(keyIndex, x, y);
407                startLongPressTimer(keyIndex);
408                showKeyPreview(keyIndex);
409                setPressedKeyGraphics(keyIndex);
410            } else if (isMajorEnoughMoveToBeOnNewKey(x, y, keyIndex)) {
411                // The pointer has been slid in to the new key from the previous key, we must call
412                // onRelease() first to notify that the previous key has been released, then call
413                // onPress() to notify that the new key is being pressed.
414                setReleasedKeyGraphics(oldKeyIndex);
415                callListenerOnRelease(oldKey, oldKey.mCode, true);
416                startSlidingKeyInput(oldKey);
417                mHandler.cancelLongPressTimers();
418                if (mIsAllowedSlidingKeyInput) {
419                    // This onPress call may have changed keyboard layout. Those cases are detected
420                    // at {@link #setKeyboard}. In those cases, we should update keyIndex according
421                    // to the new keyboard layout.
422                    if (callListenerOnPressAndCheckKeyboardLayoutChange(getKey(keyIndex), true))
423                        keyIndex = keyState.onMoveKey(x, y);
424                    keyState.onMoveToNewKey(keyIndex, x, y);
425                    startLongPressTimer(keyIndex);
426                    setPressedKeyGraphics(keyIndex);
427                    showKeyPreview(keyIndex);
428                } else {
429                    // HACK: On some devices, quick successive touches may be translated to sudden
430                    // move by touch panel firmware. This hack detects the case and translates the
431                    // move event to successive up and down events.
432                    final int dx = x - lastX;
433                    final int dy = y - lastY;
434                    final int lastMoveSquared = dx * dx + dy * dy;
435                    if (lastMoveSquared >= mKeyQuarterWidthSquared) {
436                        if (DEBUG_MODE)
437                            Log.w(TAG, String.format("onMoveEvent: sudden move is translated to "
438                                    + "up[%d,%d]/down[%d,%d] events", lastX, lastY, x, y));
439                        onUpEventInternal(lastX, lastY, eventTime, true);
440                        onDownEventInternal(x, y, eventTime);
441                    } else {
442                        mKeyAlreadyProcessed = true;
443                        dismissKeyPreview();
444                        setReleasedKeyGraphics(oldKeyIndex);
445                    }
446                }
447            }
448            // TODO: Remove this hack code
449            else if (isSpaceKey(keyIndex) && !mIsInSlidingLanguageSwitch
450                    && mKeyboard instanceof LatinKeyboard) {
451                final LatinKeyboard keyboard = ((LatinKeyboard)mKeyboard);
452                if (mSubtypeSwitcher.useSpacebarLanguageSwitcher()
453                        && mSubtypeSwitcher.getEnabledKeyboardLocaleCount() > 1) {
454                    final int diff = x - keyState.getKeyX();
455                    if (keyboard.shouldTriggerSpacebarSlidingLanguageSwitch(diff)) {
456                        // Detect start sliding language switch.
457                        mIsInSlidingLanguageSwitch = true;
458                        mSpaceKeyIndex = keyIndex;
459                        keyboard.updateSpacebarPreviewIcon(diff);
460                        // Display spacebar slide language switcher.
461                        showKeyPreview(keyIndex);
462                        queue.releaseAllPointersExcept(this, eventTime, true);
463                    }
464                }
465            }
466        } else {
467            if (oldKey != null && isMajorEnoughMoveToBeOnNewKey(x, y, keyIndex)) {
468                // The pointer has been slid out from the previous key, we must call onRelease() to
469                // notify that the previous key has been released.
470                setReleasedKeyGraphics(oldKeyIndex);
471                callListenerOnRelease(oldKey, oldKey.mCode, true);
472                startSlidingKeyInput(oldKey);
473                mHandler.cancelLongPressTimers();
474                if (mIsAllowedSlidingKeyInput) {
475                    keyState.onMoveToNewKey(keyIndex, x, y);
476                } else {
477                    mKeyAlreadyProcessed = true;
478                    dismissKeyPreview();
479                }
480            }
481        }
482    }
483
484    public void onUpEvent(int x, int y, long eventTime, PointerTrackerQueue queue) {
485        if (ENABLE_ASSERTION) checkAssertion(queue);
486        if (DEBUG_EVENT)
487            printTouchEvent("onUpEvent  :", x, y, eventTime);
488
489        if (queue != null) {
490            if (isModifier()) {
491                // Before processing an up event of modifier key, all pointers already being
492                // tracked should be released.
493                queue.releaseAllPointersExcept(this, eventTime, true);
494            } else {
495                queue.releaseAllPointersOlderThan(this, eventTime);
496            }
497            queue.remove(this);
498        }
499        onUpEventInternal(x, y, eventTime, true);
500    }
501
502    // Let this pointer tracker know that one of newer-than-this pointer trackers got an up event.
503    // This pointer tracker needs to keep the key top graphics "pressed", but needs to get a
504    // "virtual" up event.
505    public void onPhantomUpEvent(int x, int y, long eventTime, boolean updateReleasedKeyGraphics) {
506        if (DEBUG_EVENT)
507            printTouchEvent("onPhntEvent:", x, y, eventTime);
508        onUpEventInternal(x, y, eventTime, updateReleasedKeyGraphics);
509        mKeyAlreadyProcessed = true;
510    }
511
512    private void onUpEventInternal(int x, int y, long eventTime,
513            boolean updateReleasedKeyGraphics) {
514        mHandler.cancelKeyTimers();
515        mHandler.cancelShowKeyPreview(this);
516        mIsInSlidingKeyInput = false;
517        final PointerTrackerKeyState keyState = mKeyState;
518        final int keyX, keyY;
519        if (isMajorEnoughMoveToBeOnNewKey(x, y, keyState.onMoveKey(x, y))) {
520            keyX = x;
521            keyY = y;
522        } else {
523            // Use previous fixed key coordinates.
524            keyX = keyState.getKeyX();
525            keyY = keyState.getKeyY();
526        }
527        final int keyIndex = keyState.onUpKey(keyX, keyY, eventTime);
528        dismissKeyPreview();
529        if (updateReleasedKeyGraphics)
530            setReleasedKeyGraphics(keyIndex);
531        if (mKeyAlreadyProcessed)
532            return;
533        // TODO: Remove this hacking code
534        if (mIsInSlidingLanguageSwitch) {
535            setReleasedKeyGraphics(mSpaceKeyIndex);
536            final int languageDir = ((LatinKeyboard)mKeyboard).getLanguageChangeDirection();
537            if (languageDir != 0) {
538                final int code = (languageDir == 1)
539                        ? LatinKeyboard.CODE_NEXT_LANGUAGE : LatinKeyboard.CODE_PREV_LANGUAGE;
540                // This will change keyboard layout.
541                mListener.onCodeInput(code, new int[] {code}, keyX, keyY);
542            }
543            ((LatinKeyboard)mKeyboard).updateSpacebarPreviewIcon(0);
544            mIsInSlidingLanguageSwitch = false;
545            return;
546        }
547        if (!mIsRepeatableKey) {
548            detectAndSendKey(keyIndex, keyX, keyY);
549        }
550    }
551
552    public void onLongPressed(PointerTrackerQueue queue) {
553        mKeyAlreadyProcessed = true;
554        queue.remove(this);
555    }
556
557    public void onCancelEvent(int x, int y, long eventTime, PointerTrackerQueue queue) {
558        if (ENABLE_ASSERTION) checkAssertion(queue);
559        if (DEBUG_EVENT)
560            printTouchEvent("onCancelEvt:", x, y, eventTime);
561
562        if (queue != null) {
563            queue.releaseAllPointersExcept(this, eventTime, true);
564            queue.remove(this);
565        }
566        onCancelEventInternal();
567    }
568
569    private void onCancelEventInternal() {
570        mHandler.cancelKeyTimers();
571        mHandler.cancelShowKeyPreview(this);
572        dismissKeyPreview();
573        setReleasedKeyGraphics(mKeyState.getKeyIndex());
574        mIsInSlidingKeyInput = false;
575    }
576
577    public void repeatKey(int keyIndex) {
578        Key key = getKey(keyIndex);
579        if (key != null) {
580            detectAndSendKey(keyIndex, key.mX, key.mY);
581        }
582    }
583
584    public int getLastX() {
585        return mKeyState.getLastX();
586    }
587
588    public int getLastY() {
589        return mKeyState.getLastY();
590    }
591
592    public long getDownTime() {
593        return mKeyState.getDownTime();
594    }
595
596    private boolean isMajorEnoughMoveToBeOnNewKey(int x, int y, int newKey) {
597        if (mKeys == null || mKeyHysteresisDistanceSquared < 0)
598            throw new IllegalStateException("keyboard and/or hysteresis not set");
599        int curKey = mKeyState.getKeyIndex();
600        if (newKey == curKey) {
601            return false;
602        } else if (isValidKeyIndex(curKey)) {
603            return mKeys.get(curKey).squaredDistanceToEdge(x, y) >= mKeyHysteresisDistanceSquared;
604        } else {
605            return true;
606        }
607    }
608
609    // The modifier key, such as shift key, should not show its key preview. If accessibility is
610    // turned on, the modifier key should show its key preview.
611    private boolean isKeyPreviewNotRequired(int keyIndex) {
612        final Key key = getKey(keyIndex);
613        if (!key.mEnabled)
614            return true;
615        if (mIsAccessibilityEnabled)
616            return false;
617        // Such as spacebar sliding language switch.
618        if (mKeyboard.needSpacebarPreview(keyIndex))
619            return false;
620        final int code = key.mCode;
621        return isModifierCode(code) || code == Keyboard.CODE_DELETE
622                || code == Keyboard.CODE_ENTER || code == Keyboard.CODE_SPACE;
623    }
624
625    private void showKeyPreview(int keyIndex) {
626        if (isKeyPreviewNotRequired(keyIndex))
627            return;
628        mProxy.showKeyPreview(keyIndex, this);
629    }
630
631    private void dismissKeyPreview() {
632        mProxy.dismissKeyPreview(this);
633    }
634
635    private void startLongPressTimer(int keyIndex) {
636        // Accessibility disables long press because users are likely to need to pause on a key
637        // for an unspecified duration in order to hear the key's spoken description.
638        if (mIsAccessibilityEnabled) {
639            return;
640        }
641        Key key = getKey(keyIndex);
642        if (!key.mEnabled)
643            return;
644        if (key.mCode == Keyboard.CODE_SHIFT) {
645            mHandler.startLongPressShiftTimer(mLongPressShiftKeyTimeout, keyIndex, this);
646        } else if (key.mManualTemporaryUpperCaseCode != Keyboard.CODE_DUMMY
647                && mKeyboard.isManualTemporaryUpperCase()) {
648            // We need not start long press timer on the key which has manual temporary upper case
649            // code defined and the keyboard is in manual temporary upper case mode.
650            return;
651        } else if (mKeyboardSwitcher.isInMomentaryAutoModeSwitchState()) {
652            // We use longer timeout for sliding finger input started from the symbols mode key.
653            mHandler.startLongPressTimer(mLongPressKeyTimeout * 3, keyIndex, this);
654        } else {
655            mHandler.startLongPressTimer(mLongPressKeyTimeout, keyIndex, this);
656        }
657    }
658
659    private void detectAndSendKey(int index, int x, int y) {
660        final Key key = getKey(index);
661        if (key == null) {
662            callListenerOnCancelInput();
663            return;
664        }
665        if (key.mOutputText != null) {
666            callListenerOnTextInput(key);
667            callListenerOnRelease(key, key.mCode, false);
668        } else {
669            int code = key.mCode;
670            final int[] codes = mKeyDetector.newCodeArray();
671            mKeyDetector.getKeyIndexAndNearbyCodes(x, y, codes);
672
673            // If keyboard is in manual temporary upper case state and key has manual temporary
674            // shift code, alternate character code should be sent.
675            if (mKeyboard.isManualTemporaryUpperCase()
676                    && key.mManualTemporaryUpperCaseCode != Keyboard.CODE_DUMMY) {
677                code = key.mManualTemporaryUpperCaseCode;
678                codes[0] = code;
679            }
680
681            // Swap the first and second values in the codes array if the primary code is not the
682            // first value but the second value in the array. This happens when key debouncing is
683            // in effect.
684            if (codes.length >= 2 && codes[0] != code && codes[1] == code) {
685                codes[1] = codes[0];
686                codes[0] = code;
687            }
688            callListenerOnCodeInput(key, code, codes, x, y);
689            callListenerOnRelease(key, code, false);
690        }
691    }
692
693    public CharSequence getPreviewText(Key key) {
694        return key.mLabel;
695    }
696
697    private long mPreviousEventTime;
698
699    private void printTouchEvent(String title, int x, int y, long eventTime) {
700        final int keyIndex = mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null);
701        final Key key = getKey(keyIndex);
702        final String code = (key == null) ? "----" : keyCodePrintable(key.mCode);
703        final long delta = eventTime - mPreviousEventTime;
704        Log.d(TAG, String.format("%s%s[%d] %4d %4d %5d %3d(%s)", title,
705                (mKeyAlreadyProcessed ? "-" : " "), mPointerId, x, y, delta, keyIndex, code));
706        mPreviousEventTime = eventTime;
707    }
708
709    private static String keyCodePrintable(int primaryCode) {
710        final String modifier = isModifierCode(primaryCode) ? " modifier" : "";
711        return  String.format((primaryCode < 0) ? "%4d" : "0x%02x", primaryCode) + modifier;
712    }
713}
714