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