diff options
Diffstat (limited to 'java/src/com/android/inputmethod/keyboard/PointerTracker.java')
-rw-r--r-- | java/src/com/android/inputmethod/keyboard/PointerTracker.java | 599 |
1 files changed, 599 insertions, 0 deletions
diff --git a/java/src/com/android/inputmethod/keyboard/PointerTracker.java b/java/src/com/android/inputmethod/keyboard/PointerTracker.java new file mode 100644 index 000000000..a981f724f --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/PointerTracker.java @@ -0,0 +1,599 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.inputmethod.keyboard; + +import com.android.inputmethod.keyboard.KeyboardView.UIHandler; +import com.android.inputmethod.latin.LatinImeLogger; +import com.android.inputmethod.latin.R; + +import android.content.res.Resources; +import android.util.Log; +import android.view.MotionEvent; + +import java.util.Arrays; + +public class PointerTracker { + private static final String TAG = PointerTracker.class.getSimpleName(); + private static final boolean ENABLE_ASSERTION = false; + private static final boolean DEBUG_EVENT = false; + private static final boolean DEBUG_MOVE_EVENT = false; + private static final boolean DEBUG_LISTENER = false; + private static boolean DEBUG_MODE = LatinImeLogger.sDBG; + + public interface UIProxy { + public void invalidateKey(Key key); + public void showPreview(int keyIndex, PointerTracker tracker); + public boolean hasDistinctMultitouch(); + } + + public final int mPointerId; + + // Timing constants + private final int mDelayBeforeKeyRepeatStart; + private final int mLongPressKeyTimeout; + private final int mLongPressShiftKeyTimeout; + + // Miscellaneous constants + private static final int NOT_A_KEY = KeyDetector.NOT_A_KEY; + + private final UIProxy mProxy; + private final UIHandler mHandler; + private final KeyDetector mKeyDetector; + private KeyboardActionListener mListener = EMPTY_LISTENER; + private final KeyboardSwitcher mKeyboardSwitcher; + private final boolean mHasDistinctMultitouch; + private final boolean mConfigSlidingKeyInputEnabled; + + private final int mTouchNoiseThresholdMillis; + private final int mTouchNoiseThresholdDistanceSquared; + + private Keyboard mKeyboard; + private Key[] mKeys; + private int mKeyHysteresisDistanceSquared = -1; + private int mKeyQuarterWidthSquared; + + private final PointerTrackerKeyState mKeyState; + + // true if keyboard layout has been changed. + private boolean mKeyboardLayoutHasBeenChanged; + + // true if event is already translated to a key action (long press or mini-keyboard) + private boolean mKeyAlreadyProcessed; + + // true if this pointer is repeatable key + private boolean mIsRepeatableKey; + + // true if this pointer is in sliding key input + private boolean mIsInSlidingKeyInput; + + // true if sliding key is allowed. + private boolean mIsAllowedSlidingKeyInput; + + // pressed key + private int mPreviousKey = NOT_A_KEY; + + // Empty {@link KeyboardActionListener} + private static final KeyboardActionListener EMPTY_LISTENER = new KeyboardActionListener() { + @Override + public void onPress(int primaryCode) {} + @Override + public void onRelease(int primaryCode) {} + @Override + public void onCodeInput(int primaryCode, int[] keyCodes, int x, int y) {} + @Override + public void onTextInput(CharSequence text) {} + @Override + public void onCancelInput() {} + @Override + public void onSwipeDown() {} + }; + + public PointerTracker(int id, UIHandler handler, KeyDetector keyDetector, UIProxy proxy, + Resources res) { + if (proxy == null || handler == null || keyDetector == null) + throw new NullPointerException(); + mPointerId = id; + mProxy = proxy; + mHandler = handler; + mKeyDetector = keyDetector; + mKeyboardSwitcher = KeyboardSwitcher.getInstance(); + mKeyState = new PointerTrackerKeyState(keyDetector); + mHasDistinctMultitouch = proxy.hasDistinctMultitouch(); + mConfigSlidingKeyInputEnabled = res.getBoolean(R.bool.config_sliding_key_input_enabled); + mDelayBeforeKeyRepeatStart = res.getInteger(R.integer.config_delay_before_key_repeat_start); + mLongPressKeyTimeout = res.getInteger(R.integer.config_long_press_key_timeout); + mLongPressShiftKeyTimeout = res.getInteger(R.integer.config_long_press_shift_key_timeout); + mTouchNoiseThresholdMillis = res.getInteger(R.integer.config_touch_noise_threshold_millis); + final float touchNoiseThresholdDistance = res.getDimension( + R.dimen.config_touch_noise_threshold_distance); + mTouchNoiseThresholdDistanceSquared = (int)( + touchNoiseThresholdDistance * touchNoiseThresholdDistance); + } + + public void setOnKeyboardActionListener(KeyboardActionListener listener) { + mListener = listener; + } + + // Returns true if keyboard has been changed by this callback. + private boolean callListenerOnPressAndCheckKeyboardLayoutChange(int primaryCode) { + if (DEBUG_LISTENER) + Log.d(TAG, "onPress : " + keyCodePrintable(primaryCode)); + mListener.onPress(primaryCode); + final boolean keyboardLayoutHasBeenChanged = mKeyboardLayoutHasBeenChanged; + mKeyboardLayoutHasBeenChanged = false; + return keyboardLayoutHasBeenChanged; + } + + private void callListenerOnCodeInput(int primaryCode, int[] keyCodes, int x, int y) { + if (DEBUG_LISTENER) + Log.d(TAG, "onCodeInput: " + keyCodePrintable(primaryCode) + + " codes="+ Arrays.toString(keyCodes) + " x=" + x + " y=" + y); + mListener.onCodeInput(primaryCode, keyCodes, x, y); + } + + private void callListenerOnTextInput(CharSequence text) { + if (DEBUG_LISTENER) + Log.d(TAG, "onTextInput: text=" + text); + mListener.onTextInput(text); + } + + private void callListenerOnRelease(int primaryCode) { + if (DEBUG_LISTENER) + Log.d(TAG, "onRelease : " + keyCodePrintable(primaryCode)); + mListener.onRelease(primaryCode); + } + + private void callListenerOnCancelInput() { + if (DEBUG_LISTENER) + Log.d(TAG, "onCancelInput"); + mListener.onCancelInput(); + } + + public void setKeyboard(Keyboard keyboard, Key[] keys, float keyHysteresisDistance) { + if (keyboard == null || keys == null || keyHysteresisDistance < 0) + throw new IllegalArgumentException(); + mKeyboard = keyboard; + mKeys = keys; + mKeyHysteresisDistanceSquared = (int)(keyHysteresisDistance * keyHysteresisDistance); + final int keyQuarterWidth = keyboard.getKeyWidth() / 4; + mKeyQuarterWidthSquared = keyQuarterWidth * keyQuarterWidth; + // Mark that keyboard layout has been changed. + mKeyboardLayoutHasBeenChanged = true; + } + + public boolean isInSlidingKeyInput() { + return mIsInSlidingKeyInput; + } + + private boolean isValidKeyIndex(int keyIndex) { + return keyIndex >= 0 && keyIndex < mKeys.length; + } + + public Key getKey(int keyIndex) { + return isValidKeyIndex(keyIndex) ? mKeys[keyIndex] : null; + } + + private static boolean isModifierCode(int primaryCode) { + return primaryCode == Keyboard.CODE_SHIFT + || primaryCode == Keyboard.CODE_SWITCH_ALPHA_SYMBOL; + } + + private boolean isModifierInternal(int keyIndex) { + final Key key = getKey(keyIndex); + return key == null ? false : isModifierCode(key.mCode); + } + + public boolean isModifier() { + return isModifierInternal(mKeyState.getKeyIndex()); + } + + private boolean isOnModifierKey(int x, int y) { + return isModifierInternal(mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null)); + } + + public boolean isOnShiftKey(int x, int y) { + final Key key = getKey(mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null)); + return key != null && key.mCode == Keyboard.CODE_SHIFT; + } + + public boolean isSpaceKey(int keyIndex) { + Key key = getKey(keyIndex); + return key != null && key.mCode == Keyboard.CODE_SPACE; + } + + public void releaseKey() { + updateKeyGraphics(NOT_A_KEY); + } + + private void updateKeyGraphics(int keyIndex) { + int oldKeyIndex = mPreviousKey; + mPreviousKey = keyIndex; + if (keyIndex != oldKeyIndex) { + if (isValidKeyIndex(oldKeyIndex)) { + // if new key index is not a key, old key was just released inside of the key. + final boolean inside = (keyIndex == NOT_A_KEY); + mKeys[oldKeyIndex].onReleased(inside); + mProxy.invalidateKey(mKeys[oldKeyIndex]); + } + if (isValidKeyIndex(keyIndex)) { + mKeys[keyIndex].onPressed(); + mProxy.invalidateKey(mKeys[keyIndex]); + } + } + } + + public void setAlreadyProcessed() { + mKeyAlreadyProcessed = true; + } + + private void checkAssertion(PointerTrackerQueue queue) { + if (mHasDistinctMultitouch && queue == null) + throw new RuntimeException( + "PointerTrackerQueue must be passed on distinct multi touch device"); + if (!mHasDistinctMultitouch && queue != null) + throw new RuntimeException( + "PointerTrackerQueue must be null on non-distinct multi touch device"); + } + + public void onTouchEvent(int action, int x, int y, long eventTime, PointerTrackerQueue queue) { + switch (action) { + case MotionEvent.ACTION_MOVE: + onMoveEvent(x, y, eventTime, queue); + break; + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: + onDownEvent(x, y, eventTime, queue); + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: + onUpEvent(x, y, eventTime, queue); + break; + case MotionEvent.ACTION_CANCEL: + onCancelEvent(x, y, eventTime, queue); + break; + } + } + + public void onDownEvent(int x, int y, long eventTime, PointerTrackerQueue queue) { + if (ENABLE_ASSERTION) checkAssertion(queue); + if (DEBUG_EVENT) + printTouchEvent("onDownEvent:", x, y, eventTime); + + // Naive up-to-down noise filter. + final long deltaT = eventTime - mKeyState.getUpTime(); + if (deltaT < mTouchNoiseThresholdMillis) { + final int dx = x - mKeyState.getLastX(); + final int dy = y - mKeyState.getLastY(); + final int distanceSquared = (dx * dx + dy * dy); + if (distanceSquared < mTouchNoiseThresholdDistanceSquared) { + if (DEBUG_MODE) + Log.w(TAG, "onDownEvent: ignore potential noise: time=" + deltaT + + " distance=" + distanceSquared); + setAlreadyProcessed(); + return; + } + } + + if (queue != null) { + if (isOnModifierKey(x, y)) { + // Before processing a down event of modifier key, all pointers already being + // tracked should be released. + queue.releaseAllPointers(eventTime); + } + queue.add(this); + } + onDownEventInternal(x, y, eventTime); + } + + private void onDownEventInternal(int x, int y, long eventTime) { + int keyIndex = mKeyState.onDownKey(x, y, eventTime); + // Sliding key is allowed when 1) enabled by configuration, 2) this pointer starts sliding + // from modifier key, or 3) this pointer is on mini-keyboard. + mIsAllowedSlidingKeyInput = mConfigSlidingKeyInputEnabled || isModifierInternal(keyIndex) + || mKeyDetector instanceof MiniKeyboardKeyDetector; + mKeyboardLayoutHasBeenChanged = false; + mKeyAlreadyProcessed = false; + mIsRepeatableKey = false; + mIsInSlidingKeyInput = false; + if (isValidKeyIndex(keyIndex)) { + // This onPress call may have changed keyboard layout. Those cases are detected at + // {@link #setKeyboard}. In those cases, we should update keyIndex according to the new + // keyboard layout. + if (callListenerOnPressAndCheckKeyboardLayoutChange(mKeys[keyIndex].mCode)) + keyIndex = mKeyState.onDownKey(x, y, eventTime); + } + if (isValidKeyIndex(keyIndex)) { + if (mKeys[keyIndex].mRepeatable) { + repeatKey(keyIndex); + mHandler.startKeyRepeatTimer(mDelayBeforeKeyRepeatStart, keyIndex, this); + mIsRepeatableKey = true; + } + startLongPressTimer(keyIndex); + } + showKeyPreviewAndUpdateKeyGraphics(keyIndex); + } + + public void onMoveEvent(int x, int y, long eventTime, PointerTrackerQueue queue) { + if (ENABLE_ASSERTION) checkAssertion(queue); + if (DEBUG_MOVE_EVENT) + printTouchEvent("onMoveEvent:", x, y, eventTime); + if (mKeyAlreadyProcessed) + return; + final PointerTrackerKeyState keyState = mKeyState; + + final int lastX = keyState.getLastX(); + final int lastY = keyState.getLastY(); + int keyIndex = keyState.onMoveKey(x, y); + final Key oldKey = getKey(keyState.getKeyIndex()); + if (isValidKeyIndex(keyIndex)) { + if (oldKey == null) { + // The pointer has been slid in to the new key, but the finger was not on any keys. + // In this case, we must call onPress() to notify that the new key is being pressed. + // This onPress call may have changed keyboard layout. Those cases are detected at + // {@link #setKeyboard}. In those cases, we should update keyIndex according to the + // new keyboard layout. + if (callListenerOnPressAndCheckKeyboardLayoutChange(getKey(keyIndex).mCode)) + keyIndex = keyState.onMoveKey(x, y); + keyState.onMoveToNewKey(keyIndex, x, y); + startLongPressTimer(keyIndex); + } else if (!isMinorMoveBounce(x, y, keyIndex)) { + // The pointer has been slid in to the new key from the previous key, we must call + // onRelease() first to notify that the previous key has been released, then call + // onPress() to notify that the new key is being pressed. + mIsInSlidingKeyInput = true; + callListenerOnRelease(oldKey.mCode); + mHandler.cancelLongPressTimers(); + if (mIsAllowedSlidingKeyInput) { + // This onPress call may have changed keyboard layout. Those cases are detected + // at {@link #setKeyboard}. In those cases, we should update keyIndex according + // to the new keyboard layout. + if (callListenerOnPressAndCheckKeyboardLayoutChange(getKey(keyIndex).mCode)) + keyIndex = keyState.onMoveKey(x, y); + keyState.onMoveToNewKey(keyIndex, x, y); + startLongPressTimer(keyIndex); + } else { + // HACK: On some devices, quick successive touches may be translated to sudden + // move by touch panel firmware. This hack detects the case and translates the + // move event to successive up and down events. + final int dx = x - lastX; + final int dy = y - lastY; + final int lastMoveSquared = dx * dx + dy * dy; + if (lastMoveSquared >= mKeyQuarterWidthSquared) { + if (DEBUG_MODE) + Log.w(TAG, String.format("onMoveEvent: sudden move is translated to " + + "up[%d,%d]/down[%d,%d] events", lastX, lastY, x, y)); + onUpEventInternal(lastX, lastY, eventTime); + onDownEventInternal(x, y, eventTime); + } else { + setAlreadyProcessed(); + showKeyPreviewAndUpdateKeyGraphics(NOT_A_KEY); + } + return; + } + } + } else { + if (oldKey != null && !isMinorMoveBounce(x, y, keyIndex)) { + // The pointer has been slid out from the previous key, we must call onRelease() to + // notify that the previous key has been released. + mIsInSlidingKeyInput = true; + callListenerOnRelease(oldKey.mCode); + mHandler.cancelLongPressTimers(); + if (mIsAllowedSlidingKeyInput) { + keyState.onMoveToNewKey(keyIndex, x ,y); + } else { + setAlreadyProcessed(); + showKeyPreviewAndUpdateKeyGraphics(NOT_A_KEY); + return; + } + } + } + showKeyPreviewAndUpdateKeyGraphics(mKeyState.getKeyIndex()); + } + + public void onUpEvent(int x, int y, long eventTime, PointerTrackerQueue queue) { + if (ENABLE_ASSERTION) checkAssertion(queue); + if (DEBUG_EVENT) + printTouchEvent("onUpEvent :", x, y, eventTime); + + if (queue != null) { + if (isModifier()) { + // Before processing an up event of modifier key, all pointers already being + // tracked should be released. + queue.releaseAllPointersExcept(this, eventTime); + } else { + queue.releaseAllPointersOlderThan(this, eventTime); + } + queue.remove(this); + } + onUpEventInternal(x, y, eventTime); + } + + public void onUpEventForRelease(int x, int y, long eventTime) { + onUpEventInternal(x, y, eventTime); + } + + private void onUpEventInternal(int pointX, int pointY, long eventTime) { + int x = pointX; + int y = pointY; + mHandler.cancelKeyTimers(); + mHandler.cancelPopupPreview(); + showKeyPreviewAndUpdateKeyGraphics(NOT_A_KEY); + mIsInSlidingKeyInput = false; + if (mKeyAlreadyProcessed) + return; + final PointerTrackerKeyState keyState = mKeyState; + int keyIndex = keyState.onUpKey(x, y, eventTime); + if (isMinorMoveBounce(x, y, keyIndex)) { + // Use previous fixed key index and coordinates. + keyIndex = keyState.getKeyIndex(); + x = keyState.getKeyX(); + y = keyState.getKeyY(); + } + if (!mIsRepeatableKey) { + detectAndSendKey(keyIndex, x, y); + } + + if (isValidKeyIndex(keyIndex)) + mProxy.invalidateKey(mKeys[keyIndex]); + } + + public void onCancelEvent(int x, int y, long eventTime, PointerTrackerQueue queue) { + if (ENABLE_ASSERTION) checkAssertion(queue); + if (DEBUG_EVENT) + printTouchEvent("onCancelEvt:", x, y, eventTime); + + if (queue != null) + queue.remove(this); + onCancelEventInternal(); + } + + private void onCancelEventInternal() { + mHandler.cancelKeyTimers(); + mHandler.cancelPopupPreview(); + showKeyPreviewAndUpdateKeyGraphics(NOT_A_KEY); + mIsInSlidingKeyInput = false; + int keyIndex = mKeyState.getKeyIndex(); + if (isValidKeyIndex(keyIndex)) + mProxy.invalidateKey(mKeys[keyIndex]); + } + + public void repeatKey(int keyIndex) { + Key key = getKey(keyIndex); + if (key != null) { + detectAndSendKey(keyIndex, key.mX, key.mY); + } + } + + public int getLastX() { + return mKeyState.getLastX(); + } + + public int getLastY() { + return mKeyState.getLastY(); + } + + public long getDownTime() { + return mKeyState.getDownTime(); + } + + // These package scope methods are only for debugging purpose. + /* package */ int getStartX() { + return mKeyState.getStartX(); + } + + /* package */ int getStartY() { + return mKeyState.getStartY(); + } + + private boolean isMinorMoveBounce(int x, int y, int newKey) { + if (mKeys == null || mKeyHysteresisDistanceSquared < 0) + throw new IllegalStateException("keyboard and/or hysteresis not set"); + int curKey = mKeyState.getKeyIndex(); + if (newKey == curKey) { + return true; + } else if (isValidKeyIndex(curKey)) { + return mKeys[curKey].squaredDistanceToEdge(x, y) < mKeyHysteresisDistanceSquared; + } else { + return false; + } + } + + private void showKeyPreviewAndUpdateKeyGraphics(int keyIndex) { + updateKeyGraphics(keyIndex); + // The modifier key, such as shift key, should not be shown as preview when multi-touch is + // supported. On the other hand, if multi-touch is not supported, the modifier key should + // be shown as preview. + if (mHasDistinctMultitouch && isModifier()) { + mProxy.showPreview(NOT_A_KEY, this); + } else { + mProxy.showPreview(keyIndex, this); + } + } + + private void startLongPressTimer(int keyIndex) { + Key key = getKey(keyIndex); + if (key.mCode == Keyboard.CODE_SHIFT) { + mHandler.startLongPressShiftTimer(mLongPressShiftKeyTimeout, keyIndex, this); + } else if (key.mManualTemporaryUpperCaseCode != Keyboard.CODE_DUMMY + && mKeyboard.isManualTemporaryUpperCase()) { + // We need not start long press timer on the key which has manual temporary upper case + // code defined and the keyboard is in manual temporary upper case mode. + return; + } else if (mKeyboardSwitcher.isInMomentaryAutoModeSwitchState()) { + // We use longer timeout for sliding finger input started from the symbols mode key. + mHandler.startLongPressTimer(mLongPressKeyTimeout * 3, keyIndex, this); + } else { + mHandler.startLongPressTimer(mLongPressKeyTimeout, keyIndex, this); + } + } + + private void detectAndSendKey(int index, int x, int y) { + final Key key = getKey(index); + if (key == null) { + callListenerOnCancelInput(); + return; + } + if (key.mOutputText != null) { + callListenerOnTextInput(key.mOutputText); + callListenerOnRelease(key.mCode); + } else { + int code = key.mCode; + final int[] codes = mKeyDetector.newCodeArray(); + mKeyDetector.getKeyIndexAndNearbyCodes(x, y, codes); + + // If keyboard is in manual temporary upper case state and key has manual temporary + // shift code, alternate character code should be sent. + if (mKeyboard.isManualTemporaryUpperCase() + && key.mManualTemporaryUpperCaseCode != Keyboard.CODE_DUMMY) { + code = key.mManualTemporaryUpperCaseCode; + codes[0] = code; + } + + // Swap the first and second values in the codes array if the primary code is not the + // first value but the second value in the array. This happens when key debouncing is + // in effect. + if (codes.length >= 2 && codes[0] != code && codes[1] == code) { + codes[1] = codes[0]; + codes[0] = code; + } + if (key.mEnabled) + callListenerOnCodeInput(code, codes, x, y); + callListenerOnRelease(code); + } + } + + public CharSequence getPreviewText(Key key) { + return key.mLabel; + } + + private long mPreviousEventTime; + + private void printTouchEvent(String title, int x, int y, long eventTime) { + final int keyIndex = mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null); + final Key key = getKey(keyIndex); + final String code = (key == null) ? "----" : keyCodePrintable(key.mCode); + final long delta = eventTime - mPreviousEventTime; + Log.d(TAG, String.format("%s%s[%d] %4d %4d %5d %3d(%s)", title, + (mKeyAlreadyProcessed ? "-" : " "), mPointerId, x, y, delta, keyIndex, code)); + mPreviousEventTime = eventTime; + } + + private static String keyCodePrintable(int primaryCode) { + final String modifier = isModifierCode(primaryCode) ? " modifier" : ""; + return String.format((primaryCode < 0) ? "%4d" : "0x%02x", primaryCode) + modifier; + } +} |