diff options
Diffstat (limited to 'java/src/com/android/inputmethod/keyboard/PointerTracker.java')
-rw-r--r-- | java/src/com/android/inputmethod/keyboard/PointerTracker.java | 714 |
1 files changed, 446 insertions, 268 deletions
diff --git a/java/src/com/android/inputmethod/keyboard/PointerTracker.java b/java/src/com/android/inputmethod/keyboard/PointerTracker.java index 03d610a39..c86805232 100644 --- a/java/src/com/android/inputmethod/keyboard/PointerTracker.java +++ b/java/src/com/android/inputmethod/keyboard/PointerTracker.java @@ -23,9 +23,11 @@ import android.view.MotionEvent; 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.Constants; import com.android.inputmethod.latin.InputPointers; import com.android.inputmethod.latin.LatinImeLogger; import com.android.inputmethod.latin.R; @@ -34,12 +36,12 @@ import com.android.inputmethod.research.ResearchLogger; import java.util.ArrayList; -public class PointerTracker implements PointerTrackerQueue.Element { +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; @@ -117,40 +119,45 @@ public class PointerTracker implements PointerTrackerQueue.Element { } } - static class PointerTrackerParams { + static final class PointerTrackerParams { public final boolean mSlidingKeyInputEnabled; public final int mTouchNoiseThresholdTime; - public final float mTouchNoiseThresholdDistance; - public final int mTouchNoiseThresholdDistanceSquared; + public final int mTouchNoiseThresholdDistance; + public final int mSuppressKeyPreviewAfterBatchInputDuration; public static final PointerTrackerParams DEFAULT = new PointerTrackerParams(); private PointerTrackerParams() { mSlidingKeyInputEnabled = false; mTouchNoiseThresholdTime = 0; - mTouchNoiseThresholdDistance = 0.0f; - mTouchNoiseThresholdDistanceSquared = 0; + mTouchNoiseThresholdDistance = 0; + mSuppressKeyPreviewAfterBatchInputDuration = 0; } - public PointerTrackerParams(TypedArray mainKeyboardViewAttr) { + public PointerTrackerParams(final TypedArray mainKeyboardViewAttr) { mSlidingKeyInputEnabled = mainKeyboardViewAttr.getBoolean( R.styleable.MainKeyboardView_slidingKeyInputEnable, false); mTouchNoiseThresholdTime = mainKeyboardViewAttr.getInt( R.styleable.MainKeyboardView_touchNoiseThresholdTime, 0); - final float touchNouseThresholdDistance = mainKeyboardViewAttr.getDimension( + mTouchNoiseThresholdDistance = mainKeyboardViewAttr.getDimensionPixelSize( R.styleable.MainKeyboardView_touchNoiseThresholdDistance, 0); - mTouchNoiseThresholdDistance = touchNouseThresholdDistance; - mTouchNoiseThresholdDistanceSquared = - (int)(touchNouseThresholdDistance * touchNouseThresholdDistance); + mSuppressKeyPreviewAfterBatchInputDuration = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_suppressKeyPreviewAfterBatchInputDuration, 0); } } // Parameters for pointer handling. 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 = CollectionUtils.newArrayList(); - private static PointerTrackerQueue sPointerTrackerQueue; + private static final PointerTrackerQueue sPointerTrackerQueue = new PointerTrackerQueue(); public final int mPointerId; @@ -160,15 +167,126 @@ public class PointerTracker implements PointerTrackerQueue.Element { private KeyboardActionListener mListener = EMPTY_LISTENER; private Keyboard mKeyboard; - private int mKeyQuarterWidthSquared; + 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; - private static long sLastRecognitionTime = 0; + 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; @@ -193,34 +311,32 @@ public class PointerTracker implements PointerTrackerQueue.Element { // true if this pointer has been long-pressed and is showing a more keys panel. private boolean mIsShowingMoreKeysPanel; - // 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) { - sPointerTrackerQueue = new PointerTrackerQueue(); - } else { - sPointerTrackerQueue = null; - } + public static void init(final boolean needsPhantomSuddenMoveEventHack) { 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() { @@ -254,7 +370,7 @@ public class PointerTracker implements PointerTrackerQueue.Element { } public static boolean isAnyInSlidingKeyInput() { - return sPointerTrackerQueue != null ? sPointerTrackerQueue.isAnyInSlidingKeyInput() : false; + return sPointerTrackerQueue.isAnyInSlidingKeyInput(); } public static void setKeyboardActionListener(final KeyboardActionListener listener) { @@ -291,7 +407,8 @@ public class PointerTracker implements PointerTrackerQueue.Element { throw new NullPointerException(); } mPointerId = id; - mGestureStrokeWithPreviewPoints = new GestureStrokeWithPreviewPoints(id); + mGestureStrokeWithPreviewPoints = new GestureStrokeWithPreviewPoints( + id, sGestureStrokeParams); setKeyDetectorInner(handler.getKeyDetector()); mListener = handler.getKeyboardActionListener(); mDrawingProxy = handler.getDrawingProxy(); @@ -303,11 +420,12 @@ public class PointerTracker implements PointerTrackerQueue.Element { if (sInGesture) { return false; } - final boolean ignoreModifierKey = mIgnoreModifierKey && key.isModifier(); + final boolean ignoreModifierKey = mIsInSlidingKeyInputFromModifier && key.isModifier(); if (DEBUG_LISTENER) { - Log.d(TAG, "onPress : " + KeyDetector.printableCode(key) - + " ignoreModifier=" + ignoreModifierKey - + " enabled=" + key.isEnabled()); + Log.d(TAG, String.format("[%d] onPress : %s%s%s", mPointerId, + KeyDetector.printableCode(key), + ignoreModifierKey ? " ignoreModifier" : "", + key.isEnabled() ? "" : " disabled")); } if (ignoreModifierKey) { return false; @@ -325,15 +443,16 @@ public class PointerTracker implements PointerTrackerQueue.Element { // 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(final Key key, final int primaryCode, final int x, - final int y) { - final boolean ignoreModifierKey = mIgnoreModifierKey && key.isModifier(); + final int y, final long eventTime) { + final boolean ignoreModifierKey = mIsInSlidingKeyInputFromModifier && key.isModifier(); final boolean altersCode = key.altCodeWhileTyping() && mTimerProxy.isTypingState(); final int code = altersCode ? key.getAltCode() : primaryCode; if (DEBUG_LISTENER) { - Log.d(TAG, "onCodeInput: " + Keyboard.printableCode(code) - + " text=" + key.getOutputText() + " x=" + x + " y=" + y - + " ignoreModifier=" + ignoreModifierKey + " altersCode=" + altersCode - + " enabled=" + key.isEnabled()); + final String output = code == Constants.CODE_OUTPUT_TEXT + ? key.getOutputText() : Constants.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, @@ -344,26 +463,28 @@ public class PointerTracker implements PointerTrackerQueue.Element { } // Even if the key is disabled, it should respond if it is in the altCodeWhileTyping state. if (key.isEnabled() || altersCode) { - if (code == Keyboard.CODE_OUTPUT_TEXT) { + sTimeRecorder.onCodeInput(code, eventTime); + if (code == Constants.CODE_OUTPUT_TEXT) { mListener.onTextInput(key.getOutputText()); - } else if (code != Keyboard.CODE_UNSPECIFIED) { + } else if (code != Constants.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(final Key key, final int primaryCode, final boolean withSliding) { if (sInGesture) { return; } - final boolean ignoreModifierKey = mIgnoreModifierKey && key.isModifier(); + 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, + Constants.printableCode(primaryCode), + withSliding ? " sliding" : "", ignoreModifierKey ? " ignoreModifier" : "", + key.isEnabled() ? "": " disabled")); } if (ProductionFlag.IS_EXPERIMENTAL) { ResearchLogger.pointerTracker_callListenerOnRelease(key, primaryCode, withSliding, @@ -379,7 +500,7 @@ public class PointerTracker implements PointerTrackerQueue.Element { private void callListenerOnCancelInput() { if (DEBUG_LISTENER) { - Log.d(TAG, "onCancelInput"); + Log.d(TAG, String.format("[%d] onCancelInput", mPointerId)); } if (ProductionFlag.IS_EXPERIMENTAL) { ResearchLogger.pointerTracker_callListenerOnCancelInput(); @@ -388,9 +509,15 @@ public class PointerTracker implements PointerTrackerQueue.Element { } private void setKeyDetectorInner(final KeyDetector keyDetector) { + final Keyboard keyboard = keyDetector.getKeyboard(); + if (keyDetector == mKeyDetector && keyboard == mKeyboard) { + return; + } mKeyDetector = keyDetector; mKeyboard = keyDetector.getKeyboard(); - mGestureStrokeWithPreviewPoints.setKeyboardGeometry(mKeyboard.mMostCommonKeyWidth); + 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) { @@ -398,8 +525,8 @@ public class PointerTracker implements PointerTrackerQueue.Element { } // Keep {@link #mCurrentKey} that comes from previous keyboard. } - final int keyQuarterWidth = mKeyboard.mMostCommonKeyWidth / 4; - mKeyQuarterWidthSquared = keyQuarterWidth * keyQuarterWidth; + mPhantonSuddenMoveThreshold = (int)(keyWidth * PHANTOM_SUDDEN_MOVE_THRESHOLD); + mBogusMoveEventDetector.setKeyboardGeometry(keyWidth, keyHeight); } @Override @@ -451,7 +578,12 @@ public class PointerTracker implements PointerTrackerQueue.Element { } } - private void setPressedKeyGraphics(final 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; } @@ -463,7 +595,7 @@ public class PointerTracker implements PointerTrackerQueue.Element { return; } - if (!key.noKeyPreview() && !sInGesture) { + if (!key.noKeyPreview() && !sInGesture && !needsToSuppressKeyPreviewPopup(eventTime)) { mDrawingProxy.showKeyPreview(this); } updatePressKeyGraphics(key); @@ -518,10 +650,16 @@ public class PointerTracker implements PointerTrackerQueue.Element { private Key onDownKey(final int x, final int y, final long eventTime) { mDownTime = eventTime; + mBogusMoveEventDetector.onDownKey(); return onMoveToNewKey(onMoveKeyInternal(x, y), x, 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); @@ -539,71 +677,69 @@ public class PointerTracker implements PointerTrackerQueue.Element { } private static int getActivePointerTrackerCount() { - return (sPointerTrackerQueue == null) ? 1 : sPointerTrackerQueue.size(); + return sPointerTrackerQueue.size(); + } + + private static boolean isOldestTrackerInQueue(final PointerTracker tracker) { + return sPointerTrackerQueue.getOldestElement() == tracker; } - private void mayStartBatchInput() { + 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, "onStartBatchInput"); + Log.d(TAG, String.format("[%d] onStartBatchInput", mPointerId)); } sInGesture = true; - mListener.onStartBatchInput(); - final boolean isOldestTracker = sPointerTrackerQueue.getOldestElement() == this; - mDrawingProxy.showGesturePreviewTrail(this, isOldestTracker); + synchronized (sAggregratedPointers) { + sAggregratedPointers.reset(); + sLastRecognitionPointSize = 0; + sLastRecognitionTime = 0; + mListener.onStartBatchInput(); + } + mTimerProxy.cancelLongPressTimer(); + mDrawingProxy.showGesturePreviewTrail(this, isOldestTrackerInQueue(this)); } - private void updateBatchInput(final long eventTime) { - synchronized (sAggregratedPointers) { - mGestureStrokeWithPreviewPoints.appendIncrementalBatchPoints(sAggregratedPointers); - final int size = sAggregratedPointers.getPointerSize(); - if (size > sLastRecognitionPointSize - && GestureStroke.hasRecognitionTimePast(eventTime, sLastRecognitionTime)) { - sLastRecognitionPointSize = size; - sLastRecognitionTime = eventTime; - if (DEBUG_LISTENER) { - Log.d(TAG, "onUpdateBatchInput: batchPoints=" + size); + 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); } - mListener.onUpdateBatchInput(sAggregratedPointers); } } - final boolean isOldestTracker = sPointerTrackerQueue.getOldestElement() == this; - mDrawingProxy.showGesturePreviewTrail(this, isOldestTracker); + mDrawingProxy.showGesturePreviewTrail(this, isOldestTrackerInQueue(this)); } - private void mayEndBatchInput() { + private void mayEndBatchInput(final long eventTime) { synchronized (sAggregratedPointers) { mGestureStrokeWithPreviewPoints.appendAllBatchPoints(sAggregratedPointers); - mGestureStrokeWithPreviewPoints.reset(); if (getActivePointerTrackerCount() == 1) { if (DEBUG_LISTENER) { - Log.d(TAG, "onEndBatchInput: batchPoints=" - + sAggregratedPointers.getPointerSize()); + Log.d(TAG, String.format("[%d] onEndBatchInput : batchPoints=%d", + mPointerId, sAggregratedPointers.getPointerSize())); } sInGesture = false; + sTimeRecorder.onEndBatchInput(eventTime); mListener.onEndBatchInput(sAggregratedPointers); - clearBatchInputPointsOfAllPointerTrackers(); } } - final boolean isOldestTracker = sPointerTrackerQueue.getOldestElement() == this; - mDrawingProxy.showGesturePreviewTrail(this, isOldestTracker); - } - - private static void abortBatchInput() { - clearBatchInputPointsOfAllPointerTrackers(); - } - - private static void clearBatchInputPointsOfAllPointerTrackers() { - final int trackersSize = sTrackers.size(); - for (int i = 0; i < trackersSize; ++i) { - final PointerTracker tracker = sTrackers.get(i); - tracker.mGestureStrokeWithPreviewPoints.reset(); - } - sAggregratedPointers.reset(); - sLastRecognitionPointSize = 0; - sLastRecognitionTime = 0; + mDrawingProxy.showGesturePreviewTrail(this, isOldestTrackerInQueue(this)); } public void processMotionEvent(final int action, final int x, final int y, final long eventTime, @@ -639,15 +775,14 @@ public class PointerTracker implements PointerTrackerQueue.Element { // 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 < sParams.mTouchNoiseThresholdDistanceSquared) { + 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; @@ -655,42 +790,29 @@ public class PointerTracker implements PointerTrackerQueue.Element { } final Key key = getKeyOn(x, y); - final PointerTrackerQueue queue = sPointerTrackerQueue; - if (queue != null) { - if (key != null && key.isModifier()) { - // Before processing a down event of modifier key, all pointers already being - // tracked should be released. - queue.releaseAllPointers(eventTime); - } - queue.add(this); + mBogusMoveEventDetector.onActualDownEvent(x, y); + if (key != null && key.isModifier()) { + // Before processing a down event of modifier key, all pointers already being + // tracked should be released. + sPointerTrackerQueue.releaseAllPointers(eventTime); } + sPointerTrackerQueue.add(this); onDownEventInternal(x, y, eventTime); if (!sShouldHandleGesture) { return; } - final int activePointerTrackerCount = getActivePointerTrackerCount(); - if (activePointerTrackerCount == 1) { - mIsDetectingGesture = false; - // A gesture should start only from the letter key. - final boolean isAlphabetKeyboard = (mKeyboard != null) - && mKeyboard.mId.isAlphabetKeyboard(); - if (isAlphabetKeyboard && !mIsShowingMoreKeysPanel && key != null - && Keyboard.isLetterCode(key.mCode)) { + // A gesture should start only from a non-modifier key. + mIsDetectingGesture = (mKeyboard != null) && mKeyboard.mId.isAlphabetKeyboard() + && !mIsShowingMoreKeysPanel && key != null && !key.isModifier(); + if (mIsDetectingGesture) { + if (getActivePointerTrackerCount() == 1) { sGestureFirstDownTime = eventTime; - onGestureDownEvent(x, y, eventTime); } - } else if (sInGesture && activePointerTrackerCount > 1) { - onGestureDownEvent(x, y, eventTime); + mGestureStrokeWithPreviewPoints.onDownEvent(x, y, eventTime, sGestureFirstDownTime, + sTimeRecorder.getLastLetterTypingTime()); } } - private void onGestureDownEvent(final int x, final int y, final long eventTime) { - mIsDetectingGesture = true; - final int elapsedTimeFromFirstDown = (int)(eventTime - sGestureFirstDownTime); - mGestureStrokeWithPreviewPoints.addPoint(x, y, elapsedTimeFromFirstDown, - false /* isHistorical */); - } - 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 @@ -700,8 +822,7 @@ public class PointerTracker implements PointerTrackerQueue.Element { || mKeyDetector.alwaysAllowsSlidingInput(); mKeyboardLayoutHasBeenChanged = false; mKeyAlreadyProcessed = 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 @@ -712,25 +833,30 @@ public class PointerTracker implements PointerTrackerQueue.Element { startRepeatKey(key); startLongPressTimer(key); - setPressedKeyGraphics(key); + setPressedKeyGraphics(key, eventTime); } } private void startSlidingKeyInput(final Key key) { if (!mIsInSlidingKeyInput) { - mIgnoreModifierKey = key.isModifier(); + mIsInSlidingKeyInputFromModifier = key.isModifier(); } mIsInSlidingKeyInput = true; } + private void resetSlidingKeyInput() { + mIsInSlidingKeyInput = false; + mIsInSlidingKeyInputFromModifier = false; + } + private void onGestureMoveEvent(final int x, final int y, final long eventTime, - final boolean isHistorical, final Key key) { + final boolean isMajorEvent, final Key key) { final int gestureTime = (int)(eventTime - sGestureFirstDownTime); if (mIsDetectingGesture) { - mGestureStrokeWithPreviewPoints.addPoint(x, y, gestureTime, isHistorical); - mayStartBatchInput(); - if (sInGesture && key != null) { - updateBatchInput(eventTime); + mGestureStrokeWithPreviewPoints.addPoint(x, y, gestureTime, isMajorEvent); + mayStartBatchInput(key); + if (sInGesture) { + mayUpdateBatchInput(eventTime, key); } } } @@ -752,118 +878,157 @@ public class PointerTracker implements PointerTrackerQueue.Element { final int historicalY = (int)me.getHistoricalY(pointerIndex, h); final long historicalTime = me.getHistoricalEventTime(h); onGestureMoveEvent(historicalX, historicalY, historicalTime, - true /* isHistorical */, null); + false /* isMajorEvent */, null); } } onMoveEventInternal(x, y, eventTime); } + private void processSlidingKeyInput(final Key newKey, final int x, final int y, + final long eventTime) { + // 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 keyboard layout. + Key key = newKey; + if (callListenerOnPressAndCheckKeyboardLayoutChange(key)) { + key = onMoveKey(x, y); + } + onMoveToNewKey(key, x, y); + startLongPressTimer(key); + setPressedKeyGraphics(key, eventTime); + } + + private void processPhantomSuddenMoveHack(final Key key, final int x, final int y, + final long eventTime, final Key oldKey, final int lastX, final int lastY) { + if (DEBUG_MODE) { + 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, Constants.printableCode(oldKey.mCode), + x, y, Constants.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(eventTime); + onDownEventInternal(x, y, eventTime); + } + + private void processProximateBogusDownMoveUpEventHack(final Key key, final int x, final int y, + final long eventTime, final Key oldKey, final int lastX, final int lastY) { + 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, Constants.printableCode(oldKey.mCode), + x, y, Constants.printableCode(key.mCode))); + } + onUpEventInternal(eventTime); + onDownEventInternal(x, y, eventTime); + } + + private void processSildeOutFromOldKey(final Key oldKey) { + setReleasedKeyGraphics(oldKey); + callListenerOnRelease(oldKey, oldKey.mCode, true); + startSlidingKeyInput(oldKey); + mTimerProxy.cancelKeyTimers(); + } + + private void slideFromOldKeyToNewKey(final Key key, final int x, final int y, + final long eventTime, final Key oldKey, final int lastX, final int lastY) { + // 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. + processSildeOutFromOldKey(oldKey); + startRepeatKey(key); + if (mIsAllowedSlidingKeyInput) { + processSlidingKeyInput(key, x, y, eventTime); + } + // 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. + // TODO: Should find a way to balance gesture detection and this hack. + else if (sNeedsPhantomSuddenMoveEventHack + && getDistance(x, y, lastX, lastY) >= mPhantonSuddenMoveThreshold) { + processPhantomSuddenMoveHack(key, x, y, eventTime, oldKey, lastX, lastY); + } + // 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)) { + processProximateBogusDownMoveUpEventHack(key, x, y, eventTime, oldKey, lastX, lastY); + } + // 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. + else if (getActivePointerTrackerCount() > 1 + && !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; + setReleasedKeyGraphics(oldKey); + } else { + if (!mIsDetectingGesture) { + mKeyAlreadyProcessed = true; + } + setReleasedKeyGraphics(oldKey); + } + } + + private void slideOutFromOldKey(final Key oldKey, final int x, final int y) { + // The pointer has been slid out from the previous key, we must call onRelease() to + // notify that the previous key has been released. + processSildeOutFromOldKey(oldKey); + if (mIsAllowedSlidingKeyInput) { + onMoveToNewKey(null, x, y); + } else { + if (!mIsDetectingGesture) { + mKeyAlreadyProcessed = true; + } + } + } + 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); + final Key newKey = onMoveKey(x, y); if (sShouldHandleGesture) { // Register move event on gesture tracker. - onGestureMoveEvent(x, y, eventTime, false /* isHistorical */, key); + onGestureMoveEvent(x, y, eventTime, true /* isMajorEvent */, newKey); if (sInGesture) { - mIgnoreModifierKey = true; - mTimerProxy.cancelLongPressTimer(); - mIsInSlidingKeyInput = true; mCurrentKey = null; setReleasedKeyGraphics(oldKey); return; } } - if (key != null) { - if (oldKey == null) { + if (newKey != null) { + if (oldKey != null && isMajorEnoughMoveToBeOnNewKey(x, y, eventTime, newKey)) { + slideFromOldKeyToNewKey(newKey, x, y, eventTime, oldKey, lastX, lastY); + } else 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 key according to the - // new keyboard layout. - if (callListenerOnPressAndCheckKeyboardLayoutChange(key)) { - key = onMoveKey(x, y); - } - onMoveToNewKey(key, x, y); - startLongPressTimer(key); - setPressedKeyGraphics(key); - } else if (isMajorEnoughMoveToBeOnNewKey(x, y, 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. - setReleasedKeyGraphics(oldKey); - callListenerOnRelease(oldKey, oldKey.mCode, true); - startSlidingKeyInput(oldKey); - mTimerProxy.cancelKeyTimers(); - startRepeatKey(key); - if (mIsAllowedSlidingKeyInput) { - // 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 keyboard layout. - if (callListenerOnPressAndCheckKeyboardLayoutChange(key)) { - key = onMoveKey(x, y); - } - onMoveToNewKey(key, x, y); - startLongPressTimer(key); - setPressedKeyGraphics(key); - } 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; - // TODO: Should find a way to balance gesture detection and this hack. - if (sNeedsPhantomSuddenMoveEventHack - && lastMoveSquared >= mKeyQuarterWidthSquared - && !mIsDetectingGesture) { - 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)); - } - // TODO: This should be moved to outside of this nested if-clause? - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.pointerTracker_onMoveEvent(x, y, lastX, lastY); - } - onUpEventInternal(); - onDownEventInternal(x, y, eventTime); - } else { - // 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)) { - onUpEventInternal(); - } - if (!mIsDetectingGesture) { - mKeyAlreadyProcessed = true; - } - setReleasedKeyGraphics(oldKey); - } - } + processSlidingKeyInput(newKey, x, y, eventTime); } - } else { - if (oldKey != null && isMajorEnoughMoveToBeOnNewKey(x, y, 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); - callListenerOnRelease(oldKey, oldKey.mCode, true); - startSlidingKeyInput(oldKey); - mTimerProxy.cancelLongPressTimer(); - if (mIsAllowedSlidingKeyInput) { - onMoveToNewKey(key, x, y); - } else { - if (!mIsDetectingGesture) { - mKeyAlreadyProcessed = true; - } - } + } else { // newKey == null + if (oldKey != null && isMajorEnoughMoveToBeOnNewKey(x, y, eventTime, newKey)) { + slideOutFromOldKey(oldKey, x, y); } } } @@ -873,22 +1038,17 @@ public class PointerTracker implements PointerTrackerQueue.Element { printTouchEvent("onUpEvent :", x, y, eventTime); } - final PointerTrackerQueue queue = sPointerTrackerQueue; - if (queue != null) { - 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); - } + if (!sInGesture) { + if (mCurrentKey != null && mCurrentKey.isModifier()) { + // Before processing an up event of modifier key, all pointers already being + // tracked should be released. + sPointerTrackerQueue.releaseAllPointersExcept(this, eventTime); + } else { + sPointerTrackerQueue.releaseAllPointersOlderThan(this, eventTime); } } - onUpEventInternal(); - if (queue != null) { - queue.remove(this); - } + onUpEventInternal(eventTime); + sPointerTrackerQueue.remove(this); } // Let this pointer tracker know that one of newer-than-this pointer trackers got an up event. @@ -899,13 +1059,13 @@ public class PointerTracker implements PointerTrackerQueue.Element { if (DEBUG_EVENT) { printTouchEvent("onPhntEvent:", getLastX(), getLastY(), eventTime); } - onUpEventInternal(); + 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; @@ -920,22 +1080,19 @@ public class PointerTracker implements PointerTrackerQueue.Element { if (currentKey != null) { callListenerOnRelease(currentKey, currentKey.mCode, true); } - mayEndBatchInput(); + mayEndBatchInput(eventTime); return; } - // This event will be recognized as a regular code input. Clear unused possible batch points - // so they are not mistakenly displayed as preview. - clearBatchInputPointsOfAllPointerTrackers(); + if (mKeyAlreadyProcessed) { return; } if (currentKey != null && !currentKey.isRepeatable()) { - detectAndSendKey(currentKey, mKeyX, mKeyY); + detectAndSendKey(currentKey, mKeyX, mKeyY, eventTime); } } public void onShowMoreKeysPanel(final int x, final int y, final KeyEventHandler handler) { - abortBatchInput(); onLongPressed(); mIsShowingMoreKeysPanel = true; onDownEvent(x, y, SystemClock.uptimeMillis(), handler); @@ -944,10 +1101,7 @@ public class PointerTracker implements PointerTrackerQueue.Element { public void onLongPressed() { mKeyAlreadyProcessed = true; setReleasedKeyGraphics(mCurrentKey); - final PointerTrackerQueue queue = sPointerTrackerQueue; - if (queue != null) { - queue.remove(this); - } + sPointerTrackerQueue.remove(this); } public void onCancelEvent(final int x, final int y, final long eventTime) { @@ -955,18 +1109,15 @@ public class PointerTracker implements PointerTrackerQueue.Element { printTouchEvent("onCancelEvt:", x, y, eventTime); } - final PointerTrackerQueue queue = sPointerTrackerQueue; - if (queue != null) { - queue.releaseAllPointersExcept(this, eventTime); - queue.remove(this); - } + sPointerTrackerQueue.releaseAllPointersExcept(this, eventTime); + sPointerTrackerQueue.remove(this); onCancelEventInternal(); } private void onCancelEventInternal() { mTimerProxy.cancelKeyTimers(); setReleasedKeyGraphics(mCurrentKey); - mIsInSlidingKeyInput = false; + resetSlidingKeyInput(); if (mIsShowingMoreKeysPanel) { mDrawingProxy.dismissMoreKeysPanel(); mIsShowingMoreKeysPanel = false; @@ -982,24 +1133,51 @@ public class PointerTracker implements PointerTrackerQueue.Element { public void onRegisterKey(final Key key) { if (key != null) { - detectAndSendKey(key, key.mX, key.mY); + detectAndSendKey(key, key.mX, key.mY, SystemClock.uptimeMillis()); mTimerProxy.startTypingStateTimer(key); } } - private boolean isMajorEnoughMoveToBeOnNewKey(final int x, final int y, final Key newKey) { + 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"); } final Key curKey = mCurrentKey; if (newKey == curKey) { return false; - } else if (curKey != null) { - return curKey.squaredDistanceToEdge(x, y) - >= mKeyDetector.getKeyHysteresisDistanceSquared(); - } else { + } + if (curKey == null /* && newKey != null */) { + return true; + } + // Here curKey points to the different key from newKey. + 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; } private void startLongPressTimer(final Key key) { @@ -1008,14 +1186,14 @@ public class PointerTracker implements PointerTrackerQueue.Element { } } - private void detectAndSendKey(final Key key, final int x, final int y) { + private void detectAndSendKey(final Key key, final int x, final int y, final long eventTime) { if (key == null) { callListenerOnCancelInput(); return; } final int code = key.mCode; - callListenerOnCodeInput(key, code, x, y); + callListenerOnCodeInput(key, code, x, y, eventTime); callListenerOnRelease(key, code, false); } @@ -1023,7 +1201,7 @@ public class PointerTracker implements PointerTrackerQueue.Element { final long eventTime) { final Key key = mKeyDetector.detectHitKey(x, y); final String code = KeyDetector.printableCode(key); - Log.d(TAG, String.format("%s%s[%d] %4d %4d %5d %s", title, - (mKeyAlreadyProcessed ? "-" : " "), mPointerId, x, y, eventTime, code)); + Log.d(TAG, String.format("[%d]%s%s %4d %4d %5d %s", mPointerId, + (mKeyAlreadyProcessed ? "-" : " "), title, x, y, eventTime, code)); } } |