Skip to content

Two-thumb experimental build (upstream v4.1.2 merged, installs side by side) - #141

Merged
AsafMah merged 7 commits into
devfrom
feat/two-thumb-experimental
Aug 20, 2026
Merged

Two-thumb experimental build (upstream v4.1.2 merged, installs side by side)#141
AsafMah merged 7 commits into
devfrom
feat/two-thumb-experimental

Conversation

@AsafMah

@AsafMah AsafMah commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Warning

DUAL_POINTER produces nonsense in real use and should not be presented as a working feature. Typing "ambulance" repeatedly with it enabled yields garbage. Root cause in #144: the research measured the in-repo AOSP decoder, but the decoder that actually runs on any device where this setting is visible is a user-supplied closed library. Merge for the packaging, the bug fix and the settings gating — not for the dual-pointer mode, which stays off by default.

Merges the new dev (LeanBitLab v4.1.2, #137) into the two-thumb work from #136, and makes it installable side by side so it can be A/B tested.

Supersedes #136 — same work, plus the upstream merge, the experimental packaging, and two rounds of fixes from actual device use.

Side-by-side packaging

New experimental build type:

  • applicationIdSuffix = ".exp"com.asafmah.leantypedual.exp
  • versionNameSuffix = "-exp"0.2.0-exp
  • src/experimental/res overrides english_ime_name"LeanTypeDual EXP"

Installing it does not replace the daily driver. Experimental input changes can only be judged against a keyboard that works, and that's impossible if installing one uninstalls the other. The label override matters for the same reason — without it the two are indistinguishable in the picker.

Verified on device: both packages installed, both listed, active IME untouched.

What device use actually found

Three separate problems, all invisible from the UI, none caught by tests:

1. The recognition settings were inert and one was harmful. .exp is a separate package with its own files/, so it had no user-supplied libjni_latinime.so and sHaveNativeGestureLib was false. The mode picker hid itself — but PREF_STROKE_IDEAL_PREFIX was not gated, so its synthesised key-centre points went into the Java fallback engine, which scores a single trail and ignores pointer ids. That produced nonsense words. Now the whole group is gated on a loaded library.

2. Even with the library, the mode could not run. StrokeAligner is only reached from WordComposer.setBatchInputPointers during multi-part composition, which is armed by mGestureManualSpacing || mCombiningGraceMs > 0 — both at defaults. So changing the mode picker legitimately did nothing. The screen now says "Not active at this spacing mode" with the reason, instead of showing settings that structurally cannot take effect.

3. With everything correctly armed, DUAL_POINTER still produces nonsense. See #144. This is the finding that matters: the two-track premise came from app/src/main/jni/, but sHaveNativeGestureLib is true only when a user-supplied or system library is loaded — so the binary being measured was never the binary being used.

What still stands

The merge

Only CHANGELOG.md conflicted across both dev merges; the code auto-merged, as #136 predicted.

Verification

Recommendation

Merge for the packaging, the zero-suggestions fix and the settings honesty. Treat DUAL_POINTER as a parked experiment behind its default-off setting until #144 establishes what the loaded library actually does with a second track.

Copilot AI and others added 3 commits August 20, 2026 06:12
…ck harness (#135)

Research outcome for "make two-thumb typing work WITH the native gesture
decoder". Findings in docs/TWO_THUMB_TEMPORAL_ALIGNMENT.md.

The AOSP decoder models TWO per-pointer tracks (MAX_POINTER_COUNT_G == 2) and
can spell a word by alternating between them. We have never used the second
one: InputPointers.appendAll hardcodes pointer id 0, so every merged multi-part
trail lands in track 0 as one long glide with a synthetic connector.

Measured against the real AOSP ProximityInfoState on the host (new harness):
- track membership is decided purely by pointer id; shifting or overlapping
  timestamps moves zero points between tracks, which falsifies "temporally
  shift the strokes so the library sees them as simultaneous" as a mechanism;
- deliberate overlap drives speed rates negative (impossible from real input),
  i.e. it actively corrupts the features fed to the closed weighting policy;
- global time monotonicity IS required: refreshSpeedRates walks raw indices
  across the pointer boundary, so per-stroke clocks that restart go negative.

Bug fixed: if no point carries id 0, Suggest::initializeSearch early-returns
and the gesture produces zero suggestions. Reachable in ordinary two-thumb use
(thumb A down, thumb B down, thumb A lifts, thumb B swipes on with id 1). Any
id >= 2 reaches no track at all. PointerIdNormalizer renumbers raw MotionEvent
pointer ids in first-seen order at the BatchInputArbiter seam; it is the
identity mapping for normal input and only repairs the broken cases.

- PointerIdNormalizer + 7 JVM unit tests
- two_pointer_track_test.cpp: tunable pointer-id / time-policy / tap-promotion
  knobs and a printed sweep table; runs in CI with the existing native suite
- native host suite 83/83; JVM suite unchanged vs baseline (only the 4
  documented Windows-only ParserTest failures)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…135)

Acts on the research in docs/TWO_THUMB_TEMPORAL_ALIGNMENT.md: the native
decoder keeps two per-pointer tracks and we have only ever used one, because
InputPointers.appendAll hardcoded pointer id 0 for the whole merged trail.

StrokeAligner is the merge seam WordComposer.setBatchInputPointers now
delegates to. Two modes:

  CONNECTOR    (default) everything on track 0 — the historical behaviour,
               reproduced byte-for-byte including the 25/60 ms base re-timing.
  DUAL_POINTER prior fragments on track 0, the in-flight stroke on track 1, so
               the decoder's own two-pointer search runs and there is no
               synthetic connector to hallucinate letters across.

The class enforces the four constraints the research established: track 0 is
always non-empty (an empty track 0 makes Suggest::initializeSearch return zero
suggestions), only ids 0 and 1 are emitted, timestamps stay globally monotonic,
and a given raw index never changes track mid-gesture.

IdealPrefixTrailBuilder is ported from the stranded #99/B7b branch and is now
reachable at runtime instead of via a side-by-side build type. It traces the
composing prefix through key centres and promotes a one-letter prefix to a
four-point micro-stroke — the "turn taps into small swipes" half of the
original hypothesis, which the research found sound. It iterates code points
and returns null when any letter is not on the keyboard, so a hole in the
synthetic path can never replace the raw trail.

Four prefs on Two-Thumb Typing → Recognition, all defaulting to today's
behaviour; the mode picker is hidden when no native gesture lib is present
because the Java fallback ignores pointer ids entirely.

Review fixes folded in:
- CONNECTOR pins the timing knobs to 25/60 so tuning them in dual mode and
  switching back cannot silently change "one joined trail".
- a current stroke that is ITSELF multi-pointer keeps its own ids instead of
  being flattened onto track 1, preserving genuine simultaneous two-thumb
  structure (new InputPointers.appendAllPreservingIds).
- IdealPrefixTrailBuilder no longer silently drops unmappable letters.

Tests: 31 new (StrokeAligner 19, IdealPrefixTrailBuilder 10, settings 2),
including a proof that DUAL_POINTER changes pointer ids only — never geometry
or timing — so the Java fallback engine cannot regress. Full JVM suite 356
tests with only the 4 documented Windows-only ParserTest failures; native host
suite 83/83; assembleOfflineliteDebugNoMinify builds.

Not yet verified on device: whether two tracks SCORE better than one merged
trail is decided by the closed weighting policy and needs the A/B in §6a.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…build

Merges the new dev (which now carries LeanBitLab v4.1.2) into the two-thumb
work. Only CHANGELOG.md conflicted; the code auto-merged, since the two-thumb
edits are additive at the points upstream touched.

Adds an 'experimental' build type with applicationIdSuffix .exp and
versionNameSuffix -exp, plus a src/experimental resource override for
english_ime_name. It installs alongside the normal debug build instead of
replacing it, and shows up as 'LeanTypeDual EXP' in the input-method picker.
Experimental input changes have to be A/B'd against a working daily driver,
and that is impossible if installing one uninstalls the other.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a88bd77f-d993-44c5-9efc-c7124f0d825e
Reported as 'IDK if it does anything'. Two stacked reasons, neither visible
from the UI:

1. The recognition settings synthesise points for the native gesture
   decoder, and .exp is a separate package with its own files/ directory,
   so it had no user-supplied libjni_latinime.so and sHaveNativeGestureLib
   was false. The mode picker hid itself, while PREF_STROKE_IDEAL_PREFIX
   was NOT gated and fed synthesised points to the Java fallback engine,
   which scores a single trail and ignores pointer ids - producing
   nonsense words.

2. Even with the library, StrokeAligner is only reached from
   WordComposer.setBatchInputPointers during multi-part composition, which
   is armed by the spacing mode (manual spacing, or a non-zero combining
   grace). At the default spacing mode the code never runs, so changing
   the mode picker legitimately does nothing.

Gates the whole recognition group on a loaded gesture library, and shows
an explanation above it when the spacing mode leaves it inert, instead of
rendering settings that silently cannot take effect.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a88bd77f-d993-44c5-9efc-c7124f0d825e
Merging dev after the release bump put the #135/#141 entries under [0.3.0],
but 0.3.0 was tagged from main without them. Moved them to [Unreleased] and
rewrote them to match what device testing found: both experimental
recognition modes are off by default and not currently recommended, because
with a user-supplied gesture library they produce incorrect words (#144).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a88bd77f-d993-44c5-9efc-c7124f0d825e
@AsafMah
AsafMah merged commit a3e37bf into dev Aug 20, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants