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