PointerTracker.java revision a0537fb4c73dff8beecc328720830af9719d0277
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 int getKeyIndexOn(int x, int y) {
255        return mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null);
256    }
257
258    public boolean isSpaceKey(int keyIndex) {
259        Key key = getKey(keyIndex);
260        return key != null && key.mCode == Keyboard.CODE_SPACE;
261    }
262
263    public void setReleasedKeyGraphics() {
264        setReleasedKeyGraphics(mKeyState.getKeyIndex());
265    }
266
267    private void setReleasedKeyGraphics(int keyIndex) {
268        final Key key = getKey(keyIndex);
269        if (key != null) {
270            key.onReleased();
271            mProxy.invalidateKey(key);
272        }
273    }
274
275    private void setPressedKeyGraphics(int keyIndex) {
276        final Key key = getKey(keyIndex);
277        if (key != null && key.mEnabled) {
278            key.onPressed();
279            mProxy.invalidateKey(key);
280        }
281    }
282
283    private void checkAssertion(PointerTrackerQueue queue) {
284        if (mHasDistinctMultitouch && queue == null)
285            throw new RuntimeException(
286                    "PointerTrackerQueue must be passed on distinct multi touch device");
287        if (!mHasDistinctMultitouch && queue != null)
288            throw new RuntimeException(
289                    "PointerTrackerQueue must be null on non-distinct multi touch device");
290    }
291
292    public void onTouchEvent(int action, int x, int y, long eventTime, PointerTrackerQueue queue) {
293        switch (action) {
294        case MotionEvent.ACTION_MOVE:
295            onMoveEvent(x, y, eventTime, queue);
296            break;
297        case MotionEvent.ACTION_DOWN:
298        case MotionEvent.ACTION_POINTER_DOWN:
299            onDownEvent(x, y, eventTime, queue);
300            break;
301        case MotionEvent.ACTION_UP:
302        case MotionEvent.ACTION_POINTER_UP:
303            onUpEvent(x, y, eventTime, queue);
304            break;
305        case MotionEvent.ACTION_CANCEL:
306            onCancelEvent(x, y, eventTime, queue);
307            break;
308        }
309    }
310
311    public void onDownEvent(int x, int y, long eventTime, PointerTrackerQueue queue) {
312        if (ENABLE_ASSERTION) checkAssertion(queue);
313        if (DEBUG_EVENT)
314            printTouchEvent("onDownEvent:", x, y, eventTime);
315
316        // Naive up-to-down noise filter.
317        final long deltaT = eventTime - mKeyState.getUpTime();
318        if (deltaT < mTouchNoiseThresholdMillis) {
319            final int dx = x - mKeyState.getLastX();
320            final int dy = y - mKeyState.getLastY();
321            final int distanceSquared = (dx * dx + dy * dy);
322            if (distanceSquared < mTouchNoiseThresholdDistanceSquared) {
323                if (DEBUG_MODE)
324                    Log.w(TAG, "onDownEvent: ignore potential noise: time=" + deltaT
325                            + " distance=" + distanceSquared);
326                mKeyAlreadyProcessed = true;
327                return;
328            }
329        }
330
331        if (queue != null) {
332            if (isOnModifierKey(x, y)) {
333                // Before processing a down event of modifier key, all pointers already being
334                // tracked should be released.
335                queue.releaseAllPointers(eventTime);
336            }
337            queue.add(this);
338        }
339        onDownEventInternal(x, y, eventTime);
340    }
341
342    private void onDownEventInternal(int x, int y, long eventTime) {
343        int keyIndex = mKeyState.onDownKey(x, y, eventTime);
344        // Sliding key is allowed when 1) enabled by configuration, 2) this pointer starts sliding
345        // from modifier key, 3) this pointer is on mini-keyboard, or 4) accessibility is enabled.
346        mIsAllowedSlidingKeyInput = mConfigSlidingKeyInputEnabled || isModifierInternal(keyIndex)
347                || mKeyDetector instanceof MiniKeyboardKeyDetector
348                || mIsAccessibilityEnabled;
349        mKeyboardLayoutHasBeenChanged = false;
350        mKeyAlreadyProcessed = false;
351        mIsRepeatableKey = false;
352        mIsInSlidingKeyInput = false;
353        mIsInSlidingLanguageSwitch = false;
354        mIgnoreModifierKey = false;
355        if (isValidKeyIndex(keyIndex)) {
356            // This onPress call may have changed keyboard layout. Those cases are detected at
357            // {@link #setKeyboard}. In those cases, we should update keyIndex according to the new
358            // keyboard layout.
359            if (callListenerOnPressAndCheckKeyboardLayoutChange(getKey(keyIndex), false))
360                keyIndex = mKeyState.onDownKey(x, y, eventTime);
361
362            startRepeatKey(keyIndex);
363            startLongPressTimer(keyIndex);
364            showKeyPreview(keyIndex);
365            setPressedKeyGraphics(keyIndex);
366        }
367    }
368
369    private void startSlidingKeyInput(Key key) {
370        if (!mIsInSlidingKeyInput)
371            mIgnoreModifierKey = isModifierCode(key.mCode);
372        mIsInSlidingKeyInput = true;
373    }
374
375    public void onMoveEvent(int x, int y, long eventTime, PointerTrackerQueue queue) {
376        if (ENABLE_ASSERTION) checkAssertion(queue);
377        if (DEBUG_MOVE_EVENT)
378            printTouchEvent("onMoveEvent:", x, y, eventTime);
379        if (mKeyAlreadyProcessed)
380            return;
381        final PointerTrackerKeyState keyState = mKeyState;
382
383        // TODO: Remove this hacking code
384        if (mIsInSlidingLanguageSwitch) {
385            ((LatinKeyboard)mKeyboard).updateSpacebarPreviewIcon(x - keyState.getKeyX());
386            showKeyPreview(mSpaceKeyIndex);
387            return;
388        }
389        final int lastX = keyState.getLastX();
390        final int lastY = keyState.getLastY();
391        final int oldKeyIndex = keyState.getKeyIndex();
392        final Key oldKey = getKey(oldKeyIndex);
393        int keyIndex = keyState.onMoveKey(x, y);
394        if (isValidKeyIndex(keyIndex)) {
395            if (oldKey == null) {
396                // The pointer has been slid in to the new key, but the finger was not on any keys.
397                // In this case, we must call onPress() to notify that the new key is being pressed.
398                // This onPress call may have changed keyboard layout. Those cases are detected at
399                // {@link #setKeyboard}. In those cases, we should update keyIndex according to the
400                // new keyboard layout.
401                if (callListenerOnPressAndCheckKeyboardLayoutChange(getKey(keyIndex), true))
402                    keyIndex = keyState.onMoveKey(x, y);
403                keyState.onMoveToNewKey(keyIndex, x, y);
404                startLongPressTimer(keyIndex);
405                showKeyPreview(keyIndex);
406                setPressedKeyGraphics(keyIndex);
407            } else if (isMajorEnoughMoveToBeOnNewKey(x, y, keyIndex)) {
408                // The pointer has been slid in to the new key from the previous key, we must call
409                // onRelease() first to notify that the previous key has been released, then call
410                // onPress() to notify that the new key is being pressed.
411                setReleasedKeyGraphics(oldKeyIndex);
412                callListenerOnRelease(oldKey, oldKey.mCode, true);
413                startSlidingKeyInput(oldKey);
414                mHandler.cancelKeyTimers();
415                startRepeatKey(keyIndex);
416                if (mIsAllowedSlidingKeyInput) {
417                    // This onPress call may have changed keyboard layout. Those cases are detected
418                    // at {@link #setKeyboard}. In those cases, we should update keyIndex according
419                    // to the new keyboard layout.
420                    if (callListenerOnPressAndCheckKeyboardLayoutChange(getKey(keyIndex), true))
421                        keyIndex = keyState.onMoveKey(x, y);
422                    keyState.onMoveToNewKey(keyIndex, x, y);
423                    startLongPressTimer(keyIndex);
424                    setPressedKeyGraphics(keyIndex);
425                    showKeyPreview(keyIndex);
426                } else {
427                    // HACK: On some devices, quick successive touches may be translated to sudden
428                    // move by touch panel firmware. This hack detects the case and translates the
429                    // move event to successive up and down events.
430                    final int dx = x - lastX;
431                    final int dy = y - lastY;
432                    final int lastMoveSquared = dx * dx + dy * dy;
433                    if (lastMoveSquared >= mKeyQuarterWidthSquared) {
434                        if (DEBUG_MODE)
435                            Log.w(TAG, String.format("onMoveEvent: sudden move is translated to "
436                                    + "up[%d,%d]/down[%d,%d] events", lastX, lastY, x, y));
437                        onUpEventInternal(lastX, lastY, eventTime, true);
438                        onDownEventInternal(x, y, eventTime);
439                    } else {
440                        mKeyAlreadyProcessed = true;
441                        dismissKeyPreview();
442                        setReleasedKeyGraphics(oldKeyIndex);
443                    }
444                }
445            }
446            // TODO: Remove this hack code
447            else if (isSpaceKey(keyIndex) && !mIsInSlidingLanguageSwitch
448                    && mKeyboard instanceof LatinKeyboard) {
449                final LatinKeyboard keyboard = ((LatinKeyboard)mKeyboard);
450                if (mSubtypeSwitcher.useSpacebarLanguageSwitcher()
451                        && mSubtypeSwitcher.getEnabledKeyboardLocaleCount() > 1) {
452                    final int diff = x - keyState.getKeyX();
453                    if (keyboard.shouldTriggerSpacebarSlidingLanguageSwitch(diff)) {
454                        // Detect start sliding language switch.
455                        mIsInSlidingLanguageSwitch = true;
456                        mSpaceKeyIndex = keyIndex;
457                        keyboard.updateSpacebarPreviewIcon(diff);
458                        // Display spacebar slide language switcher.
459                        showKeyPreview(keyIndex);
460                        if (queue != null)
461                            queue.releaseAllPointersExcept(this, eventTime, true);
462                    }
463                }
464            }
465        } else {
466            if (oldKey != null && isMajorEnoughMoveToBeOnNewKey(x, y, keyIndex)) {
467                // The pointer has been slid out from the previous key, we must call onRelease() to
468                // notify that the previous key has been released.
469                setReleasedKeyGraphics(oldKeyIndex);
470                callListenerOnRelease(oldKey, oldKey.mCode, true);
471                startSlidingKeyInput(oldKey);
472                mHandler.cancelLongPressTimers();
473                if (mIsAllowedSlidingKeyInput) {
474                    keyState.onMoveToNewKey(keyIndex, x, y);
475                } else {
476                    mKeyAlreadyProcessed = true;
477                    dismissKeyPreview();
478                }
479            }
480        }
481    }
482
483    public void onUpEvent(int x, int y, long eventTime, PointerTrackerQueue queue) {
484        if (ENABLE_ASSERTION) checkAssertion(queue);
485        if (DEBUG_EVENT)
486            printTouchEvent("onUpEvent  :", x, y, eventTime);
487
488        if (queue != null) {
489            if (isModifier()) {
490                // Before processing an up event of modifier key, all pointers already being
491                // tracked should be released.
492                queue.releaseAllPointersExcept(this, eventTime, true);
493            } else {
494                queue.releaseAllPointersOlderThan(this, eventTime);
495            }
496            queue.remove(this);
497        }
498        onUpEventInternal(x, y, eventTime, true);
499    }
500
501    // Let this pointer tracker know that one of newer-than-this pointer trackers got an up event.
502    // This pointer tracker needs to keep the key top graphics "pressed", but needs to get a
503    // "virtual" up event.
504    public void onPhantomUpEvent(int x, int y, long eventTime, boolean updateReleasedKeyGraphics) {
505        if (DEBUG_EVENT)
506            printTouchEvent("onPhntEvent:", x, y, eventTime);
507        onUpEventInternal(x, y, eventTime, updateReleasedKeyGraphics);
508        mKeyAlreadyProcessed = true;
509    }
510
511    private void onUpEventInternal(int x, int y, long eventTime,
512            boolean updateReleasedKeyGraphics) {
513        mHandler.cancelKeyTimers();
514        mHandler.cancelShowKeyPreview(this);
515        mIsInSlidingKeyInput = false;
516        final PointerTrackerKeyState keyState = mKeyState;
517        final int keyX, keyY;
518        if (isMajorEnoughMoveToBeOnNewKey(x, y, keyState.onMoveKey(x, y))) {
519            keyX = x;
520            keyY = y;
521        } else {
522            // Use previous fixed key coordinates.
523            keyX = keyState.getKeyX();
524            keyY = keyState.getKeyY();
525        }
526        final int keyIndex = keyState.onUpKey(keyX, keyY, eventTime);
527        dismissKeyPreview();
528        if (updateReleasedKeyGraphics)
529            setReleasedKeyGraphics(keyIndex);
530        if (mKeyAlreadyProcessed)
531            return;
532        // TODO: Remove this hacking code
533        if (mIsInSlidingLanguageSwitch) {
534            setReleasedKeyGraphics(mSpaceKeyIndex);
535            final int languageDir = ((LatinKeyboard)mKeyboard).getLanguageChangeDirection();
536            if (languageDir != 0) {
537                final int code = (languageDir == 1)
538                        ? LatinKeyboard.CODE_NEXT_LANGUAGE : LatinKeyboard.CODE_PREV_LANGUAGE;
539                // This will change keyboard layout.
540                mListener.onCodeInput(code, new int[] {code}, keyX, keyY);
541            }
542            mIsInSlidingLanguageSwitch = false;
543            ((LatinKeyboard)mKeyboard).setSpacebarSlidingLanguageSwitchDiff(0);
544            return;
545        }
546        if (!mIsRepeatableKey) {
547            detectAndSendKey(keyIndex, keyX, keyY);
548        }
549    }
550
551    public void onLongPressed(PointerTrackerQueue queue) {
552        mKeyAlreadyProcessed = true;
553        if (queue != null)
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    private void startRepeatKey(int keyIndex) {
578        // Accessibility disables key repeat because users may need to pause on a key to hear
579        // its spoken description.
580        final Key key = getKey(keyIndex);
581        if (key != null && key.mRepeatable && !mIsAccessibilityEnabled) {
582            dismissKeyPreview();
583            onRepeatKey(keyIndex);
584            mHandler.startKeyRepeatTimer(mDelayBeforeKeyRepeatStart, keyIndex, this);
585            mIsRepeatableKey = true;
586        } else {
587            mIsRepeatableKey = false;
588        }
589    }
590
591    public void onRepeatKey(int keyIndex) {
592        Key key = getKey(keyIndex);
593        if (key != null) {
594            detectAndSendKey(keyIndex, key.mX, key.mY);
595        }
596    }
597
598    public int getLastX() {
599        return mKeyState.getLastX();
600    }
601
602    public int getLastY() {
603        return mKeyState.getLastY();
604    }
605
606    public long getDownTime() {
607        return mKeyState.getDownTime();
608    }
609
610    private boolean isMajorEnoughMoveToBeOnNewKey(int x, int y, int newKey) {
611        if (mKeys == null || mKeyHysteresisDistanceSquared < 0)
612            throw new IllegalStateException("keyboard and/or hysteresis not set");
613        int curKey = mKeyState.getKeyIndex();
614        if (newKey == curKey) {
615            return false;
616        } else if (isValidKeyIndex(curKey)) {
617            return mKeys.get(curKey).squaredDistanceToEdge(x, y) >= mKeyHysteresisDistanceSquared;
618        } else {
619            return true;
620        }
621    }
622
623    // The modifier key, such as shift key, should not show its key preview. If accessibility is
624    // turned on, the modifier key should show its key preview.
625    private boolean isKeyPreviewNotRequired(int keyIndex) {
626        final Key key = getKey(keyIndex);
627        if (!key.mEnabled)
628            return true;
629        if (mIsAccessibilityEnabled)
630            return false;
631        // Such as spacebar sliding language switch.
632        if (mKeyboard.needSpacebarPreview(keyIndex))
633            return false;
634        final int code = key.mCode;
635        return isModifierCode(code) || code == Keyboard.CODE_DELETE
636                || code == Keyboard.CODE_ENTER || code == Keyboard.CODE_SPACE;
637    }
638
639    private void showKeyPreview(int keyIndex) {
640        if (isKeyPreviewNotRequired(keyIndex))
641            return;
642        mProxy.showKeyPreview(keyIndex, this);
643    }
644
645    private void dismissKeyPreview() {
646        mProxy.dismissKeyPreview(this);
647    }
648
649    private void startLongPressTimer(int keyIndex) {
650        // Accessibility disables long press because users are likely to need to pause on a key
651        // for an unspecified duration in order to hear the key's spoken description.
652        if (mIsAccessibilityEnabled) {
653            return;
654        }
655        Key key = getKey(keyIndex);
656        if (!key.mEnabled)
657            return;
658        if (key.mCode == Keyboard.CODE_SHIFT) {
659            mHandler.startLongPressShiftTimer(mLongPressShiftKeyTimeout, keyIndex, this);
660        } else if (key.mManualTemporaryUpperCaseCode != Keyboard.CODE_DUMMY
661                && mKeyboard.isManualTemporaryUpperCase()) {
662            // We need not start long press timer on the key which has manual temporary upper case
663            // code defined and the keyboard is in manual temporary upper case mode.
664            return;
665        } else if (mKeyboardSwitcher.isInMomentaryAutoModeSwitchState()) {
666            // We use longer timeout for sliding finger input started from the symbols mode key.
667            mHandler.startLongPressTimer(mLongPressKeyTimeout * 3, keyIndex, this);
668        } else {
669            mHandler.startLongPressTimer(mLongPressKeyTimeout, keyIndex, this);
670        }
671    }
672
673    private void detectAndSendKey(int index, int x, int y) {
674        final Key key = getKey(index);
675        if (key == null) {
676            callListenerOnCancelInput();
677            return;
678        }
679        if (key.mOutputText != null) {
680            callListenerOnTextInput(key);
681            callListenerOnRelease(key, key.mCode, false);
682        } else {
683            int code = key.mCode;
684            final int[] codes = mKeyDetector.newCodeArray();
685            mKeyDetector.getKeyIndexAndNearbyCodes(x, y, codes);
686
687            // If keyboard is in manual temporary upper case state and key has manual temporary
688            // shift code, alternate character code should be sent.
689            if (mKeyboard.isManualTemporaryUpperCase()
690                    && key.mManualTemporaryUpperCaseCode != Keyboard.CODE_DUMMY) {
691                code = key.mManualTemporaryUpperCaseCode;
692                codes[0] = code;
693            }
694
695            // Swap the first and second values in the codes array if the primary code is not the
696            // first value but the second value in the array. This happens when key debouncing is
697            // in effect.
698            if (codes.length >= 2 && codes[0] != code && codes[1] == code) {
699                codes[1] = codes[0];
700                codes[0] = code;
701            }
702            callListenerOnCodeInput(key, code, codes, x, y);
703            callListenerOnRelease(key, code, false);
704        }
705    }
706
707    public CharSequence getPreviewText(Key key) {
708        return key.mLabel;
709    }
710
711    private long mPreviousEventTime;
712
713    private void printTouchEvent(String title, int x, int y, long eventTime) {
714        final int keyIndex = mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null);
715        final Key key = getKey(keyIndex);
716        final String code = (key == null) ? "----" : keyCodePrintable(key.mCode);
717        final long delta = eventTime - mPreviousEventTime;
718        Log.d(TAG, String.format("%s%s[%d] %4d %4d %5d %3d(%s)", title,
719                (mKeyAlreadyProcessed ? "-" : " "), mPointerId, x, y, delta, keyIndex, code));
720        mPreviousEventTime = eventTime;
721    }
722
723    private static String keyCodePrintable(int primaryCode) {
724        final String modifier = isModifierCode(primaryCode) ? " modifier" : "";
725        return  String.format((primaryCode < 0) ? "%4d" : "0x%02x", primaryCode) + modifier;
726    }
727}
728