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