Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 02a67200 authored by Tadashi G. Takaoka's avatar Tadashi G. Takaoka
Browse files

Fix gesture start detection algorithm

Bug: 7032858
Change-Id: I9f4d939fa87fdead4c5a5921338a16cd0a59b7ac
parent 73779f76
Loading
Loading
Loading
Loading
+5 −5
Original line number Diff line number Diff line
@@ -688,7 +688,7 @@ public class PointerTracker implements PointerTrackerQueue.Element {
        mIsDetectingGesture = true;
        final int elapsedTimeFromFirstDown = (int)(eventTime - sGestureFirstDownTime);
        mGestureStrokeWithPreviewPoints.addPoint(x, y, elapsedTimeFromFirstDown,
                false /* isHistorical */);
                true /* isMajorEvent */);
    }

    private void onDownEventInternal(final int x, final int y, final long eventTime) {
@@ -724,10 +724,10 @@ public class PointerTracker implements PointerTrackerQueue.Element {
    }

    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);
            mGestureStrokeWithPreviewPoints.addPoint(x, y, gestureTime, isMajorEvent);
            mayStartBatchInput();
            if (sInGesture && key != null) {
                updateBatchInput(eventTime);
@@ -752,7 +752,7 @@ 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);
            }
        }

@@ -767,7 +767,7 @@ public class PointerTracker implements PointerTrackerQueue.Element {

        if (sShouldHandleGesture) {
            // Register move event on gesture tracker.
            onGestureMoveEvent(x, y, eventTime, false /* isHistorical */, key);
            onGestureMoveEvent(x, y, eventTime, true /* isMajorEvent */, key);
            if (sInGesture) {
                mIgnoreModifierKey = true;
                mTimerProxy.cancelLongPressTimer();
+114 −47
Original line number Diff line number Diff line
@@ -14,34 +14,45 @@

package com.android.inputmethod.keyboard.internal;

import android.util.Log;

import com.android.inputmethod.latin.InputPointers;
import com.android.inputmethod.latin.ResizableIntArray;

public class GestureStroke {
    private static final String TAG = GestureStroke.class.getSimpleName();
    private static final boolean DEBUG = false;

    public static final int DEFAULT_CAPACITY = 128;

    private final int mPointerId;
    private final ResizableIntArray mEventTimes = new ResizableIntArray(DEFAULT_CAPACITY);
    private final ResizableIntArray mXCoordinates = new ResizableIntArray(DEFAULT_CAPACITY);
    private final ResizableIntArray mYCoordinates = new ResizableIntArray(DEFAULT_CAPACITY);
    private float mLength;
    private int mIncrementalRecognitionSize;
    private int mLastIncrementalBatchSize;
    private long mLastPointTime;
    private int mLastPointX;
    private int mLastPointY;

    private int mMinGestureLength; // pixel
    private int mMinGestureSampleLength; // pixel
    private int mGestureRecognitionThreshold; // pixel / sec
    private long mLastMajorEventTime;
    private int mLastMajorEventX;
    private int mLastMajorEventY;

    private int mKeyWidth;
    private int mStartGestureLengthThreshold; // pixel
    private int mMinGestureSamplingLength; // pixel
    private int mGestureRecognitionSpeedThreshold; // pixel / sec
    private int mDetectFastMoveSpeedThreshold; // pixel /sec
    private int mDetectFastMoveTime;
    private int mDetectFastMoveX;
    private int mDetectFastMoveY;

    // TODO: Move some of these to resource.
    private static final float MIN_GESTURE_LENGTH_RATIO_TO_KEY_WIDTH = 0.75f;
    private static final int MIN_GESTURE_START_DURATION = 100; // msec
    private static final float START_GESTURE_LENGTH_THRESHOLD_RATIO_TO_KEY_WIDTH = 0.75f;
    private static final int START_GESTURE_DURATION_THRESHOLD = 70; // msec
    private static final int MIN_GESTURE_RECOGNITION_TIME = 100; // msec
    private static final float MIN_GESTURE_SAMPLING_RATIO_TO_KEY_WIDTH = 1.0f / 6.0f;
    private static final float GESTURE_RECOGNITION_SPEED_THRESHOLD_RATIO_TO_KEY_WIDTH =
            5.5f; // keyWidth / sec
    private static final float DETECT_FAST_MOVE_SPEED_THRESHOLD_RATIO_TO_KEY_WIDTH =
            5.0f; // keyWidth / sec
    private static final int MSEC_PER_SEC = 1000;

    public static final boolean hasRecognitionTimePast(
@@ -54,65 +65,121 @@ public class GestureStroke {
    }

    public void setKeyboardGeometry(final int keyWidth) {
        mKeyWidth = keyWidth;
        // TODO: Find an appropriate base metric for these length. Maybe diagonal length of the key?
        mMinGestureLength = (int)(keyWidth * MIN_GESTURE_LENGTH_RATIO_TO_KEY_WIDTH);
        mMinGestureSampleLength = (int)(keyWidth * MIN_GESTURE_SAMPLING_RATIO_TO_KEY_WIDTH);
        mGestureRecognitionThreshold =
        mStartGestureLengthThreshold =
                (int)(keyWidth * START_GESTURE_LENGTH_THRESHOLD_RATIO_TO_KEY_WIDTH);
        mMinGestureSamplingLength = (int)(keyWidth * MIN_GESTURE_SAMPLING_RATIO_TO_KEY_WIDTH);
        mGestureRecognitionSpeedThreshold =
                (int)(keyWidth * GESTURE_RECOGNITION_SPEED_THRESHOLD_RATIO_TO_KEY_WIDTH);
        mDetectFastMoveSpeedThreshold =
                (int)(keyWidth * DETECT_FAST_MOVE_SPEED_THRESHOLD_RATIO_TO_KEY_WIDTH);
        if (DEBUG) {
            Log.d(TAG, "setKeyboardGeometry: keyWidth=" + keyWidth);
        }
    }

    public boolean isStartOfAGesture() {
        if (mDetectFastMoveTime == 0) {
            return false;
        }
        final int size = mEventTimes.getLength();
        final int downDuration = (size > 0) ? mEventTimes.get(size - 1) : 0;
        return downDuration > MIN_GESTURE_START_DURATION && mLength > mMinGestureLength;
        if (size <= 0) {
            return false;
        }
        final int lastIndex = size - 1;
        final int deltaTime = mEventTimes.get(lastIndex) - mDetectFastMoveTime;
        final int deltaLength = getDistance(
                mXCoordinates.get(lastIndex), mYCoordinates.get(lastIndex),
                mDetectFastMoveX, mDetectFastMoveY);
        final boolean isStartOfAGesture = deltaTime > START_GESTURE_DURATION_THRESHOLD
                && deltaLength > mStartGestureLengthThreshold;
        if (DEBUG) {
            Log.d(TAG, "isStartOfAGesture: dT=" + deltaTime + " dL=" + deltaLength
                    + " points=" + size + (isStartOfAGesture ? " Detect start of a gesture" : ""));
        }
        return isStartOfAGesture;
    }

    public void reset() {
        mLength = 0;
        mIncrementalRecognitionSize = 0;
        mLastIncrementalBatchSize = 0;
        mLastPointTime = 0;
        mEventTimes.setLength(0);
        mXCoordinates.setLength(0);
        mYCoordinates.setLength(0);
        mLastMajorEventTime = 0;
        mDetectFastMoveTime = 0;
    }

    public void addPoint(final int x, final int y, final int time, final boolean isHistorical) {
        final boolean needsSampling;
    private void appendPoint(final int x, final int y, final int time) {
        mEventTimes.add(time);
        mXCoordinates.add(x);
        mYCoordinates.add(y);
    }

    private void updateMajorEvent(final int x, final int y, final int time) {
        mLastMajorEventTime = time;
        mLastMajorEventX = x;
        mLastMajorEventY = y;
    }

    private int detectFastMove(final int x, final int y, final int time) {
        final int size = mEventTimes.getLength();
        if (size == 0) {
            needsSampling = true;
        } else {
        final int lastIndex = size - 1;
        final int lastX = mXCoordinates.get(lastIndex);
        final int lastY = mYCoordinates.get(lastIndex);
            final float dist = getDistance(lastX, lastY, x, y);
            needsSampling = dist > mMinGestureSampleLength;
            mLength += dist;
        final int dist = getDistance(lastX, lastY, x, y);
        final int msecs = time - mEventTimes.get(lastIndex);
        if (msecs > 0) {
            final int pixels = getDistance(lastX, lastY, x, y);
            final int pixelsPerSec = pixels * MSEC_PER_SEC;
            if (DEBUG) {
                final float speed = (float)pixelsPerSec / msecs / mKeyWidth;
                Log.d(TAG, String.format("Speed=%.3f keyWidth/sec", speed));
            }
            // Equivalent to (pixels / msecs < mStartSpeedThreshold / MSEC_PER_SEC)
            if (mDetectFastMoveTime == 0 && pixelsPerSec > mDetectFastMoveSpeedThreshold * msecs) {
                if (DEBUG) {
                    Log.d(TAG, "Detect fast move: T=" + time + " points = " + size);
                }
                mDetectFastMoveTime = time;
                mDetectFastMoveX = x;
                mDetectFastMoveY = y;
            }
        }
        return dist;
    }

    public void addPoint(final int x, final int y, final int time, final boolean isMajorEvent) {
        final int size = mEventTimes.getLength();
        if (size <= 0) {
            // Down event
            appendPoint(x, y, time);
            updateMajorEvent(x, y, time);
        } else {
            final int dist = detectFastMove(x, y, time);
            if (dist > mMinGestureSamplingLength) {
                appendPoint(x, y, time);
            }
        if (needsSampling) {
            mEventTimes.add(time);
            mXCoordinates.add(x);
            mYCoordinates.add(y);
        }
        if (!isHistorical) {
        if (isMajorEvent) {
            updateIncrementalRecognitionSize(x, y, time);
            updateMajorEvent(x, y, time);
        }
    }

    private void updateIncrementalRecognitionSize(final int x, final int y, final int time) {
        final int msecs = (int)(time - mLastPointTime);
        if (msecs > 0) {
            final int pixels = (int)getDistance(mLastPointX, mLastPointY, x, y);
        final int msecs = (int)(time - mLastMajorEventTime);
        if (msecs <= 0) {
            return;
        }
        final int pixels = getDistance(mLastMajorEventX, mLastMajorEventY, x, y);
        final int pixelsPerSec = pixels * MSEC_PER_SEC;
        // Equivalent to (pixels / msecs < mGestureRecognitionThreshold / MSEC_PER_SEC)
            if (pixels * MSEC_PER_SEC < mGestureRecognitionThreshold * msecs) {
        if (pixelsPerSec < mGestureRecognitionSpeedThreshold * msecs) {
            mIncrementalRecognitionSize = mEventTimes.getLength();
        }
    }
        mLastPointTime = time;
        mLastPointX = x;
        mLastPointY = y;
    }

    public void appendAllBatchPoints(final InputPointers out) {
        appendBatchPoints(out, mEventTimes.getLength());
@@ -132,11 +199,11 @@ public class GestureStroke {
        mLastIncrementalBatchSize = size;
    }

    private static float getDistance(final int x1, final int y1, final int x2, final int y2) {
        final float dx = x1 - x2;
        final float dy = y1 - y2;
    private static int getDistance(final int x1, final int y1, final int x2, final int y2) {
        final int dx = x1 - x2;
        final int dy = y1 - y2;
        // Note that, in recent versions of Android, FloatMath is actually slower than
        // java.lang.Math due to the way the JIT optimizes java.lang.Math.
        return (float)Math.sqrt(dx * dx + dy * dy);
        return (int)Math.sqrt(dx * dx + dy * dy);
    }
}
+6 −9
Original line number Diff line number Diff line
@@ -65,21 +65,18 @@ public class GestureStrokeWithPreviewPoints extends GestureStroke {
    private boolean needsSampling(final int x, final int y) {
        final int dx = x - mLastX;
        final int dy = y - mLastY;
        final boolean needsSampling = (dx * dx + dy * dy >= mMinPreviewSampleLengthSquare);
        if (needsSampling) {
            mLastX = x;
            mLastY = y;
        }
        return needsSampling;
        return dx * dx + dy * dy >= mMinPreviewSampleLengthSquare;
    }

    @Override
    public void addPoint(final int x, final int y, final int time, final boolean isHistorical) {
        super.addPoint(x, y, time, isHistorical);
        if (mPreviewEventTimes.getLength() == 0 || isHistorical || needsSampling(x, y)) {
    public void addPoint(final int x, final int y, final int time, final boolean isMajorEvent) {
        super.addPoint(x, y, time, isMajorEvent);
        if (isMajorEvent || needsSampling(x, y)) {
            mPreviewEventTimes.add(time);
            mPreviewXCoordinates.add(x);
            mPreviewYCoordinates.add(y);
            mLastX = x;
            mLastY = y;
        }
    }

+1 −1
Original line number Diff line number Diff line
@@ -45,7 +45,7 @@ public class BinaryDictionary extends Dictionary {
    public static final int MAX_WORDS = 18;
    public static final int MAX_SPACES = 16;

    private static final String TAG = "BinaryDictionary";
    private static final String TAG = BinaryDictionary.class.getSimpleName();
    private static final int MAX_PREDICTIONS = 60;
    private static final int MAX_RESULTS = Math.max(MAX_PREDICTIONS, MAX_WORDS);

+1 −1
Original line number Diff line number Diff line
@@ -42,7 +42,7 @@ import java.util.concurrent.locks.ReentrantLock;
 * cancellation or manual picks. This allows the keyboard to adapt to the typist over time.
 */
public class UserHistoryDictionary extends ExpandableDictionary {
    private static final String TAG = "UserHistoryDictionary";
    private static final String TAG = UserHistoryDictionary.class.getSimpleName();
    public static final boolean DBG_SAVE_RESTORE = false;
    public static final boolean DBG_STRESS_TEST = false;
    public static final boolean DBG_ALWAYS_WRITE = false;