Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions SPEC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Step-One Tap + Gesture Combining

## Goal

Add the smallest upstreamable foundation for two-thumb typing: allow tap input from one pointer and a simultaneous/overlapping gesture from another pointer to coexist during the current word, and optionally draw a visual trail of taps and gestures for debugging.

This is intentionally not the full fork two-thumb system. It is a first slice that makes the input pipeline tolerate overlapping multi-pointer mixed tap/gesture word construction while preserving upstream's current word-boundary behavior. A single-finger tap followed later by a single-finger swipe after all fingers have left the keyboard is out of scope for this step.

## Non-goals

- No spacing changes.
- No phantom-space behavior.
- No grace timers.
- No auto-finish/auto-commit delay after all fingers leave the keyboard.
- No re-recognize experiments.
- No toolbar, clipboard, dictionary, or shortcut-row changes.
- No local test identity changes (`applicationId`, package name, app label, version).
- No Java/Kotlin package or namespace refactor.

## User-visible behavior

Add exactly two toggles under Gesture Typing, in a new category:

1. `Combine simultaneous tapping and additional gestures`
- Allows mixed tap and gesture input only when the subsequent gesture starts while at least one other pointer is still active (overlapping multi-pointer input).
- Does not keep the word open after all fingers leave the keyboard; a later single-finger swipe starts a new word and keeps the normal fast-typing cooldown.
2. `Draw taps and gestures`
- Draws visual debug points/trails for tap and gesture input when the first toggle is enabled.
- Has no effect on committed text.

Both default off for upstream safety.

## Design

### Settings

Follow the mandatory settings five-file pattern:

1. `Settings.java` — add two `PREF_*` constants.
2. `Defaults.kt` — add default values.
3. `SettingsValues.java` — cache the two booleans.
4. `res/values/strings.xml` — add category/title/summary strings.
5. Gesture settings screen — add the new category and toggles.

Update `SettingsContainerTest` if the settings screens are validated there.

### Input behavior

The first toggle should relax the input pipeline only enough to avoid rejecting a gesture when another pointer is still active and tap input has just contributed to the composing word. The cooldown bypass must require overlapping multi-pointer input, so a rapid tap-space-gesture sequence across a no-fingers word boundary remains a new word and keeps upstream fast-typing suppression. This branch must not add timers, deferred spacing, or no-finger continuation semantics.

### Visual trail

Reuse existing drawing surfaces/preview plumbing where possible. The debug trail should be cheap when disabled and must not allocate avoidably in hot paths.

The debug view should show enough information to verify mixed input ordering: tap points and gesture path points. It is diagnostic only and may be visually simple.

### Tests

Add focused tests for:

- settings defaults/wiring;
- any unit-testable gate or state object introduced for mixed tap/gesture behavior.

## Verification

Run targeted tests only:

- `:app:testOfflineDebugUnitTest --tests "*SettingsContainerTest*"`
- any new focused tests for the mixed input gate/state

## Commit plan

1. Commit this `SPEC.md` design.
2. Commit the upstreamable implementation.
3. Add a separate local-only test identity commit after the upstreamable implementation is complete and verified.
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import helium314.keyboard.keyboard.internal.PopupKeySpec;
import helium314.keyboard.keyboard.internal.NonDistinctMultitouchHelper;
import helium314.keyboard.keyboard.internal.SlidingKeyInputDrawingPreview;
import helium314.keyboard.keyboard.internal.TapGestureDrawingPreview;
import helium314.keyboard.keyboard.internal.TimerHandler;
import helium314.keyboard.keyboard.internal.KeyboardIconsSet;
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode;
Expand Down Expand Up @@ -124,6 +125,7 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy

// Gesture floating preview text
private final int mGestureFloatingPreviewTextLingerTimeout;
private final TapGestureDrawingPreview mTapGestureDrawingPreview;

private final KeyDetector mKeyDetector;
private final NonDistinctMultitouchHelper mNonDistinctMultitouchHelper;
Expand Down Expand Up @@ -208,6 +210,9 @@ public MainKeyboardView(final Context context, final AttributeSet attrs, final i

mSlidingKeyInputDrawingPreview = new SlidingKeyInputDrawingPreview(mainKeyboardViewAttr);
mSlidingKeyInputDrawingPreview.setDrawingView(drawingPreviewPlacerView);

mTapGestureDrawingPreview = new TapGestureDrawingPreview();
mTapGestureDrawingPreview.setDrawingView(drawingPreviewPlacerView);
mainKeyboardViewAttr.recycle();

mDrawingPreviewPlacerView = drawingPreviewPlacerView;
Expand Down Expand Up @@ -449,6 +454,9 @@ private void setGesturePreviewMode(final boolean isGestureTrailEnabled,
final boolean isGestureFloatingPreviewTextEnabled) {
mGestureFloatingTextDrawingPreview.setPreviewEnabled(isGestureFloatingPreviewTextEnabled);
mGestureTrailsDrawingPreview.setPreviewEnabled(isGestureTrailEnabled);
mTapGestureDrawingPreview.setPreviewEnabled(
Settings.getValues().mGestureCombineTapsAndGestures
&& Settings.getValues().mGestureDrawTapsAndGestures);
}

public void showGestureFloatingPreviewText(@NonNull final SuggestedWords suggestedWords,
Expand All @@ -468,6 +476,25 @@ public void dismissGestureFloatingPreviewTextWithoutDelay() {
mGestureFloatingTextDrawingPreview.dismissGestureFloatingPreviewText();
}

@Override
public void setTapGesturePreviewEnabled(final boolean enabled) {
mTapGestureDrawingPreview.setPreviewEnabled(enabled);
if (!enabled) {
mTapGestureDrawingPreview.clear();
}
}

@Override
public void recordTapGesturePoint(final int x, final int y, final boolean gesture) {
locatePreviewPlacerView();
mTapGestureDrawingPreview.addPoint(x, y, gesture);
}

@Override
public void clearTapGesturePreview() {
mTapGestureDrawingPreview.clear();
}

@Override
public void showGestureTrail(@NonNull final PointerTracker tracker,
final boolean showsFloatingPreviewText) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,7 @@ private void cancelBatchInput() {
if (DEBUG_LISTENER) {
Log.d(TAG, String.format(Locale.US, "[%d] onCancelBatchInput", mPointerId));
}
sDrawingProxy.clearTapGesturePreview();
sListener.onCancelBatchInput();
}

Expand Down Expand Up @@ -715,6 +716,11 @@ private void onDownEvent(final int x, final int y, final long eventTime,
}
}
sPointerTrackerQueue.add(this);
if (Settings.getValues().mGestureCombineTapsAndGestures
&& Settings.getValues().mGestureDrawTapsAndGestures
&& getActivePointerTrackerCount() == 1) {
sDrawingProxy.clearTapGesturePreview();
}
onDownEventInternal(x, y, eventTime);
if (!sGestureEnabler.shouldHandleGesture()) {
return;
Expand All @@ -725,10 +731,19 @@ private void onDownEvent(final int x, final int y, final long eventTime,
mIsDetectingGesture = (mKeyboard != null) && mKeyboard.mId.isAlphabetKeyboard()
&& key != null && !key.isModifier() && !mKeySwipeAllowed && !sInKeySwipe;
if (mIsDetectingGesture) {
final SettingsValues settingsValues = Settings.getValues();
final boolean combineTapsAndGestures = settingsValues.mGestureCombineTapsAndGestures
&& getActivePointerTrackerCount() >= 2
&& sTypingTimeRecorder.wasLastCodeInputLetter();
mBatchInputArbiter.addDownEventPoint(x, y, eventTime,
sTypingTimeRecorder.getLastLetterTypingTime(), getActivePointerTrackerCount());
sTypingTimeRecorder.getLastLetterTypingTime(), getActivePointerTrackerCount(),
combineTapsAndGestures);
mGestureStrokeDrawingPoints.onDownEvent(
x, y, mBatchInputArbiter.getElapsedTimeSinceFirstDown(eventTime));
if (settingsValues.mGestureCombineTapsAndGestures
&& settingsValues.mGestureDrawTapsAndGestures) {
sDrawingProxy.recordTapGesturePoint(x, y, true);
}
}
}

Expand Down Expand Up @@ -1253,6 +1268,11 @@ eventTime, getActivePointerTrackerCount(), this)) {
&& (currentKey.getCode() == currentRepeatingKeyCode) && !isInDraggingFinger) {
return;
}
if (Settings.getValues().mGestureCombineTapsAndGestures
&& Settings.getValues().mGestureDrawTapsAndGestures && currentKey != null
&& !currentKey.isModifier()) {
sDrawingProxy.recordTapGesturePoint(x, y, false);
}
detectAndSendKey(currentKey, mKeyX, mKeyY, eventTime);
if (isInSlidingKeyInput) {
callListenerOnFinishSlidingInput();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,18 @@ public int getElapsedTimeSinceFirstDown(final long eventTime) {
*/
public void addDownEventPoint(final int x, final int y, final long downEventTime,
final long lastLetterTypingTime, final int activePointerCount) {
addDownEventPoint(x, y, downEventTime, lastLetterTypingTime, activePointerCount, false);
}

public void addDownEventPoint(final int x, final int y, final long downEventTime,
final long lastLetterTypingTime, final int activePointerCount,
final boolean ignoreFastTypingCooldown) {
if (activePointerCount == 1) {
sGestureFirstDownTime = downEventTime;
}
final int elapsedTimeSinceFirstDown = getElapsedTimeSinceFirstDown(downEventTime);
final int elapsedTimeSinceLastTyping = (int)(downEventTime - lastLetterTypingTime);
final int elapsedTimeSinceLastTyping = ignoreFastTypingCooldown
? Integer.MAX_VALUE : (int)(downEventTime - lastLetterTypingTime);
mRecognitionPoints.addDownEventPoint(
x, y, elapsedTimeSinceFirstDown, elapsedTimeSinceLastTyping);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,10 @@ public interface DrawingProxy {
* Dismiss a gesture floating preview text without delay.
*/
void dismissGestureFloatingPreviewTextWithoutDelay();

void setTapGesturePreviewEnabled(boolean enabled);

void recordTapGesturePoint(int x, int y, boolean gesture);

void clearTapGesturePreview();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* SPDX-License-Identifier: GPL-3.0-only
*/
package helium314.keyboard.keyboard.internal;

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;

import androidx.annotation.NonNull;

import helium314.keyboard.keyboard.PointerTracker;

/** Draws simple diagnostic tap and gesture points for mixed tap/gesture input. */
public final class TapGestureDrawingPreview extends AbstractDrawingPreview {
private static final int MAX_POINTS = 192;
private static final float TAP_RADIUS = 9.0f;
private static final float GESTURE_RADIUS = 5.0f;

private final int[] mXs = new int[MAX_POINTS];
private final int[] mYs = new int[MAX_POINTS];
private final boolean[] mGesture = new boolean[MAX_POINTS];
private int mSize;
private int mKeyboardHeight;

private final Paint mTapPaint = new Paint();
private final Paint mGesturePaint = new Paint();
private final Paint mLinePaint = new Paint();

public TapGestureDrawingPreview() {
mTapPaint.setAntiAlias(true);
mTapPaint.setColor(0xCCFF9800);
mTapPaint.setStyle(Paint.Style.FILL);

mGesturePaint.setAntiAlias(true);
mGesturePaint.setColor(0xCC03A9F4);
mGesturePaint.setStyle(Paint.Style.FILL);

mLinePaint.setAntiAlias(true);
mLinePaint.setColor(0x9903A9F4);
mLinePaint.setStrokeWidth(3.0f);
mLinePaint.setStyle(Paint.Style.STROKE);
}

@Override
public void setKeyboardViewGeometry(@NonNull final int[] originCoords, final int width,
final int height) {
super.setKeyboardViewGeometry(originCoords, width, height);
mKeyboardHeight = height;
}

public void clear() {
mSize = 0;
invalidateDrawingView();
}

public void addPoint(final int x, final int y, final boolean gesture) {
if (!isPreviewEnabled()) {
return;
}
if (mSize == MAX_POINTS) {
System.arraycopy(mXs, 1, mXs, 0, MAX_POINTS - 1);
System.arraycopy(mYs, 1, mYs, 0, MAX_POINTS - 1);
System.arraycopy(mGesture, 1, mGesture, 0, MAX_POINTS - 1);
mSize = MAX_POINTS - 1;
}
mXs[mSize] = x;
mYs[mSize] = y;
mGesture[mSize] = gesture;
mSize++;
invalidateDrawingView();
}

@Override
public void onDeallocateMemory() {
clear();
}

@Override
public void drawPreview(@NonNull final Canvas canvas) {
if (!isPreviewEnabled() || mSize == 0) {
return;
}
int lastGesture = -1;
for (int i = 0; i < mSize; i++) {
if (mGesture[i]) {
if (lastGesture >= 0) {
canvas.drawLine(mXs[lastGesture], mYs[lastGesture], mXs[i], mYs[i], mLinePaint);
}
canvas.drawCircle(mXs[i], mYs[i], GESTURE_RADIUS, mGesturePaint);
lastGesture = i;
} else {
canvas.drawCircle(mXs[i], mYs[i], TAP_RADIUS, mTapPaint);
lastGesture = -1;
}
}
}

@Override
public void setPreviewPosition(@NonNull final PointerTracker tracker) {
// Points are pushed explicitly by PointerTracker.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public final class TypingTimeRecorder {
private long mLastTypingTime;
private long mLastLetterTypingTime;
private long mLastBatchInputTime;
private boolean mLastCodeInputWasLetter;

public TypingTimeRecorder(final int staticTimeThresholdAfterFastTyping,
final int suppressKeyPreviewAfterBatchInputDuration) {
Expand All @@ -29,6 +30,7 @@ private boolean wasLastInputTyping() {
}

public void onCodeInput(final int code, final long eventTime) {
mLastCodeInputWasLetter = Character.isLetter(code);
// 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.
Expand All @@ -55,6 +57,10 @@ public long getLastLetterTypingTime() {
return mLastLetterTypingTime;
}

public boolean wasLastCodeInputLetter() {
return mLastCodeInputWasLetter;
}

public boolean needsToSuppressKeyPreviewPopup(final long eventTime) {
return !wasLastInputTyping()
&& eventTime - mLastBatchInputTime < mSuppressKeyPreviewAfterBatchInputDuration;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ object Defaults {
const val PREF_GESTURE_SPACE_AWARE = false
const val PREF_GESTURE_FAST_TYPING_COOLDOWN = 500
const val PREF_GESTURE_TRAIL_FADEOUT_DURATION = 800
const val PREF_GESTURE_COMBINE_TAPS_AND_GESTURES = false
const val PREF_GESTURE_DRAW_TAPS_AND_GESTURES = false
const val PREF_SHOW_SETUP_WIZARD_ICON = true
const val PREF_USE_CONTACTS = false
const val PREF_USE_APPS = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public static final String PREF_GESTURE_SPACE_AWARE = "gesture_space_aware";
public static final String PREF_GESTURE_FAST_TYPING_COOLDOWN = "gesture_fast_typing_cooldown";
public static final String PREF_GESTURE_TRAIL_FADEOUT_DURATION = "gesture_trail_fadeout_duration";
public static final String PREF_GESTURE_COMBINE_TAPS_AND_GESTURES = "gesture_combine_taps_and_gestures";
public static final String PREF_GESTURE_DRAW_TAPS_AND_GESTURES = "gesture_draw_taps_and_gestures";
public static final String PREF_SHOW_SETUP_WIZARD_ICON = "show_setup_wizard_icon";
public static final String PREF_USE_CONTACTS = "use_contacts";
public static final String PREF_USE_APPS = "use_apps";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ public class SettingsValues {
public final boolean mGestureFloatingPreviewDynamicEnabled;
public final int mGestureFastTypingCooldown;
public final int mGestureTrailFadeoutDuration;
public final boolean mGestureCombineTapsAndGestures;
public final boolean mGestureDrawTapsAndGestures;
public final boolean mSlidingKeyInputPreviewEnabled;
public final int mKeyLongpressTimeout;
public final boolean mEnableEmojiAltPhysicalKey;
Expand Down Expand Up @@ -328,6 +330,12 @@ public SettingsValues(final Context context, final SharedPreferences prefs, fina
Defaults.PREF_GESTURE_FAST_TYPING_COOLDOWN);
mGestureTrailFadeoutDuration = prefs.getInt(Settings.PREF_GESTURE_TRAIL_FADEOUT_DURATION,
Defaults.PREF_GESTURE_TRAIL_FADEOUT_DURATION);
mGestureCombineTapsAndGestures = prefs.getBoolean(
Settings.PREF_GESTURE_COMBINE_TAPS_AND_GESTURES,
Defaults.PREF_GESTURE_COMBINE_TAPS_AND_GESTURES);
mGestureDrawTapsAndGestures = prefs.getBoolean(
Settings.PREF_GESTURE_DRAW_TAPS_AND_GESTURES,
Defaults.PREF_GESTURE_DRAW_TAPS_AND_GESTURES);
mSuggestionStripHiddenPerUserSettings = mToolbarMode == ToolbarMode.HIDDEN
|| mToolbarMode == ToolbarMode.TOOLBAR_KEYS;
mOverrideShowingSuggestions = mInputAttributes.mMayOverrideShowingSuggestions
Expand Down
Loading
Loading