diff --git a/java/res/values/strings-uix.xml b/java/res/values/strings-uix.xml index d69782d46d..babfa5c542 100644 --- a/java/res/values/strings-uix.xml +++ b/java/res/values/strings-uix.xml @@ -562,6 +562,9 @@ Shortcuts Configure spacebar and delete key gestures in long-press settings + Swipe to delete words + Swipe left on the keyboard to delete the previous word. Keep holding after the swipe and move left or right to select more or fewer words before releasing. + Show unsupported settings… Use legacy swipe algorithm The legacy algorithm is very inaccurate and not recommended. diff --git a/java/src/org/futo/inputmethod/engine/IMEInterface.kt b/java/src/org/futo/inputmethod/engine/IMEInterface.kt index b52598e041..03896f706a 100644 --- a/java/src/org/futo/inputmethod/engine/IMEInterface.kt +++ b/java/src/org/futo/inputmethod/engine/IMEInterface.kt @@ -83,6 +83,7 @@ interface IMEInterface { fun onMovePointer(steps: Int, stepOverWords: Boolean, select: Boolean?) fun onMoveDeletePointer(steps: Int) + fun onSurfaceSwipeDelete(steps: Int) {} fun onUpWithDeletePointerActive() fun onUpWithPointerActive() fun onSwipeLanguage(direction: Int) diff --git a/java/src/org/futo/inputmethod/engine/general/GeneralIME.kt b/java/src/org/futo/inputmethod/engine/general/GeneralIME.kt index 06370ccc0b..8cc8de28e0 100644 --- a/java/src/org/futo/inputmethod/engine/general/GeneralIME.kt +++ b/java/src/org/futo/inputmethod/engine/general/GeneralIME.kt @@ -672,6 +672,34 @@ class GeneralIME(val helper: IMEHelper) : IMEInterface, WordLearner, SuggestionS } } + override fun onSurfaceSwipeDelete(steps: Int) { + setNeutralSuggestionStrip() + if (inputLogic.mConnection.hasCursorPosition()) { + val count = kotlin.math.abs(steps) + repeat(count) { + if (steps < 0) { + inputLogic.cursorLeft(-1, true, true) + } else { + inputLogic.cursorRight(1, true, true) + } + } + } else { + var remaining = steps + while (remaining < 0) { + onEvent( + Event.createSoftwareKeypressEvent( + Event.NOT_A_CODE_POINT, + Constants.CODE_DELETE, + Constants.NOT_A_COORDINATE, + Constants.NOT_A_COORDINATE, + false + ) + ) + remaining++ + } + } + } + override fun onUpWithDeletePointerActive() { if (inputLogic.mConnection.hasSelection()) { val selection: CharSequence? = inputLogic.mConnection.getSelectedText(0) diff --git a/java/src/org/futo/inputmethod/keyboard/KeyboardActionListener.java b/java/src/org/futo/inputmethod/keyboard/KeyboardActionListener.java index 813426429e..f5581853cf 100644 --- a/java/src/org/futo/inputmethod/keyboard/KeyboardActionListener.java +++ b/java/src/org/futo/inputmethod/keyboard/KeyboardActionListener.java @@ -103,6 +103,7 @@ public interface KeyboardActionListener { public void onMovePointer(int steps); public void onMoveDeletePointer(int steps); + public void onSurfaceSwipeDelete(int steps); public void onUpWithDeletePointerActive(); public void onUpWithPointerActive(); public void onMovingCursorLockEvent(boolean canMoveCursor); @@ -140,6 +141,8 @@ public void onMovePointer(int steps) {} @Override public void onMoveDeletePointer(int steps) {} @Override + public void onSurfaceSwipeDelete(int steps) {} + @Override public void onUpWithDeletePointerActive() {} @Override public void onUpWithPointerActive() {} diff --git a/java/src/org/futo/inputmethod/keyboard/PointerTracker.java b/java/src/org/futo/inputmethod/keyboard/PointerTracker.java index 6469fe1ad2..19ee59520c 100644 --- a/java/src/org/futo/inputmethod/keyboard/PointerTracker.java +++ b/java/src/org/futo/inputmethod/keyboard/PointerTracker.java @@ -34,6 +34,7 @@ import org.futo.inputmethod.keyboard.internal.GestureStrokeRecognitionParams; import org.futo.inputmethod.keyboard.internal.KeyboardState; import org.futo.inputmethod.keyboard.internal.PointerTrackerQueue; +import org.futo.inputmethod.keyboard.internal.SurfaceSwipeDeleteDetector; import org.futo.inputmethod.keyboard.internal.TimerProxy; import org.futo.inputmethod.keyboard.internal.TypingTimeRecorder; import org.futo.inputmethod.latin.R; @@ -159,6 +160,10 @@ public static void setStateHint(StateHint stateHint) { private boolean mProgressReported = false; private boolean mSpacebarLongPressed = false; + private boolean mIsSurfaceSwiping = false; + private final SurfaceSwipeDeleteDetector mSurfaceSwipeDeleteDetector = + new SurfaceSwipeDeleteDetector(); + // true if keyboard layout has been changed. private boolean mKeyboardLayoutHasBeenChanged; @@ -720,6 +725,8 @@ private boolean areTwoKeysCompatibleFollowingLayoutChange(final Key a, final Key } private void onDownEventInternal(final int x, final int y, final long eventTime) { + final int surfaceSwipeHoldTimeout = + Settings.getInstance().getCurrent().mKeyLongpressTimeout; Key key = onDownKey(x, y, eventTime); // Key selection by dragging finger is allowed when 1) key selection by dragging finger is // enabled by configuration, 2) this pointer starts dragging from modifier key, or 3) this @@ -730,6 +737,8 @@ private void onDownEventInternal(final int x, final int y, final long eventTime) || mKeyDetector.alwaysAllowsKeySelectionByDraggingFinger(); mKeyboardLayoutHasBeenChanged = false; mIsTrackingForActionDisabled = false; + mIsSurfaceSwiping = false; + mSurfaceSwipeDeleteDetector.cancel(); resetKeySelectionByDraggingFinger(); if (key != null) { // This onPress call may have changed keyboard layout. Those cases are detected at @@ -766,6 +775,17 @@ private void onDownEventInternal(final int x, final int y, final long eventTime) mIsFlickingKey = !mIsSlidingCursor && key.getHasFlick(); mFlickDirection = key.flickDirection(0, 0); mCurrentKey = key; + + if (!mIsSlidingCursor && !mIsFlickingKey && !key.isModifier()) { + mIsSurfaceSwiping = true; + mSurfaceSwipeDeleteDetector.start(x, sPointerBigStep, surfaceSwipeHoldTimeout); + } + } else { + mStartX = x; + mStartY = y; + mStartTime = System.currentTimeMillis(); + mIsSurfaceSwiping = true; + mSurfaceSwipeDeleteDetector.start(x, sPointerBigStep, surfaceSwipeHoldTimeout); } } @@ -1029,6 +1049,34 @@ private void onMoveEventInternal(final int x, final int y, final long eventTime) return; } + if (!sInGesture && mIsSurfaceSwiping + && getActivePointerTrackerCount() == 1 + && settingsValues.mSurfaceSwipeDeleteEnabled) { + final int swipeIgnoreTime = settingsValues.mKeyLongpressTimeout + / MULTIPLIER_FOR_LONG_PRESS_TIMEOUT_IN_SLIDING_INPUT; + final boolean pastDeadTime = + mStartTime + swipeIgnoreTime < System.currentTimeMillis(); + if (mSurfaceSwipeDeleteDetector.isActive() || pastDeadTime) { + final int steps = + mSurfaceSwipeDeleteDetector.onMove(x, settingsValues.mIsRTL, eventTime); + if (steps != 0) { + if (oldKey != null) { + sTimerProxy.cancelKeyTimersOf(this); + setReleasedKeyGraphics(oldKey, true /* withAnimation */); + mCurrentKey = null; + mIsDetectingGesture = false; + } + mCursorMoved = true; + sListener.onSurfaceSwipeDelete(steps); + } + if (mSurfaceSwipeDeleteDetector.isActive()) { + mLastX = x; + mLastY = y; + return; + } + } + } + if(mIsFlickingKey && oldKey != null) { final Direction prevDirection = mFlickDirection; mFlickDirection = oldKey.flickDirection(x - mStartX, y - mStartY); @@ -1142,7 +1190,11 @@ private void onUpEventInternal(final int x, final int y, final long eventTime) { sListener.onSwipeLanguageReleased(); mProgressReported = false; } - if(mCursorMoved && currentKey != null && currentKey.getCode() == Constants.CODE_DELETE) { + if(mSurfaceSwipeDeleteDetector.isActive()) { + sListener.onUpWithDeletePointerActive(); + mSurfaceSwipeDeleteDetector.cancel(); + mIsSurfaceSwiping = false; + } else if(mCursorMoved && currentKey != null && currentKey.getCode() == Constants.CODE_DELETE) { sListener.onUpWithDeletePointerActive(); } else if(mCursorMoved) { sListener.onUpWithPointerActive(); @@ -1296,6 +1348,8 @@ private void onCancelEventInternal() { setReleasedKeyGraphics(mCurrentKey, true /* withAnimation */); resetKeySelectionByDraggingFinger(); dismissMoreKeysPanel(); + mSurfaceSwipeDeleteDetector.cancel(); + mIsSurfaceSwiping = false; } private boolean isMajorEnoughMoveToBeOnNewKey(final int x, final int y, final long eventTime, diff --git a/java/src/org/futo/inputmethod/keyboard/internal/SurfaceSwipeDeleteDetector.java b/java/src/org/futo/inputmethod/keyboard/internal/SurfaceSwipeDeleteDetector.java new file mode 100644 index 0000000000..fe61ea06c7 --- /dev/null +++ b/java/src/org/futo/inputmethod/keyboard/internal/SurfaceSwipeDeleteDetector.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.futo.inputmethod.keyboard.internal; + +public final class SurfaceSwipeDeleteDetector { + private int mStep; + private int mStartX; + private boolean mActive; + private int mSelectedWords; + private long mHoldTimeout; + private long mActivationTime; + private boolean mAdjustmentEnabled; + + public void start(final int startX, final int step, final long holdTimeout) { + mStartX = startX; + mStep = step > 0 ? step : 1; + mHoldTimeout = holdTimeout; + mActive = false; + mSelectedWords = 0; + mAdjustmentEnabled = false; + } + + public boolean isActive() { + return mActive; + } + + public int getSelectedWords() { + return mSelectedWords; + } + + public void cancel() { + mActive = false; + mSelectedWords = 0; + mAdjustmentEnabled = false; + } + + public int onMove(final int x, final boolean isRTL, final long eventTime) { + if (!mActive) { + final int steps = (x - mStartX) / mStep; + if (steps >= 0) { + return 0; + } + mActive = true; + mSelectedWords = 1; + mActivationTime = eventTime; + mAdjustmentEnabled = false; + mStartX = x; + return isRTL ? 1 : -1; + } + if (!mAdjustmentEnabled) { + if (eventTime - mActivationTime < mHoldTimeout) { + return 0; + } + mAdjustmentEnabled = true; + mStartX = x; + } + final int steps = (x - mStartX) / mStep; + if (steps == 0) { + return 0; + } + final int rawDelta = isRTL ? -steps : steps; + final int wordDelta = isRTL ? rawDelta : -rawDelta; + final int selectedWords = Math.max(0, mSelectedWords + wordDelta); + final int appliedDelta = selectedWords - mSelectedWords; + mSelectedWords = selectedWords; + mStartX += steps * mStep; + if (appliedDelta == 0) { + return 0; + } + return isRTL ? appliedDelta : -appliedDelta; + } +} diff --git a/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java b/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java index 7807c3dc84..25b893e026 100644 --- a/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java +++ b/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java @@ -639,6 +639,13 @@ public void onMoveDeletePointer(int steps) { ).onMoveDeletePointer(steps); } + @Override + public void onSurfaceSwipeDelete(int steps) { + mImeManager.getActiveIME( + mSettings.getCurrent() + ).onSurfaceSwipeDelete(steps); + } + @Override public void onUpWithDeletePointerActive() { mImeManager.getActiveIME( diff --git a/java/src/org/futo/inputmethod/latin/settings/Settings.java b/java/src/org/futo/inputmethod/latin/settings/Settings.java index 055c166a6c..c0b3b9d19f 100644 --- a/java/src/org/futo/inputmethod/latin/settings/Settings.java +++ b/java/src/org/futo/inputmethod/latin/settings/Settings.java @@ -137,6 +137,8 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang public static final int SPACEBAR_MODE_CURSOR = 1; public static final int SPACEBAR_MODE_LANGUAGE = 2; + public static final String PREF_SURFACE_SWIPE_DELETE = "pref_surface_swipe_delete"; + public static final String PREF_BACKSPACE_MODE_HOLD = "pref_backspace_mode_hold"; public static final String PREF_BACKSPACE_MODE = "pref_backspace_mode"; public static final int BACKSPACE_MODE_CHARACTERS = 0; // Long-press backspace and swipe backspace removes just characters diff --git a/java/src/org/futo/inputmethod/latin/settings/SettingsValues.java b/java/src/org/futo/inputmethod/latin/settings/SettingsValues.java index 30803dc90a..5c6a27df51 100644 --- a/java/src/org/futo/inputmethod/latin/settings/SettingsValues.java +++ b/java/src/org/futo/inputmethod/latin/settings/SettingsValues.java @@ -110,6 +110,7 @@ public class SettingsValues { public final int mSpacebarHoldMode; public final int mBackspaceMode; public final int mBackspaceModeHold; + public final boolean mSurfaceSwipeDeleteEnabled; public final int mNumberRowMode; public final int mAltSpacesMode; @@ -212,6 +213,7 @@ public SettingsValues(final Context context, final SharedPreferences prefs, fina mBackspaceMode = prefs.getInt(Settings.PREF_BACKSPACE_MODE, Settings.BACKSPACE_MODE_CHARACTERS); mBackspaceModeHold = prefs.getInt(Settings.PREF_BACKSPACE_MODE_HOLD, mBackspaceMode); + mSurfaceSwipeDeleteEnabled = prefs.getBoolean(Settings.PREF_SURFACE_SWIPE_DELETE, true); mNumberRowMode = mIsNumberRowEnabledByUser ? prefs.getInt(Settings.PREF_NUMBER_ROW_MODE, Settings.NUMBER_ROW_MODE_DEFAULT) : Settings.NUMBER_ROW_MODE_DEFAULT; diff --git a/java/src/org/futo/inputmethod/latin/uix/settings/pages/Swipe.kt b/java/src/org/futo/inputmethod/latin/uix/settings/pages/Swipe.kt index 54a44ec7dc..7d6790aa35 100644 --- a/java/src/org/futo/inputmethod/latin/uix/settings/pages/Swipe.kt +++ b/java/src/org/futo/inputmethod/latin/uix/settings/pages/Swipe.kt @@ -232,6 +232,13 @@ val SwipeMenu = UserSettingsMenu( default = {false}, ), + userSettingToggleSharedPrefs( + title = R.string.swipe_settings_surface_swipe_delete, + subtitle = R.string.swipe_settings_surface_swipe_delete_subtitle, + key = Settings.PREF_SURFACE_SWIPE_DELETE, + default = {true}, + ), + // KASROZ is primarily for English and the menu isn't translated, so it's hidden if user // doesn't have English layout UserSetting(R.string.swipe_settings_kasroz, subtitle = R.string.swipe_settings_kasroz_subtitle, visibilityCheck = { diff --git a/src/test/java/org/futo/inputmethod/keyboard/internal/SurfaceSwipeDeleteDetectorTest.java b/src/test/java/org/futo/inputmethod/keyboard/internal/SurfaceSwipeDeleteDetectorTest.java new file mode 100644 index 0000000000..32d25e271d --- /dev/null +++ b/src/test/java/org/futo/inputmethod/keyboard/internal/SurfaceSwipeDeleteDetectorTest.java @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.futo.inputmethod.keyboard.internal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Before; +import org.junit.Test; + +public class SurfaceSwipeDeleteDetectorTest { + private static final int STEP = 100; + private static final int START_X = 1000; + private static final long HOLD_TIMEOUT = 500; + + private SurfaceSwipeDeleteDetector mDetector; + + @Before + public void setUp() { + mDetector = new SurfaceSwipeDeleteDetector(); + mDetector.start(START_X, STEP, HOLD_TIMEOUT); + } + + @Test + public void rightwardSwipeNeverInitiates() { + assertEquals(0, mDetector.onMove(START_X + 350, false, 0)); + assertFalse(mDetector.isActive()); + assertEquals(0, mDetector.onMove(START_X + 50, false, 0)); + assertFalse(mDetector.isActive()); + } + + @Test + public void movementBelowStepDoesNothing() { + assertEquals(0, mDetector.onMove(START_X - (STEP - 1), false, 0)); + assertFalse(mDetector.isActive()); + } + + @Test + public void singleLeftStepDeletesOneWord() { + assertEquals(-1, mDetector.onMove(START_X - STEP, false, 0)); + assertTrue(mDetector.isActive()); + } + + @Test + public void longLeftSwipeStillSelectsExactlyOneWord() { + assertEquals(-1, mDetector.onMove(START_X - 3 * STEP, false, 0)); + assertTrue(mDetector.isActive()); + assertEquals(1, mDetector.getSelectedWords()); + } + + @Test + public void immediateReleaseAfterLongSwipeKeepsOneWord() { + assertEquals(-1, mDetector.onMove(START_X - 5 * STEP, false, 0)); + assertEquals(0, mDetector.onMove(START_X - 8 * STEP, false, HOLD_TIMEOUT - 1)); + assertEquals(1, mDetector.getSelectedWords()); + } + + @Test + public void noAdjustmentBeforeHold() { + assertEquals(-1, mDetector.onMove(START_X - STEP, false, 0)); + assertEquals(0, mDetector.onMove(START_X - 4 * STEP, false, HOLD_TIMEOUT - 1)); + assertEquals(0, mDetector.onMove(START_X + 2 * STEP, false, HOLD_TIMEOUT - 1)); + assertEquals(1, mDetector.getSelectedWords()); + } + + @Test + public void adjustmentOnlyAfterHoldWithRebaseline() { + assertEquals(-1, mDetector.onMove(START_X - 3 * STEP, false, 0)); + final int driftX = START_X - 6 * STEP; + assertEquals(0, mDetector.onMove(driftX, false, HOLD_TIMEOUT - 1)); + assertEquals(1, mDetector.getSelectedWords()); + assertEquals(0, mDetector.onMove(driftX, false, HOLD_TIMEOUT)); + assertEquals(1, mDetector.getSelectedWords()); + assertEquals(-1, mDetector.onMove(driftX - STEP, false, HOLD_TIMEOUT)); + assertEquals(2, mDetector.getSelectedWords()); + assertEquals(-1, mDetector.onMove(driftX - 2 * STEP, false, HOLD_TIMEOUT)); + assertEquals(3, mDetector.getSelectedWords()); + assertEquals(1, mDetector.onMove(driftX - STEP, false, HOLD_TIMEOUT)); + assertEquals(2, mDetector.getSelectedWords()); + } + + @Test + public void adjustmentClampsAtZeroWords() { + assertEquals(-1, mDetector.onMove(START_X - STEP, false, 0)); + final int origin = START_X - STEP; + assertEquals(0, mDetector.onMove(origin, false, HOLD_TIMEOUT)); + assertEquals(-1, mDetector.onMove(origin - STEP, false, HOLD_TIMEOUT)); + assertEquals(2, mDetector.getSelectedWords()); + assertEquals(2, mDetector.onMove(origin + 5 * STEP, false, HOLD_TIMEOUT)); + assertEquals(0, mDetector.getSelectedWords()); + assertEquals(0, mDetector.onMove(origin + 8 * STEP, false, HOLD_TIMEOUT)); + assertTrue(mDetector.isActive()); + } + + @Test + public void rtlAdjustsInMirrorDirection() { + assertEquals(1, mDetector.onMove(START_X - STEP, true, 0)); + assertEquals(1, mDetector.getSelectedWords()); + final int origin = START_X - STEP; + assertEquals(0, mDetector.onMove(origin, true, HOLD_TIMEOUT)); + assertEquals(1, mDetector.onMove(origin - STEP, true, HOLD_TIMEOUT)); + assertEquals(2, mDetector.getSelectedWords()); + assertEquals(-2, mDetector.onMove(origin + 5 * STEP, true, HOLD_TIMEOUT)); + assertEquals(0, mDetector.getSelectedWords()); + } + + @Test + public void stepsAreIncremental() { + assertEquals(-1, mDetector.onMove(START_X - STEP, false, 0)); + assertEquals(0, mDetector.onMove(START_X - STEP, false, HOLD_TIMEOUT)); + assertEquals(-1, mDetector.onMove(START_X - 2 * STEP, false, HOLD_TIMEOUT)); + assertEquals(0, mDetector.onMove(START_X - 2 * STEP - (STEP - 1), false, HOLD_TIMEOUT)); + assertEquals(-1, mDetector.onMove(START_X - 3 * STEP, false, HOLD_TIMEOUT)); + } + + @Test + public void rtlFlipsDirection() { + assertEquals(1, mDetector.onMove(START_X - STEP, true, 0)); + assertTrue(mDetector.isActive()); + } + + @Test + public void movingBackRightReleasesSelection() { + assertEquals(-1, mDetector.onMove(START_X - 2 * STEP, false, 0)); + assertEquals(0, mDetector.onMove(START_X - 2 * STEP, false, HOLD_TIMEOUT)); + assertEquals(1, mDetector.onMove(START_X - STEP, false, HOLD_TIMEOUT)); + assertTrue(mDetector.isActive()); + assertEquals(0, mDetector.getSelectedWords()); + } + + @Test + public void cancelStopsGesture() { + mDetector.onMove(START_X - STEP, false, 0); + assertTrue(mDetector.isActive()); + mDetector.cancel(); + assertFalse(mDetector.isActive()); + assertEquals(0, mDetector.onMove(START_X + STEP, false, 0)); + } + + @Test + public void startResetsState() { + mDetector.onMove(START_X - STEP, false, 0); + assertTrue(mDetector.isActive()); + mDetector.start(500, STEP, HOLD_TIMEOUT); + assertFalse(mDetector.isActive()); + assertEquals(-1, mDetector.onMove(500 - STEP, false, 0)); + } + + @Test + public void nonPositiveStepIsGuarded() { + mDetector.start(START_X, 0, HOLD_TIMEOUT); + assertEquals(-1, mDetector.onMove(START_X - 1, false, 0)); + assertTrue(mDetector.isActive()); + } + + @Test + public void keyPressJitterDoesNotActivate() { + for (int dx = -5; dx <= 5; dx++) { + assertEquals(0, mDetector.onMove(START_X + dx, false, 0)); + } + assertFalse(mDetector.isActive()); + } + + @Test + public void rightwardDriftFromKeyDoesNotActivate() { + assertEquals(0, mDetector.onMove(START_X + 200, false, 0)); + assertEquals(0, mDetector.onMove(START_X + 100, false, 0)); + assertFalse(mDetector.isActive()); + } + + @Test + public void leftSwipeFromKeyActivatesAfterFullStep() { + assertEquals(0, mDetector.onMove(START_X - (STEP - 1), false, 0)); + assertFalse(mDetector.isActive()); + assertEquals(-1, mDetector.onMove(START_X - STEP, false, 0)); + assertTrue(mDetector.isActive()); + } + + @Test + public void restartFromNewKeyResetsOrigin() { + assertEquals(-1, mDetector.onMove(START_X - STEP, false, 0)); + assertTrue(mDetector.isActive()); + mDetector.start(500, STEP, HOLD_TIMEOUT); + assertFalse(mDetector.isActive()); + assertEquals(0, mDetector.onMove(500 - (STEP - 1), false, 0)); + assertEquals(-1, mDetector.onMove(500 - STEP, false, 0)); + assertTrue(mDetector.isActive()); + } +}