Skip to content
Merged
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
10 changes: 4 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,15 @@ and this project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html)

### Fixed
- **Fast double-taps on Shift enable Caps Lock again.** The prior duplicate-event workaround rejected legitimate taps less than 100 ms apart; distinct taps are now identified by their press/release boundary instead. (#146)
- **Gesture typing no longer silently returns zero suggestions** when a stroke's touch points never carry pointer id 0 — reachable in two-thumb use (thumb A down, thumb B down, thumb A lifts, thumb B swipes on). Raw MotionEvent pointer ids are now renumbered in first-seen order. (#135)
- **The two-thumb recognition settings no longer appear when they cannot work.** They synthesise touch points for the native gesture decoder; the built-in fallback engine scores a single trail and ignores which thumb drew it, so applying them there corrupted the trail and produced nonsense words. The group is now gated on a loaded gesture library, and explains itself when the spacing mode leaves it inert, instead of showing controls that structurally cannot take effect. (#141)
- **Gesture typing no longer silently returns zero suggestions** when a stroke's touch points never carry pointer id 0 — reachable in two-thumb use (thumb A down, thumb B down, thumb A lifts, thumb B swipes on). Raw MotionEvent pointer ids are now renumbered in first-seen order. (#135, #147)

### Changed
- Two experimental recognition modes exist behind settings — feeding the two thumbs as separate decoder tracks, and redrawing earlier word parts through key centres — but they are **off by default and not currently recommended**. On a device with a user-supplied gesture library they produce incorrect words: the decoder that actually runs is a closed third-party library, not the in-repo AOSP engine whose two-pointer-track behaviour the research measured. (#135, #144)
- Documented the two-thumb decoder research in `docs/TWO_THUMB_TEMPORAL_ALIGNMENT.md`, including the measurement that deliberately overlapping stroke timestamps corrupts the decoder's speed features rather than helping. (#135)
- **Removed falsified two-thumb recognition experiments from production paths.** The separate decoder-track mode, synthetic ideal-prefix trail, and their timing controls produced incorrect words with the gesture library that actually runs on devices. Multi-part composition now uses the proven pre-experiment connector path directly again. (#147)
- Reframed the two-thumb decoder research as a historical record that distinguishes in-tree preprocessing facts from claims falsified against the closed runtime recognizer. (#147)

### Reliability & testing
- Added source-level and packaged-APK gates that fail upstream merges when LeanTypeDual's identity, privacy flavors, bundled offline dictionaries, fork integrations, or four-flavor release coverage are lost. (#148)
- Added a native gesture **two-pointer track harness** (`jni/tests/replay/two_pointer_track_test.cpp`) that drives the real AOSP `ProximityInfoState` on the host, with tunable knobs and a printed sweep table. Runs in CI alongside the existing native suite. Note that it exercises the in-repo engine, which is not the decoder used when a gesture library is loaded. (#135, #144)
- The multi-part trail merge moved behind a pure, unit-tested `StrokeAligner` seam whose defaults reproduce the previous behaviour exactly. (#135)
- Kept the independently useful pointer-id normalization regression coverage, added production-wiring coverage for the no-id-0 case, and pinned the restored connector's exact coordinates, timestamps, and pointer ids. Removed the native research harness whose results were easy to mistake for runtime recognizer behavior. (#147)

## [0.3.0] - 2026-08-20

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
* Maps raw {@link android.view.MotionEvent} pointer ids onto the dense track slots the native
* gesture decoder actually reads.
*
* <p><b>Why this exists.</b> The native decoder keeps exactly {@code MAX_POINTER_COUNT_G == 2}
* per-pointer tracks ({@code jni/src/defines.h}). {@code DicTraverseSession} seeds track <i>i</i>
* with pointer id <i>i</i>, and {@code ProximityInfoStateUtils::updateTouchPoints} keeps only the
* points whose {@code pointerIds[k] == i}. Two consequences follow, both measured in
* {@code jni/tests/replay/two_pointer_track_test.cpp}:
* <p><b>Why this exists.</b> The in-tree native preprocessing keeps exactly
* {@code MAX_POINTER_COUNT_G == 2} per-pointer tracks ({@code jni/src/defines.h}).
* {@code DicTraverseSession} seeds track <i>i</i> with pointer id <i>i</i>, and
* {@code ProximityInfoStateUtils::updateTouchPoints} keeps only points whose
* {@code pointerIds[k] == i}. Two consequences follow:
*
* <ul>
* <li>If <b>no</b> point carries id 0, track 0 is unused and {@code Suggest::initializeSearch}
Expand Down
27 changes: 21 additions & 6 deletions app/src/main/java/helium314/keyboard/latin/WordComposer.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@
import helium314.keyboard.latin.common.StringUtils;
import helium314.keyboard.latin.define.DebugFlags;
import helium314.keyboard.latin.define.DecoderSpecificConstants;
import helium314.keyboard.latin.gesture.StrokeAligner;
import helium314.keyboard.latin.settings.Settings;

import java.util.ArrayList;
import java.util.Collections;
Expand Down Expand Up @@ -63,6 +61,13 @@ public final class WordComposer {
// huge time discontinuity at the prefix/swipe boundary and confuse the recognizer.
private final InputPointers mExtendBatchInputBase = new InputPointers(MAX_WORD_LENGTH);
private boolean mExtendBatchInputBaseSet;
// Inter-point interval used when synthesising timestamps for the base. Roughly the
// sampling rate of a fast hand-drawn swipe; chosen to look like natural gesture speed.
private static final int EXTEND_BASE_POINT_INTERVAL_MS = 25;
// Gap inserted between the last synthetic base point and the first real point of the
// current gesture. Pretends the user briefly paused at the prefix endpoint before
// continuing the stroke — within the recogniser's "single stroke" tolerance.
private static final int EXTEND_BASE_GAP_BEFORE_NEW_MS = 60;

// Cache these values for performance
private CharSequence mTypedWordCache;
Expand Down Expand Up @@ -280,10 +285,20 @@ public void setBatchInputPointers(final InputPointers batchPointers) {
if (mExtendBatchInputBaseSet && mExtendBatchInputBase.getPointerSize() > 0
&& batchPointers.getPointerSize() > 0) {
// Multi-part composition: feed the lib the merged trail (prior fragments +
// current gesture). StrokeAligner owns the re-timing and the pointer-id policy —
// see docs/TWO_THUMB_TEMPORAL_ALIGNMENT.md.
StrokeAligner.merge(mInputPointers, mExtendBatchInputBase, batchPointers,
Settings.getValues().mStrokeAlignParams);
// current gesture) with synthesised timestamps so the base looks like a
// natural continuation of the new gesture.
final int baseSize = mExtendBatchInputBase.getPointerSize();
final int[] baseX = mExtendBatchInputBase.getXCoordinates();
final int[] baseY = mExtendBatchInputBase.getYCoordinates();
final int firstNewTime = batchPointers.getTimes()[0];
final int baseLastTime = firstNewTime - EXTEND_BASE_GAP_BEFORE_NEW_MS;
final int baseFirstTime = baseLastTime - (baseSize - 1) * EXTEND_BASE_POINT_INTERVAL_MS;
mInputPointers.reset();
for (int i = 0; i < baseSize; i++) {
mInputPointers.addPointer(baseX[i], baseY[i], 0,
baseFirstTime + i * EXTEND_BASE_POINT_INTERVAL_MS);
}
mInputPointers.appendAll(batchPointers);
} else {
mInputPointers.set(batchPointers);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,48 +107,15 @@ public void shift(final int elementCount) {
}

/**
* Append all pointers from {@code other} to the end of this, forcing pointer id 0.
*
* <p>Historically this was the only merge path, which is why the decoder's second pointer
* track was never populated by multi-part composition. Prefer
* {@link #appendAll(InputPointers, int)} when the caller knows which track the points belong
* to — see {@link helium314.keyboard.latin.gesture.StrokeAligner}.
* Append all pointers from {@code other} to the end of this. Pointer ids are forced to
* 0 since multi-part gesture composition doesn't preserve pointer identity across
* separate strokes.
*/
public void appendAll(@NonNull final InputPointers other) {
appendAll(other, 0);
}

/**
* Append all pointers from {@code other} to the end of this, stamping them with
* {@code pointerId}.
*
* <p>The native decoder keeps one {@code ProximityInfoState} per pointer id (two of them,
* {@code MAX_POINTER_COUNT_G}) and each state ingests <em>only</em> the points carrying its own
* id. So this argument decides which decoder track the appended stroke lands in. Ids outside
* {@code [0, 1]} reach no track at all.
*/
public void appendAll(@NonNull final InputPointers other, final int pointerId) {
append(pointerId, other.mTimes, other.mXCoordinates, other.mYCoordinates, 0,
append(0, other.mTimes, other.mXCoordinates, other.mYCoordinates, 0,
other.getPointerSize());
}

/**
* Append all pointers from {@code other}, keeping each point's own pointer id.
*
* <p>Used when {@code other} is already a genuine multi-pointer stroke whose track assignment
* must survive the merge.
*/
public void appendAllPreservingIds(@NonNull final InputPointers other) {
final int length = other.getPointerSize();
if (length == 0) {
return;
}
mXCoordinates.append(other.mXCoordinates, 0, length);
mYCoordinates.append(other.mYCoordinates, 0, length);
mPointerIds.append(other.mPointerIds, 0, length);
mTimes.append(other.mTimes, 0, length);
}

public void reset() {
final int defaultCapacity = mDefaultCapacity;
mXCoordinates.reset(defaultCapacity);
Expand Down

This file was deleted.

Loading
Loading