diff options
Diffstat (limited to 'java/src/com/android/inputmethod/keyboard/PointerTracker.java')
-rw-r--r-- | java/src/com/android/inputmethod/keyboard/PointerTracker.java | 787 |
1 files changed, 610 insertions, 177 deletions
diff --git a/java/src/com/android/inputmethod/keyboard/PointerTracker.java b/java/src/com/android/inputmethod/keyboard/PointerTracker.java index babf6ec99..d0f7cb276 100644 --- a/java/src/com/android/inputmethod/keyboard/PointerTracker.java +++ b/java/src/com/android/inputmethod/keyboard/PointerTracker.java @@ -16,25 +16,37 @@ package com.android.inputmethod.keyboard; +import android.content.res.TypedArray; import android.os.SystemClock; import android.util.Log; import android.view.MotionEvent; -import android.view.View; -import android.widget.TextView; +import com.android.inputmethod.accessibility.AccessibilityUtils; +import com.android.inputmethod.keyboard.internal.GestureStroke; +import com.android.inputmethod.keyboard.internal.GestureStroke.GestureStrokeParams; +import com.android.inputmethod.keyboard.internal.GestureStrokeWithPreviewPoints; import com.android.inputmethod.keyboard.internal.PointerTrackerQueue; +import com.android.inputmethod.latin.CollectionUtils; +import com.android.inputmethod.latin.InputPointers; import com.android.inputmethod.latin.LatinImeLogger; -import com.android.inputmethod.latin.ResearchLogger; +import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.define.ProductionFlag; +import com.android.inputmethod.research.ResearchLogger; import java.util.ArrayList; -public class PointerTracker { +public final class PointerTracker implements PointerTrackerQueue.Element { private static final String TAG = PointerTracker.class.getSimpleName(); 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; + private static boolean DEBUG_MODE = LatinImeLogger.sDBG || DEBUG_EVENT; + + /** True if {@link PointerTracker}s should handle gesture events. */ + private static boolean sShouldHandleGesture = false; + private static boolean sMainDictionaryAvailable = false; + private static boolean sGestureHandlingEnabledByInputField = false; + private static boolean sGestureHandlingEnabledByUser = false; public interface KeyEventHandler { /** @@ -65,13 +77,13 @@ public class PointerTracker { public interface DrawingProxy extends MoreKeysPanel.Controller { public void invalidateKey(Key key); - public TextView inflateKeyPreviewText(); public void showKeyPreview(PointerTracker tracker); public void dismissKeyPreview(PointerTracker tracker); + public void showGesturePreviewTrail(PointerTracker tracker, boolean isOldestTracker); } public interface TimerProxy { - public void startTypingStateTimer(); + public void startTypingStateTimer(Key typedKey); public boolean isTypingState(); public void startKeyRepeatTimer(PointerTracker tracker); public void startLongPressTimer(PointerTracker tracker); @@ -84,7 +96,7 @@ public class PointerTracker { public static class Adapter implements TimerProxy { @Override - public void startTypingStateTimer() {} + public void startTypingStateTimer(Key typedKey) {} @Override public boolean isTypingState() { return false; } @Override @@ -106,12 +118,44 @@ public class PointerTracker { } } + static final class PointerTrackerParams { + public final boolean mSlidingKeyInputEnabled; + public final int mTouchNoiseThresholdTime; + public final int mTouchNoiseThresholdDistance; + public final int mSuppressKeyPreviewAfterBatchInputDuration; + + public static final PointerTrackerParams DEFAULT = new PointerTrackerParams(); + + private PointerTrackerParams() { + mSlidingKeyInputEnabled = false; + mTouchNoiseThresholdTime = 0; + mTouchNoiseThresholdDistance = 0; + mSuppressKeyPreviewAfterBatchInputDuration = 0; + } + + public PointerTrackerParams(final TypedArray mainKeyboardViewAttr) { + mSlidingKeyInputEnabled = mainKeyboardViewAttr.getBoolean( + R.styleable.MainKeyboardView_slidingKeyInputEnable, false); + mTouchNoiseThresholdTime = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_touchNoiseThresholdTime, 0); + mTouchNoiseThresholdDistance = mainKeyboardViewAttr.getDimensionPixelSize( + R.styleable.MainKeyboardView_touchNoiseThresholdDistance, 0); + mSuppressKeyPreviewAfterBatchInputDuration = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_suppressKeyPreviewAfterBatchInputDuration, 0); + } + } + // Parameters for pointer handling. - private static LatinKeyboardView.PointerTrackerParams sParams; - private static int sTouchNoiseThresholdDistanceSquared; + private static PointerTrackerParams sParams; + private static GestureStrokeParams sGestureStrokeParams; private static boolean sNeedsPhantomSuddenMoveEventHack; + // Move this threshold to resource. + // TODO: Device specific parameter would be better for device specific hack? + private static final float PHANTOM_SUDDEN_MOVE_THRESHOLD = 0.25f; // in keyWidth + // This hack might be device specific. + private static final boolean sNeedsProximateBogusDownMoveUpEventHack = true; - private static final ArrayList<PointerTracker> sTrackers = new ArrayList<PointerTracker>(); + private static final ArrayList<PointerTracker> sTrackers = CollectionUtils.newArrayList(); private static PointerTrackerQueue sPointerTrackerQueue; public final int mPointerId; @@ -122,8 +166,126 @@ public class PointerTracker { private KeyboardActionListener mListener = EMPTY_LISTENER; private Keyboard mKeyboard; - private int mKeyQuarterWidthSquared; - private final TextView mKeyPreviewText; + private int mPhantonSuddenMoveThreshold; + private final BogusMoveEventDetector mBogusMoveEventDetector = new BogusMoveEventDetector(); + + private boolean mIsDetectingGesture = false; // per PointerTracker. + private static boolean sInGesture = false; + private static long sGestureFirstDownTime; + private static TimeRecorder sTimeRecorder; + private static final InputPointers sAggregratedPointers = new InputPointers( + GestureStroke.DEFAULT_CAPACITY); + private static int sLastRecognitionPointSize = 0; // synchronized using sAggregratedPointers + private static long sLastRecognitionTime = 0; // synchronized using sAggregratedPointers + + static final class BogusMoveEventDetector { + // Move these thresholds to resource. + // These thresholds' unit is a diagonal length of a key. + private static final float BOGUS_MOVE_ACCUMULATED_DISTANCE_THRESHOLD = 0.53f; + private static final float BOGUS_MOVE_RADIUS_THRESHOLD = 1.14f; + + private int mAccumulatedDistanceThreshold; + private int mRadiusThreshold; + + // Accumulated distance from actual and artificial down keys. + /* package */ int mAccumulatedDistanceFromDownKey; + private int mActualDownX; + private int mActualDownY; + + public void setKeyboardGeometry(final int keyWidth, final int keyHeight) { + final float keyDiagonal = (float)Math.hypot(keyWidth, keyHeight); + mAccumulatedDistanceThreshold = (int)( + keyDiagonal * BOGUS_MOVE_ACCUMULATED_DISTANCE_THRESHOLD); + mRadiusThreshold = (int)(keyDiagonal * BOGUS_MOVE_RADIUS_THRESHOLD); + } + + public void onActualDownEvent(final int x, final int y) { + mActualDownX = x; + mActualDownY = y; + } + + public void onDownKey() { + mAccumulatedDistanceFromDownKey = 0; + } + + public void onMoveKey(final int distance) { + mAccumulatedDistanceFromDownKey += distance; + } + + public boolean hasTraveledLongDistance(final int x, final int y) { + final int dx = Math.abs(x - mActualDownX); + final int dy = Math.abs(y - mActualDownY); + // A bogus move event should be a horizontal movement. A vertical movement might be + // a sloppy typing and should be ignored. + return dx >= dy && mAccumulatedDistanceFromDownKey >= mAccumulatedDistanceThreshold; + } + + /* package */ int getDistanceFromDownEvent(final int x, final int y) { + return getDistance(x, y, mActualDownX, mActualDownY); + } + + public boolean isCloseToActualDownEvent(final int x, final int y) { + return getDistanceFromDownEvent(x, y) < mRadiusThreshold; + } + } + + static final class TimeRecorder { + private final int mSuppressKeyPreviewAfterBatchInputDuration; + private final int mStaticTimeThresholdAfterFastTyping; // msec + private long mLastTypingTime; + private long mLastLetterTypingTime; + private long mLastBatchInputTime; + + public TimeRecorder(final PointerTrackerParams pointerTrackerParams, + final GestureStrokeParams gestureStrokeParams) { + mSuppressKeyPreviewAfterBatchInputDuration = + pointerTrackerParams.mSuppressKeyPreviewAfterBatchInputDuration; + mStaticTimeThresholdAfterFastTyping = + gestureStrokeParams.mStaticTimeThresholdAfterFastTyping; + } + + public boolean isInFastTyping(final long eventTime) { + final long elapsedTimeSinceLastLetterTyping = eventTime - mLastLetterTypingTime; + return elapsedTimeSinceLastLetterTyping < mStaticTimeThresholdAfterFastTyping; + } + + private boolean wasLastInputTyping() { + return mLastTypingTime >= mLastBatchInputTime; + } + + public void onCodeInput(final int code, final long eventTime) { + // Record the letter typing time when + // 1. Letter keys are typed successively without any batch input in between. + // 2. A letter key is typed within the threshold time since the last any key typing. + // 3. A non-letter key is typed within the threshold time since the last letter key + // typing. + if (Character.isLetter(code)) { + if (wasLastInputTyping() + || eventTime - mLastTypingTime < mStaticTimeThresholdAfterFastTyping) { + mLastLetterTypingTime = eventTime; + } + } else { + if (eventTime - mLastLetterTypingTime < mStaticTimeThresholdAfterFastTyping) { + // This non-letter typing should be treated as a part of fast typing. + mLastLetterTypingTime = eventTime; + } + } + mLastTypingTime = eventTime; + } + + public void onEndBatchInput(final long eventTime) { + mLastBatchInputTime = eventTime; + } + + public long getLastLetterTypingTime() { + return mLastLetterTypingTime; + } + + public boolean needsToSuppressKeyPreviewPopup(final long eventTime) { + return !wasLastInputTyping() + && eventTime - mLastBatchInputTime < mSuppressKeyPreviewAfterBatchInputDuration; + } + } // The position and time at which first down event occurred. private long mDownTime; @@ -148,22 +310,21 @@ public class PointerTracker { // true if this pointer has been long-pressed and is showing a more keys panel. private boolean mIsShowingMoreKeysPanel; - // true if this pointer is repeatable key - private boolean mIsRepeatableKey; - - // true if this pointer is in sliding key input + // true if this pointer is in a sliding key input. boolean mIsInSlidingKeyInput; + // true if this pointer is in a sliding key input from a modifier key, + // so that further modifier keys should be ignored. + boolean mIsInSlidingKeyInputFromModifier; - // true if sliding key is allowed. + // true if a sliding key input is allowed. private boolean mIsAllowedSlidingKeyInput; - // ignore modifier key if true - private boolean mIgnoreModifierKey; - // Empty {@link KeyboardActionListener} private static final KeyboardActionListener EMPTY_LISTENER = new KeyboardActionListener.Adapter(); + private final GestureStrokeWithPreviewPoints mGestureStrokeWithPreviewPoints; + public static void init(boolean hasDistinctMultitouch, boolean needsPhantomSuddenMoveEventHack) { if (hasDistinctMultitouch) { @@ -172,17 +333,36 @@ public class PointerTracker { sPointerTrackerQueue = null; } sNeedsPhantomSuddenMoveEventHack = needsPhantomSuddenMoveEventHack; + sParams = PointerTrackerParams.DEFAULT; + sGestureStrokeParams = GestureStrokeParams.DEFAULT; + sTimeRecorder = new TimeRecorder(sParams, sGestureStrokeParams); + } + + public static void setParameters(final TypedArray mainKeyboardViewAttr) { + sParams = new PointerTrackerParams(mainKeyboardViewAttr); + sGestureStrokeParams = new GestureStrokeParams(mainKeyboardViewAttr); + sTimeRecorder = new TimeRecorder(sParams, sGestureStrokeParams); + } + + private static void updateGestureHandlingMode() { + sShouldHandleGesture = sMainDictionaryAvailable + && sGestureHandlingEnabledByInputField + && sGestureHandlingEnabledByUser + && !AccessibilityUtils.getInstance().isTouchExplorationEnabled(); + } - setParameters(LatinKeyboardView.PointerTrackerParams.DEFAULT); + // Note that this method is called from a non-UI thread. + public static void setMainDictionaryAvailability(final boolean mainDictionaryAvailable) { + sMainDictionaryAvailable = mainDictionaryAvailable; + updateGestureHandlingMode(); } - public static void setParameters(LatinKeyboardView.PointerTrackerParams params) { - sParams = params; - sTouchNoiseThresholdDistanceSquared = (int)( - params.mTouchNoiseThresholdDistance * params.mTouchNoiseThresholdDistance); + public static void setGestureHandlingEnabledByUser(final boolean gestureHandlingEnabledByUser) { + sGestureHandlingEnabledByUser = gestureHandlingEnabledByUser; + updateGestureHandlingMode(); } - public static PointerTracker getPointerTracker(final int id, KeyEventHandler handler) { + public static PointerTracker getPointerTracker(final int id, final KeyEventHandler handler) { final ArrayList<PointerTracker> trackers = sTrackers; // Create pointer trackers until we can get 'id+1'-th tracker, if needed. @@ -198,53 +378,59 @@ public class PointerTracker { return sPointerTrackerQueue != null ? sPointerTrackerQueue.isAnyInSlidingKeyInput() : false; } - public static void setKeyboardActionListener(KeyboardActionListener listener) { - for (final PointerTracker tracker : sTrackers) { + public static void setKeyboardActionListener(final KeyboardActionListener listener) { + final int trackersSize = sTrackers.size(); + for (int i = 0; i < trackersSize; ++i) { + final PointerTracker tracker = sTrackers.get(i); tracker.mListener = listener; } } - public static void setKeyDetector(KeyDetector keyDetector) { - for (final PointerTracker tracker : sTrackers) { + public static void setKeyDetector(final KeyDetector keyDetector) { + final int trackersSize = sTrackers.size(); + for (int i = 0; i < trackersSize; ++i) { + final PointerTracker tracker = sTrackers.get(i); tracker.setKeyDetectorInner(keyDetector); // Mark that keyboard layout has been changed. tracker.mKeyboardLayoutHasBeenChanged = true; } + final Keyboard keyboard = keyDetector.getKeyboard(); + sGestureHandlingEnabledByInputField = !keyboard.mId.passwordInput(); + updateGestureHandlingMode(); } - public static void dismissAllKeyPreviews() { - for (final PointerTracker tracker : sTrackers) { - tracker.getKeyPreviewText().setVisibility(View.INVISIBLE); + public static void setReleasedKeyGraphicsToAllKeys() { + final int trackersSize = sTrackers.size(); + for (int i = 0; i < trackersSize; ++i) { + final PointerTracker tracker = sTrackers.get(i); tracker.setReleasedKeyGraphics(tracker.mCurrentKey); } } - public PointerTracker(int id, KeyEventHandler handler) { - if (handler == null) + private PointerTracker(final int id, final KeyEventHandler handler) { + if (handler == null) { throw new NullPointerException(); + } mPointerId = id; + mGestureStrokeWithPreviewPoints = new GestureStrokeWithPreviewPoints( + id, sGestureStrokeParams); setKeyDetectorInner(handler.getKeyDetector()); mListener = handler.getKeyboardActionListener(); mDrawingProxy = handler.getDrawingProxy(); mTimerProxy = handler.getTimerProxy(); - mKeyPreviewText = mDrawingProxy.inflateKeyPreviewText(); - } - - public TextView getKeyPreviewText() { - return mKeyPreviewText; } // Returns true if keyboard has been changed by this callback. - private boolean callListenerOnPressAndCheckKeyboardLayoutChange(Key key) { - final boolean ignoreModifierKey = mIgnoreModifierKey && key.isModifier(); - if (DEBUG_LISTENER) { - Log.d(TAG, "onPress : " + KeyDetector.printableCode(key) - + " ignoreModifier=" + ignoreModifierKey - + " enabled=" + key.isEnabled()); + private boolean callListenerOnPressAndCheckKeyboardLayoutChange(final Key key) { + if (sInGesture) { + return false; } - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.pointerTracker_callListenerOnPressAndCheckKeyboardLayoutChange(key, - ignoreModifierKey); + final boolean ignoreModifierKey = mIsInSlidingKeyInputFromModifier && key.isModifier(); + if (DEBUG_LISTENER) { + Log.d(TAG, String.format("[%d] onPress : %s%s%s", mPointerId, + KeyDetector.printableCode(key), + ignoreModifierKey ? " ignoreModifier" : "", + key.isEnabled() ? "" : " disabled")); } if (ignoreModifierKey) { return false; @@ -253,9 +439,7 @@ public class PointerTracker { mListener.onPressKey(key.mCode); final boolean keyboardLayoutHasBeenChanged = mKeyboardLayoutHasBeenChanged; mKeyboardLayoutHasBeenChanged = false; - if (!key.altCodeWhileTyping() && !key.isModifier()) { - mTimerProxy.startTypingStateTimer(); - } + mTimerProxy.startTypingStateTimer(key); return keyboardLayoutHasBeenChanged; } return false; @@ -263,15 +447,17 @@ public class PointerTracker { // Note that we need primaryCode argument because the keyboard may in shifted state and the // primaryCode is different from {@link Key#mCode}. - private void callListenerOnCodeInput(Key key, int primaryCode, int x, int y) { - final boolean ignoreModifierKey = mIgnoreModifierKey && key.isModifier(); + private void callListenerOnCodeInput(final Key key, final int primaryCode, final int x, + final int y, final long eventTime) { + final boolean ignoreModifierKey = mIsInSlidingKeyInputFromModifier && key.isModifier(); final boolean altersCode = key.altCodeWhileTyping() && mTimerProxy.isTypingState(); - final int code = altersCode ? key.mAltCode : primaryCode; + final int code = altersCode ? key.getAltCode() : primaryCode; if (DEBUG_LISTENER) { - Log.d(TAG, "onCodeInput: " + Keyboard.printableCode(code) + " text=" + key.mOutputText - + " x=" + x + " y=" + y - + " ignoreModifier=" + ignoreModifierKey + " altersCode=" + altersCode - + " enabled=" + key.isEnabled()); + final String output = code == Keyboard.CODE_OUTPUT_TEXT + ? key.getOutputText() : Keyboard.printableCode(code); + Log.d(TAG, String.format("[%d] onCodeInput: %4d %4d %s%s%s", mPointerId, x, y, + output, ignoreModifierKey ? " ignoreModifier" : "", + altersCode ? " altersCode" : "", key.isEnabled() ? "" : " disabled")); } if (ProductionFlag.IS_EXPERIMENTAL) { ResearchLogger.pointerTracker_callListenerOnCodeInput(key, x, y, ignoreModifierKey, @@ -282,22 +468,28 @@ public class PointerTracker { } // Even if the key is disabled, it should respond if it is in the altCodeWhileTyping state. if (key.isEnabled() || altersCode) { + sTimeRecorder.onCodeInput(code, eventTime); if (code == Keyboard.CODE_OUTPUT_TEXT) { - mListener.onTextInput(key.mOutputText); + mListener.onTextInput(key.getOutputText()); } else if (code != Keyboard.CODE_UNSPECIFIED) { mListener.onCodeInput(code, x, y); } } } - // Note that we need primaryCode argument because the keyboard may in shifted state and the + // Note that we need primaryCode argument because the keyboard may be in shifted state and the // primaryCode is different from {@link Key#mCode}. - private void callListenerOnRelease(Key key, int primaryCode, boolean withSliding) { - final boolean ignoreModifierKey = mIgnoreModifierKey && key.isModifier(); + private void callListenerOnRelease(final Key key, final int primaryCode, + final boolean withSliding) { + if (sInGesture) { + return; + } + final boolean ignoreModifierKey = mIsInSlidingKeyInputFromModifier && key.isModifier(); if (DEBUG_LISTENER) { - Log.d(TAG, "onRelease : " + Keyboard.printableCode(primaryCode) - + " sliding=" + withSliding + " ignoreModifier=" + ignoreModifierKey - + " enabled="+ key.isEnabled()); + Log.d(TAG, String.format("[%d] onRelease : %s%s%s%s", mPointerId, + Keyboard.printableCode(primaryCode), + withSliding ? " sliding" : "", ignoreModifierKey ? " ignoreModifier" : "", + key.isEnabled() ? "": " disabled")); } if (ProductionFlag.IS_EXPERIMENTAL) { ResearchLogger.pointerTracker_callListenerOnRelease(key, primaryCode, withSliding, @@ -312,21 +504,37 @@ public class PointerTracker { } private void callListenerOnCancelInput() { - if (DEBUG_LISTENER) - Log.d(TAG, "onCancelInput"); + if (DEBUG_LISTENER) { + Log.d(TAG, String.format("[%d] onCancelInput", mPointerId)); + } if (ProductionFlag.IS_EXPERIMENTAL) { ResearchLogger.pointerTracker_callListenerOnCancelInput(); } mListener.onCancelInput(); } - private void setKeyDetectorInner(KeyDetector keyDetector) { + private void setKeyDetectorInner(final KeyDetector keyDetector) { + final Keyboard keyboard = keyDetector.getKeyboard(); + if (keyDetector == mKeyDetector && keyboard == mKeyboard) { + return; + } mKeyDetector = keyDetector; mKeyboard = keyDetector.getKeyboard(); - final int keyQuarterWidth = mKeyboard.mMostCommonKeyWidth / 4; - mKeyQuarterWidthSquared = keyQuarterWidth * keyQuarterWidth; + final int keyWidth = mKeyboard.mMostCommonKeyWidth; + final int keyHeight = mKeyboard.mMostCommonKeyHeight; + mGestureStrokeWithPreviewPoints.setKeyboardGeometry(keyWidth); + final Key newKey = mKeyDetector.detectHitKey(mKeyX, mKeyY); + if (newKey != mCurrentKey) { + if (mDrawingProxy != null) { + setReleasedKeyGraphics(mCurrentKey); + } + // Keep {@link #mCurrentKey} that comes from previous keyboard. + } + mPhantonSuddenMoveThreshold = (int)(keyWidth * PHANTOM_SUDDEN_MOVE_THRESHOLD); + mBogusMoveEventDetector.setKeyboardGeometry(keyWidth, keyHeight); } + @Override public boolean isInSlidingKeyInput() { return mIsInSlidingKeyInput; } @@ -335,15 +543,16 @@ public class PointerTracker { return mCurrentKey; } + @Override public boolean isModifier() { return mCurrentKey != null && mCurrentKey.isModifier(); } - public Key getKeyOn(int x, int y) { + public Key getKeyOn(final int x, final int y) { return mKeyDetector.detectHitKey(x, y); } - private void setReleasedKeyGraphics(Key key) { + private void setReleasedKeyGraphics(final Key key) { mDrawingProxy.dismissKeyPreview(this); if (key == null) { return; @@ -361,20 +570,25 @@ public class PointerTracker { } if (key.altCodeWhileTyping()) { - final int altCode = key.mAltCode; + final int altCode = key.getAltCode(); final Key altKey = mKeyboard.getKey(altCode); if (altKey != null) { updateReleaseKeyGraphics(altKey); } for (final Key k : mKeyboard.mAltCodeKeysWhileTyping) { - if (k != key && k.mAltCode == altCode) { + if (k != key && k.getAltCode() == altCode) { updateReleaseKeyGraphics(k); } } } } - private void setPressedKeyGraphics(Key key) { + private static boolean needsToSuppressKeyPreviewPopup(final long eventTime) { + if (!sShouldHandleGesture) return false; + return sTimeRecorder.needsToSuppressKeyPreviewPopup(eventTime); + } + + private void setPressedKeyGraphics(final Key key, final long eventTime) { if (key == null) { return; } @@ -386,7 +600,7 @@ public class PointerTracker { return; } - if (!key.noKeyPreview()) { + if (!key.noKeyPreview() && !sInGesture && !needsToSuppressKeyPreviewPopup(eventTime)) { mDrawingProxy.showKeyPreview(this); } updatePressKeyGraphics(key); @@ -400,29 +614,33 @@ public class PointerTracker { } if (key.altCodeWhileTyping() && mTimerProxy.isTypingState()) { - final int altCode = key.mAltCode; + final int altCode = key.getAltCode(); final Key altKey = mKeyboard.getKey(altCode); if (altKey != null) { updatePressKeyGraphics(altKey); } for (final Key k : mKeyboard.mAltCodeKeysWhileTyping) { - if (k != key && k.mAltCode == altCode) { + if (k != key && k.getAltCode() == altCode) { updatePressKeyGraphics(k); } } } } - private void updateReleaseKeyGraphics(Key key) { + private void updateReleaseKeyGraphics(final Key key) { key.onReleased(); mDrawingProxy.invalidateKey(key); } - private void updatePressKeyGraphics(Key key) { + private void updatePressKeyGraphics(final Key key) { key.onPressed(); mDrawingProxy.invalidateKey(key); } + public GestureStrokeWithPreviewPoints getGestureStrokeWithPreviewPoints() { + return mGestureStrokeWithPreviewPoints; + } + public int getLastX() { return mLastX; } @@ -435,30 +653,100 @@ public class PointerTracker { return mDownTime; } - private Key onDownKey(int x, int y, long eventTime) { + private Key onDownKey(final int x, final int y, final long eventTime) { mDownTime = eventTime; + mBogusMoveEventDetector.onDownKey(); return onMoveToNewKey(onMoveKeyInternal(x, y), x, y); } - private Key onMoveKeyInternal(int x, int y) { + static int getDistance(final int x1, final int y1, final int x2, final int y2) { + return (int)Math.hypot(x1 - x2, y1 - y2); + } + + private Key onMoveKeyInternal(final int x, final int y) { + mBogusMoveEventDetector.onMoveKey(getDistance(x, y, mLastX, mLastY)); mLastX = x; mLastY = y; return mKeyDetector.detectHitKey(x, y); } - private Key onMoveKey(int x, int y) { + private Key onMoveKey(final int x, final int y) { return onMoveKeyInternal(x, y); } - private Key onMoveToNewKey(Key newKey, int x, int y) { + private Key onMoveToNewKey(final Key newKey, final int x, final int y) { mCurrentKey = newKey; mKeyX = x; mKeyY = y; return newKey; } - public void processMotionEvent(int action, int x, int y, long eventTime, - KeyEventHandler handler) { + private static int getActivePointerTrackerCount() { + return (sPointerTrackerQueue == null) ? 1 : sPointerTrackerQueue.size(); + } + + private void mayStartBatchInput(final Key key) { + if (sInGesture || !mGestureStrokeWithPreviewPoints.isStartOfAGesture()) { + return; + } + if (key == null || !Character.isLetter(key.mCode)) { + return; + } + if (DEBUG_LISTENER) { + Log.d(TAG, String.format("[%d] onStartBatchInput", mPointerId)); + } + sInGesture = true; + synchronized (sAggregratedPointers) { + sAggregratedPointers.reset(); + sLastRecognitionPointSize = 0; + sLastRecognitionTime = 0; + mListener.onStartBatchInput(); + } + final boolean isOldestTracker = sPointerTrackerQueue.getOldestElement() == this; + mDrawingProxy.showGesturePreviewTrail(this, isOldestTracker); + } + + private void mayUpdateBatchInput(final long eventTime, final Key key) { + if (key != null) { + synchronized (sAggregratedPointers) { + final GestureStroke stroke = mGestureStrokeWithPreviewPoints; + stroke.appendIncrementalBatchPoints(sAggregratedPointers); + final int size = sAggregratedPointers.getPointerSize(); + if (size > sLastRecognitionPointSize + && stroke.hasRecognitionTimePast(eventTime, sLastRecognitionTime)) { + sLastRecognitionPointSize = size; + sLastRecognitionTime = eventTime; + if (DEBUG_LISTENER) { + Log.d(TAG, String.format("[%d] onUpdateBatchInput: batchPoints=%d", + mPointerId, size)); + } + mListener.onUpdateBatchInput(sAggregratedPointers); + } + } + } + final boolean isOldestTracker = sPointerTrackerQueue.getOldestElement() == this; + mDrawingProxy.showGesturePreviewTrail(this, isOldestTracker); + } + + private void mayEndBatchInput(final long eventTime) { + synchronized (sAggregratedPointers) { + mGestureStrokeWithPreviewPoints.appendAllBatchPoints(sAggregratedPointers); + if (getActivePointerTrackerCount() == 1) { + if (DEBUG_LISTENER) { + Log.d(TAG, String.format("[%d] onEndBatchInput : batchPoints=%d", + mPointerId, sAggregratedPointers.getPointerSize())); + } + sInGesture = false; + sTimeRecorder.onEndBatchInput(eventTime); + mListener.onEndBatchInput(sAggregratedPointers); + } + } + final boolean isOldestTracker = sPointerTrackerQueue.getOldestElement() == this; + mDrawingProxy.showGesturePreviewTrail(this, isOldestTracker); + } + + public void processMotionEvent(final int action, final int x, final int y, final long eventTime, + final KeyEventHandler handler) { switch (action) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_POINTER_DOWN: @@ -469,7 +757,7 @@ public class PointerTracker { onUpEvent(x, y, eventTime); break; case MotionEvent.ACTION_MOVE: - onMoveEvent(x, y, eventTime); + onMoveEvent(x, y, eventTime, null); break; case MotionEvent.ACTION_CANCEL: onCancelEvent(x, y, eventTime); @@ -477,9 +765,11 @@ public class PointerTracker { } } - public void onDownEvent(int x, int y, long eventTime, KeyEventHandler handler) { - if (DEBUG_EVENT) + public void onDownEvent(final int x, final int y, final long eventTime, + final KeyEventHandler handler) { + if (DEBUG_EVENT) { printTouchEvent("onDownEvent:", x, y, eventTime); + } mDrawingProxy = handler.getDrawingProxy(); mTimerProxy = handler.getTimerProxy(); @@ -488,24 +778,24 @@ public class PointerTracker { // Naive up-to-down noise filter. final long deltaT = eventTime - mUpTime; if (deltaT < sParams.mTouchNoiseThresholdTime) { - final int dx = x - mLastX; - final int dy = y - mLastY; - final int distanceSquared = (dx * dx + dy * dy); - if (distanceSquared < sTouchNoiseThresholdDistanceSquared) { + final int distance = getDistance(x, y, mLastX, mLastY); + if (distance < sParams.mTouchNoiseThresholdDistance) { if (DEBUG_MODE) - Log.w(TAG, "onDownEvent: ignore potential noise: time=" + deltaT - + " distance=" + distanceSquared); + Log.w(TAG, String.format("[%d] onDownEvent:" + + " ignore potential noise: time=%d distance=%d", + mPointerId, deltaT, distance)); if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.pointerTracker_onDownEvent(deltaT, distanceSquared); + ResearchLogger.pointerTracker_onDownEvent(deltaT, distance * distance); } mKeyAlreadyProcessed = true; return; } } + final Key key = getKeyOn(x, y); + mBogusMoveEventDetector.onActualDownEvent(x, y); final PointerTrackerQueue queue = sPointerTrackerQueue; if (queue != null) { - final Key key = getKeyOn(x, y); if (key != null && key.isModifier()) { // Before processing a down event of modifier key, all pointers already being // tracked should be released. @@ -514,9 +804,22 @@ public class PointerTracker { queue.add(this); } onDownEventInternal(x, y, eventTime); + if (!sShouldHandleGesture) { + return; + } + // A gesture should start only from the letter key. + mIsDetectingGesture = (mKeyboard != null) && mKeyboard.mId.isAlphabetKeyboard() + && !mIsShowingMoreKeysPanel && key != null && Keyboard.isLetterCode(key.mCode); + if (mIsDetectingGesture) { + if (getActivePointerTrackerCount() == 1) { + sGestureFirstDownTime = eventTime; + } + mGestureStrokeWithPreviewPoints.onDownEvent(x, y, eventTime, sGestureFirstDownTime, + sTimeRecorder.getLastLetterTypingTime()); + } } - private void onDownEventInternal(int x, int y, long eventTime) { + private void onDownEventInternal(final int x, final int y, final long eventTime) { Key key = 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's KeyDetector always allows sliding input. @@ -525,9 +828,7 @@ public class PointerTracker { || mKeyDetector.alwaysAllowsSlidingInput(); mKeyboardLayoutHasBeenChanged = false; mKeyAlreadyProcessed = false; - mIsRepeatableKey = false; - mIsInSlidingKeyInput = false; - mIgnoreModifierKey = false; + resetSlidingKeyInput(); if (key != null) { // This onPress call may have changed keyboard layout. Those cases are detected at // {@link #setKeyboard}. In those cases, we should update key according to the new @@ -538,27 +839,75 @@ public class PointerTracker { startRepeatKey(key); startLongPressTimer(key); - setPressedKeyGraphics(key); + setPressedKeyGraphics(key, eventTime); } } - private void startSlidingKeyInput(Key key) { + private void startSlidingKeyInput(final Key key) { if (!mIsInSlidingKeyInput) { - mIgnoreModifierKey = key.isModifier(); + mIsInSlidingKeyInputFromModifier = key.isModifier(); } mIsInSlidingKeyInput = true; } - public void onMoveEvent(int x, int y, long eventTime) { - if (DEBUG_MOVE_EVENT) + private void resetSlidingKeyInput() { + mIsInSlidingKeyInput = false; + mIsInSlidingKeyInputFromModifier = false; + } + + private void onGestureMoveEvent(final int x, final int y, final long eventTime, + final boolean isMajorEvent, final Key key) { + final int gestureTime = (int)(eventTime - sGestureFirstDownTime); + if (mIsDetectingGesture) { + mGestureStrokeWithPreviewPoints.addPoint(x, y, gestureTime, isMajorEvent); + mayStartBatchInput(key); + if (sInGesture) { + mayUpdateBatchInput(eventTime, key); + } + } + } + + public void onMoveEvent(final int x, final int y, final long eventTime, final MotionEvent me) { + if (DEBUG_MOVE_EVENT) { printTouchEvent("onMoveEvent:", x, y, eventTime); - if (mKeyAlreadyProcessed) + } + if (mKeyAlreadyProcessed) { return; + } + + if (sShouldHandleGesture && me != null) { + // Add historical points to gesture path. + final int pointerIndex = me.findPointerIndex(mPointerId); + final int historicalSize = me.getHistorySize(); + for (int h = 0; h < historicalSize; h++) { + final int historicalX = (int)me.getHistoricalX(pointerIndex, h); + final int historicalY = (int)me.getHistoricalY(pointerIndex, h); + final long historicalTime = me.getHistoricalEventTime(h); + onGestureMoveEvent(historicalX, historicalY, historicalTime, + false /* isMajorEvent */, null); + } + } + onMoveEventInternal(x, y, eventTime); + } + + private void onMoveEventInternal(final int x, final int y, final long eventTime) { final int lastX = mLastX; final int lastY = mLastY; final Key oldKey = mCurrentKey; Key key = onMoveKey(x, y); + + if (sShouldHandleGesture) { + // Register move event on gesture tracker. + onGestureMoveEvent(x, y, eventTime, true /* isMajorEvent */, key); + if (sInGesture) { + mTimerProxy.cancelLongPressTimer(); + mCurrentKey = null; + setReleasedKeyGraphics(oldKey); + return; + } + } + if (key != null) { if (oldKey == null) { // The pointer has been slid in to the new key, but the finger was not on any keys. @@ -571,8 +920,8 @@ public class PointerTracker { } onMoveToNewKey(key, x, y); startLongPressTimer(key); - setPressedKeyGraphics(key); - } else if (isMajorEnoughMoveToBeOnNewKey(x, y, key)) { + setPressedKeyGraphics(key, eventTime); + } else if (isMajorEnoughMoveToBeOnNewKey(x, y, eventTime, key)) { // 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. @@ -590,34 +939,75 @@ public class PointerTracker { } onMoveToNewKey(key, x, y); startLongPressTimer(key); - setPressedKeyGraphics(key); + setPressedKeyGraphics(key, eventTime); } 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 + // HACK: On some devices, quick successive touches may be reported as a sudden + // move by touch panel firmware. This hack detects such cases 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; + // TODO: Should find a way to balance gesture detection and this hack. if (sNeedsPhantomSuddenMoveEventHack - && lastMoveSquared >= mKeyQuarterWidthSquared) { + && getDistance(x, y, lastX, lastY) >= mPhantonSuddenMoveThreshold) { if (DEBUG_MODE) { - Log.w(TAG, String.format("onMoveEvent:" - + " phantom sudden move event is translated to " - + "up[%d,%d]/down[%d,%d] events", lastX, lastY, x, y)); + Log.w(TAG, String.format("[%d] onMoveEvent:" + + " phantom sudden move event (distance=%d) is translated to " + + "up[%d,%d,%s]/down[%d,%d,%s] events", mPointerId, + getDistance(x, y, lastX, lastY), + lastX, lastY, Keyboard.printableCode(oldKey.mCode), + x, y, Keyboard.printableCode(key.mCode))); } + // TODO: This should be moved to outside of this nested if-clause? if (ProductionFlag.IS_EXPERIMENTAL) { ResearchLogger.pointerTracker_onMoveEvent(x, y, lastX, lastY); } - onUpEventInternal(); + onUpEventInternal(eventTime); + onDownEventInternal(x, y, eventTime); + } + // HACK: On some devices, quick successive proximate touches may be reported as + // a bogus down-move-up event by touch panel firmware. This hack detects such + // cases and breaks these events into separate up and down events. + else if (sNeedsProximateBogusDownMoveUpEventHack + && sTimeRecorder.isInFastTyping(eventTime) + && mBogusMoveEventDetector.isCloseToActualDownEvent(x, y)) { + if (DEBUG_MODE) { + final float keyDiagonal = (float)Math.hypot( + mKeyboard.mMostCommonKeyWidth, mKeyboard.mMostCommonKeyHeight); + final float radiusRatio = + mBogusMoveEventDetector.getDistanceFromDownEvent(x, y) + / keyDiagonal; + Log.w(TAG, String.format("[%d] onMoveEvent:" + + " bogus down-move-up event (raidus=%.2f key diagonal) is " + + " translated to up[%d,%d,%s]/down[%d,%d,%s] events", + mPointerId, radiusRatio, + lastX, lastY, Keyboard.printableCode(oldKey.mCode), + x, y, Keyboard.printableCode(key.mCode))); + } + onUpEventInternal(eventTime); onDownEventInternal(x, y, eventTime); } else { - mKeyAlreadyProcessed = true; + // HACK: If there are currently multiple touches, register the key even if + // the finger slides off the key. This defends against noise from some + // touch panels when there are close multiple touches. + // Caveat: When in chording input mode with a modifier key, we don't use + // this hack. + if (getActivePointerTrackerCount() > 1 && sPointerTrackerQueue != null + && !sPointerTrackerQueue.hasModifierKeyOlderThan(this)) { + if (DEBUG_MODE) { + Log.w(TAG, String.format("[%d] onMoveEvent:" + + " detected sliding finger while multi touching", + mPointerId)); + } + onUpEvent(x, y, eventTime); + mKeyAlreadyProcessed = true; + } + if (!mIsDetectingGesture) { + mKeyAlreadyProcessed = true; + } setReleasedKeyGraphics(oldKey); } } } } else { - if (oldKey != null && isMajorEnoughMoveToBeOnNewKey(x, y, key)) { + if (oldKey != null && isMajorEnoughMoveToBeOnNewKey(x, y, eventTime, key)) { // The pointer has been slid out from the previous key, we must call onRelease() to // notify that the previous key has been released. setReleasedKeyGraphics(oldKey); @@ -627,60 +1017,82 @@ public class PointerTracker { if (mIsAllowedSlidingKeyInput) { onMoveToNewKey(key, x, y); } else { - mKeyAlreadyProcessed = true; + if (!mIsDetectingGesture) { + mKeyAlreadyProcessed = true; + } } } } } - public void onUpEvent(int x, int y, long eventTime) { - if (DEBUG_EVENT) + public void onUpEvent(final int x, final int y, final long eventTime) { + if (DEBUG_EVENT) { printTouchEvent("onUpEvent :", x, y, eventTime); + } final PointerTrackerQueue queue = sPointerTrackerQueue; if (queue != null) { - if (mCurrentKey != null && mCurrentKey.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); + if (!sInGesture) { + if (mCurrentKey != null && mCurrentKey.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); + } } + } + onUpEventInternal(eventTime); + if (queue != null) { queue.remove(this); } - onUpEventInternal(); } // Let this pointer tracker know that one of newer-than-this pointer trackers got an up event. // This pointer tracker needs to keep the key top graphics "pressed", but needs to get a // "virtual" up event. - public void onPhantomUpEvent(int x, int y, long eventTime) { - if (DEBUG_EVENT) - printTouchEvent("onPhntEvent:", x, y, eventTime); - onUpEventInternal(); + @Override + public void onPhantomUpEvent(final long eventTime) { + if (DEBUG_EVENT) { + printTouchEvent("onPhntEvent:", getLastX(), getLastY(), eventTime); + } + onUpEventInternal(eventTime); mKeyAlreadyProcessed = true; } - private void onUpEventInternal() { + private void onUpEventInternal(final long eventTime) { mTimerProxy.cancelKeyTimers(); - mIsInSlidingKeyInput = false; + resetSlidingKeyInput(); + mIsDetectingGesture = false; + final Key currentKey = mCurrentKey; + mCurrentKey = null; // Release the last pressed key. - setReleasedKeyGraphics(mCurrentKey); + setReleasedKeyGraphics(currentKey); if (mIsShowingMoreKeysPanel) { mDrawingProxy.dismissMoreKeysPanel(); mIsShowingMoreKeysPanel = false; } - if (mKeyAlreadyProcessed) + + if (sInGesture) { + if (currentKey != null) { + callListenerOnRelease(currentKey, currentKey.mCode, true); + } + mayEndBatchInput(eventTime); + return; + } + + if (mKeyAlreadyProcessed) { return; - if (!mIsRepeatableKey) { - detectAndSendKey(mCurrentKey, mKeyX, mKeyY); + } + if (currentKey != null && !currentKey.isRepeatable()) { + detectAndSendKey(currentKey, mKeyX, mKeyY, eventTime); } } - public void onShowMoreKeysPanel(int x, int y, KeyEventHandler handler) { + public void onShowMoreKeysPanel(final int x, final int y, final KeyEventHandler handler) { onLongPressed(); - onDownEvent(x, y, SystemClock.uptimeMillis(), handler); mIsShowingMoreKeysPanel = true; + onDownEvent(x, y, SystemClock.uptimeMillis(), handler); } public void onLongPressed() { @@ -692,9 +1104,10 @@ public class PointerTracker { } } - public void onCancelEvent(int x, int y, long eventTime) { - if (DEBUG_EVENT) + public void onCancelEvent(final int x, final int y, final long eventTime) { + if (DEBUG_EVENT) { printTouchEvent("onCancelEvt:", x, y, eventTime); + } final PointerTrackerQueue queue = sPointerTrackerQueue; if (queue != null) { @@ -707,71 +1120,91 @@ public class PointerTracker { private void onCancelEventInternal() { mTimerProxy.cancelKeyTimers(); setReleasedKeyGraphics(mCurrentKey); - mIsInSlidingKeyInput = false; + resetSlidingKeyInput(); if (mIsShowingMoreKeysPanel) { mDrawingProxy.dismissMoreKeysPanel(); mIsShowingMoreKeysPanel = false; } } - private void startRepeatKey(Key key) { - if (key != null && key.isRepeatable()) { + private void startRepeatKey(final Key key) { + if (key != null && key.isRepeatable() && !sInGesture) { onRegisterKey(key); mTimerProxy.startKeyRepeatTimer(this); - mIsRepeatableKey = true; - } else { - mIsRepeatableKey = false; } } - public void onRegisterKey(Key key) { + public void onRegisterKey(final Key key) { if (key != null) { - detectAndSendKey(key, key.mX, key.mY); - if (!key.altCodeWhileTyping() && !key.isModifier()) { - mTimerProxy.startTypingStateTimer(); - } + detectAndSendKey(key, key.mX, key.mY, SystemClock.uptimeMillis()); + mTimerProxy.startTypingStateTimer(key); } } - private boolean isMajorEnoughMoveToBeOnNewKey(int x, int y, Key newKey) { - if (mKeyDetector == null) + private boolean isMajorEnoughMoveToBeOnNewKey(final int x, final int y, final long eventTime, + final Key newKey) { + if (mKeyDetector == null) { throw new NullPointerException("keyboard and/or key detector not set"); - Key curKey = mCurrentKey; + } + final Key curKey = mCurrentKey; if (newKey == curKey) { return false; } else if (curKey != null) { - return curKey.squaredDistanceToEdge(x, y) - >= mKeyDetector.getKeyHysteresisDistanceSquared(); - } else { + final int keyHysteresisDistanceSquared = mKeyDetector.getKeyHysteresisDistanceSquared( + mIsInSlidingKeyInputFromModifier); + final int distanceFromKeyEdgeSquared = curKey.squaredDistanceToEdge(x, y); + if (distanceFromKeyEdgeSquared >= keyHysteresisDistanceSquared) { + if (DEBUG_MODE) { + final float distanceToEdgeRatio = (float)Math.sqrt(distanceFromKeyEdgeSquared) + / mKeyboard.mMostCommonKeyWidth; + Log.d(TAG, String.format("[%d] isMajorEnoughMoveToBeOnNewKey:" + +" %.2f key width from key edge", + mPointerId, distanceToEdgeRatio)); + } + return true; + } + if (sNeedsProximateBogusDownMoveUpEventHack && !mIsAllowedSlidingKeyInput + && sTimeRecorder.isInFastTyping(eventTime) + && mBogusMoveEventDetector.hasTraveledLongDistance(x, y)) { + if (DEBUG_MODE) { + final float keyDiagonal = (float)Math.hypot( + mKeyboard.mMostCommonKeyWidth, mKeyboard.mMostCommonKeyHeight); + final float lengthFromDownRatio = + mBogusMoveEventDetector.mAccumulatedDistanceFromDownKey / keyDiagonal; + Log.d(TAG, String.format("[%d] isMajorEnoughMoveToBeOnNewKey:" + + " %.2f key diagonal from virtual down point", + mPointerId, lengthFromDownRatio)); + } + return true; + } + return false; + } else { // curKey == null && newKey != null return true; } } - private void startLongPressTimer(Key key) { - if (key != null && key.isLongPressEnabled()) { + private void startLongPressTimer(final Key key) { + if (key != null && key.isLongPressEnabled() && !sInGesture) { mTimerProxy.startLongPressTimer(this); } } - private void detectAndSendKey(Key key, int x, int y) { + private void detectAndSendKey(final Key key, final int x, final int y, final long eventTime) { if (key == null) { callListenerOnCancelInput(); return; } - int code = key.mCode; - callListenerOnCodeInput(key, code, x, y); + final int code = key.mCode; + callListenerOnCodeInput(key, code, x, y, eventTime); callListenerOnRelease(key, code, false); } - private long mPreviousEventTime; - - private void printTouchEvent(String title, int x, int y, long eventTime) { + private void printTouchEvent(final String title, final int x, final int y, + final long eventTime) { final Key key = mKeyDetector.detectHitKey(x, y); final String code = KeyDetector.printableCode(key); - final long delta = eventTime - mPreviousEventTime; - Log.d(TAG, String.format("%s%s[%d] %4d %4d %5d %s", title, - (mKeyAlreadyProcessed ? "-" : " "), mPointerId, x, y, delta, code)); - mPreviousEventTime = eventTime; + Log.d(TAG, String.format("[%d]%s%s %4d %4d %5d %s", mPointerId, + (mKeyAlreadyProcessed ? "-" : " "), title, x, y, eventTime, code)); } } |