PointerTracker.java revision 5a309f57155fb95667c2ccdda730eaf175de8876
1/*
2 * Copyright (C) 2010 Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
17package com.android.inputmethod.keyboard;
18
19import com.android.inputmethod.keyboard.KeyboardView.UIHandler;
20import com.android.inputmethod.latin.LatinIME;
21import com.android.inputmethod.latin.R;
22
23import android.content.res.Resources;
24import android.util.Log;
25import android.view.MotionEvent;
26
27public class PointerTracker {
28    private static final String TAG = "PointerTracker";
29    private static final boolean DEBUG = false;
30    private static final boolean DEBUG_MOVE = false;
31
32    public interface UIProxy {
33        public void invalidateKey(Key key);
34        public void showPreview(int keyIndex, PointerTracker tracker);
35        public boolean hasDistinctMultitouch();
36    }
37
38    public final int mPointerId;
39
40    // Timing constants
41    private final int mDelayBeforeKeyRepeatStart;
42    private final int mLongPressKeyTimeout;
43    private final int mLongPressShiftKeyTimeout;
44    private final int mMultiTapKeyTimeout;
45
46    // Miscellaneous constants
47    private static final int NOT_A_KEY = KeyDetector.NOT_A_KEY;
48    private static final int[] KEY_DELETE = { Keyboard.KEYCODE_DELETE };
49
50    private final UIProxy mProxy;
51    private final UIHandler mHandler;
52    private final KeyDetector mKeyDetector;
53    private KeyboardActionListener mListener;
54    private final boolean mHasDistinctMultitouch;
55
56    private Keyboard mKeyboard;
57    private Key[] mKeys;
58    private int mKeyHysteresisDistanceSquared = -1;
59
60    private final KeyState mKeyState;
61
62    // true if event is already translated to a key action (long press or mini-keyboard)
63    private boolean mKeyAlreadyProcessed;
64
65    // true if this pointer is repeatable key
66    private boolean mIsRepeatableKey;
67
68    // For multi-tap
69    private int mLastSentIndex;
70    private int mTapCount;
71    private long mLastTapTime;
72    private boolean mInMultiTap;
73    private final StringBuilder mPreviewLabel = new StringBuilder(1);
74
75    // pressed key
76    private int mPreviousKey = NOT_A_KEY;
77
78    // This class keeps track of a key index and a position where this pointer is.
79    private static class KeyState {
80        private final KeyDetector mKeyDetector;
81
82        // The position and time at which first down event occurred.
83        private int mStartX;
84        private int mStartY;
85        private long mDownTime;
86
87        // The current key index where this pointer is.
88        private int mKeyIndex = NOT_A_KEY;
89        // The position where mKeyIndex was recognized for the first time.
90        private int mKeyX;
91        private int mKeyY;
92
93        // Last pointer position.
94        private int mLastX;
95        private int mLastY;
96
97        public KeyState(KeyDetector keyDetecor) {
98            mKeyDetector = keyDetecor;
99        }
100
101        public int getKeyIndex() {
102            return mKeyIndex;
103        }
104
105        public int getKeyX() {
106            return mKeyX;
107        }
108
109        public int getKeyY() {
110            return mKeyY;
111        }
112
113        public int getStartX() {
114            return mStartX;
115        }
116
117        public int getStartY() {
118            return mStartY;
119        }
120
121        public long getDownTime() {
122            return mDownTime;
123        }
124
125        public int getLastX() {
126            return mLastX;
127        }
128
129        public int getLastY() {
130            return mLastY;
131        }
132
133        public int onDownKey(int x, int y, long eventTime) {
134            mStartX = x;
135            mStartY = y;
136            mDownTime = eventTime;
137
138            return onMoveToNewKey(onMoveKeyInternal(x, y), x, y);
139        }
140
141        private int onMoveKeyInternal(int x, int y) {
142            mLastX = x;
143            mLastY = y;
144            return mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null);
145        }
146
147        public int onMoveKey(int x, int y) {
148            return onMoveKeyInternal(x, y);
149        }
150
151        public int onMoveToNewKey(int keyIndex, int x, int y) {
152            mKeyIndex = keyIndex;
153            mKeyX = x;
154            mKeyY = y;
155            return keyIndex;
156        }
157
158        public int onUpKey(int x, int y) {
159            return onMoveKeyInternal(x, y);
160        }
161
162        public void onSetKeyboard() {
163            mKeyIndex = mKeyDetector.getKeyIndexAndNearbyCodes(mKeyX, mKeyY, null);
164        }
165    }
166
167    public PointerTracker(int id, UIHandler handler, KeyDetector keyDetector, UIProxy proxy,
168            Resources res) {
169        if (proxy == null || handler == null || keyDetector == null)
170            throw new NullPointerException();
171        mPointerId = id;
172        mProxy = proxy;
173        mHandler = handler;
174        mKeyDetector = keyDetector;
175        mKeyState = new KeyState(keyDetector);
176        mHasDistinctMultitouch = proxy.hasDistinctMultitouch();
177        mDelayBeforeKeyRepeatStart = res.getInteger(R.integer.config_delay_before_key_repeat_start);
178        mLongPressKeyTimeout = res.getInteger(R.integer.config_long_press_key_timeout);
179        mLongPressShiftKeyTimeout = res.getInteger(R.integer.config_long_press_shift_key_timeout);
180        mMultiTapKeyTimeout = res.getInteger(R.integer.config_multi_tap_key_timeout);
181        resetMultiTap();
182    }
183
184    public void setOnKeyboardActionListener(KeyboardActionListener listener) {
185        mListener = listener;
186    }
187
188    public void setKeyboard(Keyboard keyboard, Key[] keys, float keyHysteresisDistance) {
189        if (keyboard == null || keys == null || keyHysteresisDistance < 0)
190            throw new IllegalArgumentException();
191        mKeyboard = keyboard;
192        mKeys = keys;
193        mKeyHysteresisDistanceSquared = (int)(keyHysteresisDistance * keyHysteresisDistance);
194        // Update current key index because keyboard layout has been changed.
195        mKeyState.onSetKeyboard();
196    }
197
198    private boolean isValidKeyIndex(int keyIndex) {
199        return keyIndex >= 0 && keyIndex < mKeys.length;
200    }
201
202    public Key getKey(int keyIndex) {
203        return isValidKeyIndex(keyIndex) ? mKeys[keyIndex] : null;
204    }
205
206    private boolean isModifierInternal(int keyIndex) {
207        Key key = getKey(keyIndex);
208        if (key == null)
209            return false;
210        int primaryCode = key.codes[0];
211        return primaryCode == Keyboard.KEYCODE_SHIFT
212                || primaryCode == Keyboard.KEYCODE_MODE_CHANGE;
213    }
214
215    public boolean isModifier() {
216        return isModifierInternal(mKeyState.getKeyIndex());
217    }
218
219    public boolean isOnModifierKey(int x, int y) {
220        return isModifierInternal(mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null));
221    }
222
223    public boolean isSpaceKey(int keyIndex) {
224        Key key = getKey(keyIndex);
225        return key != null && key.codes[0] == LatinIME.KEYCODE_SPACE;
226    }
227
228    public void releaseKey() {
229        updateKeyGraphics(NOT_A_KEY);
230    }
231
232    private void updateKeyGraphics(int keyIndex) {
233        int oldKeyIndex = mPreviousKey;
234        mPreviousKey = keyIndex;
235        if (keyIndex != oldKeyIndex) {
236            if (isValidKeyIndex(oldKeyIndex)) {
237                // if new key index is not a key, old key was just released inside of the key.
238                final boolean inside = (keyIndex == NOT_A_KEY);
239                mKeys[oldKeyIndex].onReleased(inside);
240                mProxy.invalidateKey(mKeys[oldKeyIndex]);
241            }
242            if (isValidKeyIndex(keyIndex)) {
243                mKeys[keyIndex].onPressed();
244                mProxy.invalidateKey(mKeys[keyIndex]);
245            }
246        }
247    }
248
249    public void setAlreadyProcessed() {
250        mKeyAlreadyProcessed = true;
251    }
252
253    public void onTouchEvent(int action, int x, int y, long eventTime) {
254        switch (action) {
255        case MotionEvent.ACTION_MOVE:
256            onMoveEvent(x, y, eventTime);
257            break;
258        case MotionEvent.ACTION_DOWN:
259        case MotionEvent.ACTION_POINTER_DOWN:
260            onDownEvent(x, y, eventTime);
261            break;
262        case MotionEvent.ACTION_UP:
263        case MotionEvent.ACTION_POINTER_UP:
264            onUpEvent(x, y, eventTime);
265            break;
266        case MotionEvent.ACTION_CANCEL:
267            onCancelEvent(x, y, eventTime);
268            break;
269        }
270    }
271
272    public void onDownEvent(int x, int y, long eventTime) {
273        if (DEBUG)
274            debugLog("onDownEvent:", x, y);
275        int keyIndex = mKeyState.onDownKey(x, y, eventTime);
276        mKeyAlreadyProcessed = false;
277        mIsRepeatableKey = false;
278        checkMultiTap(eventTime, keyIndex);
279        if (mListener != null) {
280            if (isValidKeyIndex(keyIndex)) {
281                mListener.onPress(mKeys[keyIndex].codes[0]);
282                // This onPress call may have changed keyboard layout and have updated mKeyIndex.
283                // If that's the case, mKeyIndex has been updated in setKeyboard().
284                keyIndex = mKeyState.getKeyIndex();
285            }
286        }
287        if (isValidKeyIndex(keyIndex)) {
288            if (mKeys[keyIndex].repeatable) {
289                repeatKey(keyIndex);
290                mHandler.startKeyRepeatTimer(mDelayBeforeKeyRepeatStart, keyIndex, this);
291                mIsRepeatableKey = true;
292            }
293            startLongPressTimer(keyIndex);
294        }
295        showKeyPreviewAndUpdateKeyGraphics(keyIndex);
296    }
297
298    public void onMoveEvent(int x, int y, long eventTime) {
299        if (DEBUG_MOVE)
300            debugLog("onMoveEvent:", x, y);
301        if (mKeyAlreadyProcessed)
302            return;
303        KeyState keyState = mKeyState;
304        final int keyIndex = keyState.onMoveKey(x, y);
305        final Key oldKey = getKey(keyState.getKeyIndex());
306        if (isValidKeyIndex(keyIndex)) {
307            if (oldKey == null) {
308                keyState.onMoveToNewKey(keyIndex, x, y);
309                startLongPressTimer(keyIndex);
310            } else if (!isMinorMoveBounce(x, y, keyIndex)) {
311                if (mListener != null)
312                    mListener.onRelease(oldKey.codes[0]);
313                resetMultiTap();
314                keyState.onMoveToNewKey(keyIndex, x, y);
315                startLongPressTimer(keyIndex);
316            }
317        } else {
318            if (oldKey != null) {
319                if (mListener != null)
320                    mListener.onRelease(oldKey.codes[0]);
321                keyState.onMoveToNewKey(keyIndex, x ,y);
322                mHandler.cancelLongPressTimers();
323            } else if (!isMinorMoveBounce(x, y, keyIndex)) {
324                resetMultiTap();
325                keyState.onMoveToNewKey(keyIndex, x ,y);
326                mHandler.cancelLongPressTimers();
327            }
328        }
329        showKeyPreviewAndUpdateKeyGraphics(mKeyState.getKeyIndex());
330    }
331
332    public void onUpEvent(int x, int y, long eventTime) {
333        if (DEBUG)
334            debugLog("onUpEvent  :", x, y);
335        showKeyPreviewAndUpdateKeyGraphics(NOT_A_KEY);
336        if (mKeyAlreadyProcessed)
337            return;
338        mHandler.cancelKeyTimers();
339        mHandler.cancelPopupPreview();
340        int keyIndex = mKeyState.onUpKey(x, y);
341        if (isMinorMoveBounce(x, y, keyIndex)) {
342            // Use previous fixed key index and coordinates.
343            keyIndex = mKeyState.getKeyIndex();
344            x = mKeyState.getKeyX();
345            y = mKeyState.getKeyY();
346        }
347        if (!mIsRepeatableKey) {
348            detectAndSendKey(keyIndex, x, y, eventTime);
349        }
350
351        if (isValidKeyIndex(keyIndex))
352            mProxy.invalidateKey(mKeys[keyIndex]);
353    }
354
355    public void onCancelEvent(int x, int y, long eventTime) {
356        if (DEBUG)
357            debugLog("onCancelEvt:", x, y);
358        mHandler.cancelKeyTimers();
359        mHandler.cancelPopupPreview();
360        showKeyPreviewAndUpdateKeyGraphics(NOT_A_KEY);
361        int keyIndex = mKeyState.getKeyIndex();
362        if (isValidKeyIndex(keyIndex))
363           mProxy.invalidateKey(mKeys[keyIndex]);
364    }
365
366    public void repeatKey(int keyIndex) {
367        Key key = getKey(keyIndex);
368        if (key != null) {
369            // While key is repeating, because there is no need to handle multi-tap key, we can
370            // pass -1 as eventTime argument.
371            detectAndSendKey(keyIndex, key.x, key.y, -1);
372        }
373    }
374
375    public int getLastX() {
376        return mKeyState.getLastX();
377    }
378
379    public int getLastY() {
380        return mKeyState.getLastY();
381    }
382
383    public long getDownTime() {
384        return mKeyState.getDownTime();
385    }
386
387    // These package scope methods are only for debugging purpose.
388    /* package */ int getStartX() {
389        return mKeyState.getStartX();
390    }
391
392    /* package */ int getStartY() {
393        return mKeyState.getStartY();
394    }
395
396    private boolean isMinorMoveBounce(int x, int y, int newKey) {
397        if (mKeys == null || mKeyHysteresisDistanceSquared < 0)
398            throw new IllegalStateException("keyboard and/or hysteresis not set");
399        int curKey = mKeyState.getKeyIndex();
400        if (newKey == curKey) {
401            return true;
402        } else if (isValidKeyIndex(curKey)) {
403            return mKeys[curKey].squaredDistanceToEdge(x, y) < mKeyHysteresisDistanceSquared;
404        } else {
405            return false;
406        }
407    }
408
409    private void showKeyPreviewAndUpdateKeyGraphics(int keyIndex) {
410        updateKeyGraphics(keyIndex);
411        // The modifier key, such as shift key, should not be shown as preview when multi-touch is
412        // supported. On the other hand, if multi-touch is not supported, the modifier key should
413        // be shown as preview.
414        if (mHasDistinctMultitouch && isModifier()) {
415            mProxy.showPreview(NOT_A_KEY, this);
416        } else {
417            mProxy.showPreview(keyIndex, this);
418        }
419    }
420
421    private void startLongPressTimer(int keyIndex) {
422        Key key = getKey(keyIndex);
423        if (key.codes[0] == Keyboard.KEYCODE_SHIFT) {
424            mHandler.startLongPressShiftTimer(mLongPressShiftKeyTimeout, keyIndex, this);
425        } else {
426            mHandler.startLongPressTimer(mLongPressKeyTimeout, keyIndex, this);
427        }
428    }
429
430    private boolean isManualTemporaryUpperCase() {
431        return mKeyboard instanceof LatinKeyboard
432                && ((LatinKeyboard)mKeyboard).isManualTemporaryUpperCase();
433    }
434
435    private void detectAndSendKey(int index, int x, int y, long eventTime) {
436        final KeyboardActionListener listener = mListener;
437        final Key key = getKey(index);
438
439        if (key == null) {
440            if (listener != null)
441                listener.onCancel();
442        } else {
443            if (key.text != null) {
444                if (listener != null) {
445                    listener.onText(key.text);
446                    listener.onRelease(NOT_A_KEY);
447                }
448            } else {
449                int code = key.codes[0];
450                //TextEntryState.keyPressedAt(key, x, y);
451                int[] codes = mKeyDetector.newCodeArray();
452                mKeyDetector.getKeyIndexAndNearbyCodes(x, y, codes);
453                // Multi-tap
454                if (mInMultiTap) {
455                    if (mTapCount != -1) {
456                        mListener.onKey(Keyboard.KEYCODE_DELETE, KEY_DELETE, x, y);
457                    } else {
458                        mTapCount = 0;
459                    }
460                    code = key.codes[mTapCount];
461                }
462
463                // If keyboard is in manual temporary upper case state and key has manual temporary
464                // shift code, alternate character code should be sent.
465                if (isManualTemporaryUpperCase() && key.manualTemporaryUpperCaseCode != 0) {
466                    code = key.manualTemporaryUpperCaseCode;
467                    codes[0] = code;
468                }
469
470                /*
471                 * Swap the first and second values in the codes array if the primary code is not
472                 * the first value but the second value in the array. This happens when key
473                 * debouncing is in effect.
474                 */
475                if (codes.length >= 2 && codes[0] != code && codes[1] == code) {
476                    codes[1] = codes[0];
477                    codes[0] = code;
478                }
479                if (listener != null) {
480                    listener.onKey(code, codes, x, y);
481                    listener.onRelease(code);
482                }
483            }
484            mLastSentIndex = index;
485            mLastTapTime = eventTime;
486        }
487    }
488
489    /**
490     * Handle multi-tap keys by producing the key label for the current multi-tap state.
491     */
492    public CharSequence getPreviewText(Key key) {
493        if (mInMultiTap) {
494            // Multi-tap
495            mPreviewLabel.setLength(0);
496            mPreviewLabel.append((char) key.codes[mTapCount < 0 ? 0 : mTapCount]);
497            return mPreviewLabel;
498        } else {
499            return key.label;
500        }
501    }
502
503    private void resetMultiTap() {
504        mLastSentIndex = NOT_A_KEY;
505        mTapCount = 0;
506        mLastTapTime = -1;
507        mInMultiTap = false;
508    }
509
510    private void checkMultiTap(long eventTime, int keyIndex) {
511        Key key = getKey(keyIndex);
512        if (key == null)
513            return;
514
515        final boolean isMultiTap =
516                (eventTime < mLastTapTime + mMultiTapKeyTimeout && keyIndex == mLastSentIndex);
517        if (key.codes.length > 1) {
518            mInMultiTap = true;
519            if (isMultiTap) {
520                mTapCount = (mTapCount + 1) % key.codes.length;
521                return;
522            } else {
523                mTapCount = -1;
524                return;
525            }
526        }
527        if (!isMultiTap) {
528            resetMultiTap();
529        }
530    }
531
532    private void debugLog(String title, int x, int y) {
533        int keyIndex = mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null);
534        Key key = getKey(keyIndex);
535        final String code;
536        if (key == null) {
537            code = "----";
538        } else {
539            int primaryCode = key.codes[0];
540            code = String.format((primaryCode < 0) ? "%4d" : "0x%02x", primaryCode);
541        }
542        Log.d(TAG, String.format("%s%s[%d] %3d,%3d %3d(%s) %s", title,
543                (mKeyAlreadyProcessed ? "-" : " "), mPointerId, x, y, keyIndex, code,
544                (isModifier() ? "modifier" : "")));
545    }
546}
547