Conversation
…) (#64) A learned word in no real dictionary (main/contacts/apps/personal) could out-rank a real dictionary word with better geometry after a single misfire. Now, as the LAST ranking step in getSuggestionResults (after session boost, so it can't be undone), an uncurated USER_HISTORY candidate that still outscores the best real-dictionary candidate is CAPPED just below it — until its user-history frequency crosses a confirmation threshold (~3 repetitions). This guarantees a one-off junk word can't hijack a real word regardless of native score magnitude, while a deliberately repeated new word still learns and keeps full score. When no real candidate exists, new words are left untouched (still offerable). - Capping (not score-scaling) avoids any dependence on native score calibration or sign; applied post-session-boost so the boost can't re-promote junk. - Decision is a pure companion helper (shouldPenalizeUnconfirmedWord), unit-tested; uncurated check reuses isInNonHistoryDictionary; threshold is a tunable constant. - Gated by new pref PREF_GRADUATED_TRUST (default on). The actual ranking effect needs the native scorer, so the threshold and the 'real candidate' set want on-device playtesting.
…row (#36) (#68) Follow-up to #58: the left-pin used natural icon width, so N icons tiled from the left and the far-right keys couldn't reach them. Now each icon is sized to rowWidth/iconCount (usable normal-key row span), so the N icons tile the whole row and a swipe maps proportionally — swiping from the i-th 1/N of the row selects the i-th icon. Threads a fixedKeyWidth into PopupKeysKeyboard.Builder (0 = default for normal/emoji popups); dividers are dropped in that mode so the icons tile exactly.
… (#66) * feat(toolbar): UNDO_WORD key — revert last commit to its alternatives (#35) Adds an Undo-word toolbar key that reverts the last committed (gesture) word back to its alternatives in the suggestion strip, modeled on the JOIN_NEXT toolbar key. - New KeyCode.UNDO_WORD (-251, next free toolbar code); ToolbarKey.UNDO_WORD + getCodeForToolbarKey mapping; ic_undo / ic_undo_rounded icons; talkback string. - InputLogic.handleUndoWord: guarded by mLastComposedWord.canRevertCommit(), reuses the backspace revert sequence (revertCommit + restartSuggestionsOnWordTouchedByCursor) so the committed word's SuggestionSpans repopulate the strip with its alternatives. Scope: toolbar key only. The swipe-up/down assignable target (also in #35) is deferred until the shortcut-assignment plumbing (shared with #37) exists. Needs on-device verification that gesture-committed words restore their alternatives (vs dictionary-fresh suggestions) — see PR notes. * fix(toolbar): rework UNDO_WORD to non-destructive recorrection (#35) On-device testing found the revertCommit reuse was wrong: it deletes committedWord.length()+separator before the CURRENT cursor, but after a gesture the trailing space is a pending PHANTOM space (not in the text), so it over-deleted into the previous word; on stale/empty state it deleted the wrong span -> garbage. Drop revertCommit entirely. handleUndoWord now just calls restartSuggestionsOnWordTouchedByCursor: after a gesture the cursor sits right after the committed word (phantom space not in text), so that word is re-opened for re-selection with its alternatives shown — exactly like tapping a word to re-edit it. Non-destructive (no delete -> no garbage, no over-delete), and its existing guards no-op safely when the cursor isn't on a word / has selection / no-spaces language / suggestions disabled. Also: distinct icon — UNDO_WORD now uses ic_edit (UNDO already uses ic_undo). * fix(toolbar): UNDO_WORD shows the word's real alternatives, not a fresh lookup (#35) restartSuggestionsOnWordTouchedByCursor reads suggestion spans from the EDITOR text, but gesture commits don't leave them there, so it fell into a fresh typing-style lookup -> nonsense suggestions. Read the alternatives from mLastComposedWord.mCommittedWord's in-memory SuggestionSpan instead (the actual gesture/autocorrect candidates the original revertCommit used). Still non-destructive: verify the committed word is exactly before the cursor, set the composing region over it (no delete), and show those alternatives; no-op if the word moved/changed or has no real alternatives. * feat(toolbar): UNDO_WORD recovers the last word's alternatives after space (#35) The real value is AFTER you've pressed space (the strip already shows alternatives right after a word). mLastComposedWord.deactivate() runs on the space keypress, but the object + its in-memory SuggestionSpan alternatives survive. So handleUndoWord now finds the committed word before the cursor allowing a trailing whitespace separator, moves the cursor back into it (setSelection), re-composes it, and shows its original alternatives. Non-destructive (no deletion). No-ops if the last word before the cursor isn't the committed one or it has no alternatives. * fix(toolbar): UNDO_WORD reads alternatives from the editor via resume (#35) Root cause of the no-ops: commitChosenWord stores the PLAIN chosenWord as mLastComposedWord.mCommittedWord (the spans-carrying string only goes to the editor at commitText), so reading SuggestionSpans off mCommittedWord always found none -> size<=1 -> no-op. The candidates actually live in the editor text's SuggestionSpans. So: move the cursor to the end of the last word before the cursor (skip a trailing space), then postResumeSuggestions(delay) -> MSG_RESUME_SUGGESTIONS -> restartSuggestionsOnWordTouchedByCursor, which reads the word's editor spans after the cursor settles. Non-destructive, race-free (uses the standard resume mechanism).
Debug capture mode: when DebugSettings.PREF_RECORD_INPUT_TRACES is on, each completed gesture session is dumped to filesDir/input_traces/trace-<ms>.json (InputPointers x/y/t/id + committed word + keyboard geometry/locale), for the JUnit replay harness (#21). - Hooked in onUpdateTailBatchInputCompleted (where committed word + InputPointers + keyboard are all synchronously available on the UI thread), not onEndBatchInput. - TraceRecorder copies the pointer arrays synchronously (InputPointers isn't thread-safe) then writes off a single-threaded executor; app context, no IME leak. - Default off -> a single boolean check in normal use. Format documented in KDoc.
Adds 10 backspace regression tests to InputLogicTest (the #31 safety net): default per-char delete (composing/committed/recompose), the PR-#11 'precool' word-mash corruption guard, combining whole-word delete, cursor-in-middle recompose, legacy fragment-pop, and two monotonicity invariants. Assertions are observable (editor + composing text) only, so they survive the #31 input-unit-stack refactor that removes mGestureFragmentBoundaries. The multi-stroke gesture-extension case is @ignore'd (not JVM-simulable; documented for on-device verification). 9 active tests green; 1 @ignore; no new failures vs the InputLogicTest baseline (3 pre-existing env failures unchanged).
#76) The 'Update README Badges' workflow ran daily and git-pushed straight to main, which now fails on every run (GH006: protected branch — changes must be a PR) since branch protection was enabled. It also fetched stats from the wrong repo (LeanBitLab/HeliboardL) and committed static SVGs instead of using live badges. Remove the workflow + the generated docs/badges/*.svg, and point the README badges at live shields.io URLs for AsafMah/LeanType (auto-update, no CI). Note: the Download buttons still reference LeanBitLab/HeliboardL + com.leanbitlab.leantype — left as-is (distribution/branding decision).
…tack (#31) (#77) * refactor(inputlogic): consolidate backspace state into BackspaceUnitStack (#31) The fragment- and whole-word-backspace length bookkeeping lived in three fields scattered across InputLogic (mGestureFragmentBoundaries + mLastGestureCommittedLength + mLastGestureCommittedFragmentLengths), read and mutated by several interleaved branches — the most corruption-prone area (PR #11 class). Extract them into one BackspaceUnitStack: the active composing word's fragment boundaries and the last committed gesture word's total + per-fragment lengths, with all the boundary/pop math as named methods. InputLogic keeps the policy (when to record/pop) and editor side effects; the stack owns only the length bookkeeping. Pure, behaviour-preserving extraction — same decisions, same outputs. Adds BackspaceUnitStackTest: 17 direct unit tests for the pop/commit math that previously had only indirect coverage via InputLogic. Verification: :app:testOfflineDebugUnitTest --tests InputLogicTest --tests BackspaceUnitStackTest -> 120 completed, 3 failed (pre-existing baseline: autospace-indicator / Hangul / autocorrect-revert), 1 skipped. 0 new failures; full #21 backspace corpus green; all 17 stack tests green. Increment 1 of #31 (state consolidation). The committed-gesture pop paths (gesture-only, not JVM-reachable) are migrated structurally but need on-device confirmation; the single-plan path collapse is the follow-up. * test(inputlogic): lock empty-fragment commit invariant for BackspaceUnitStack A gesture word committed with no recorded fragments must still set committedLength (arms the first-backspace whole-word delete) while keeping the fragment list empty — the empty-list commit case flagged in review as the easiest place to silently drop the length.
) (#81) The native engine (app/src/main/jni) does the real gesture/dictionary/geometry work, but its gtest suite (tests/) only had an AOSP-platform-build runner (run-tests.sh / HostUnitTests.mk via mmm/lunch/BUILD_HOST_NATIVE_TEST) that cannot run in gradle/NDK CI. So the engine had ZERO automated coverage — only on-device manual testing caught a recognizer/dictionary regression. Add a standalone host build: - app/src/main/jni/CMakeLists.txt — compiles the engine core + the existing gtest suite + GoogleTest (FetchContent) into a host executable. Excludes the JNI bridge (needs jni.h at link); pulls JDK jni.h headers via find_package(JNI) (or -DLATINIME_JNI_INCLUDE for local builds). No AOSP tree needed. - host_test_compat.h — force-included for the host build only, supplying the few standard headers (climits/cstdint/...) modern g++ wants but the AOSP clang pulled transitively. Shared sources untouched (they build fine under the NDK). - .github/workflows/native-tests.yml — ubuntu + setup-java + cmake + ctest, run on changes to app/src/main/jni/**. Verified locally (WSL g++13): 66 tests build and run, 65 pass. The 1 failure (FormatUtilsTest.TestDetectFormatVersion) is a host-toolchain anomaly in code that works on-device, quarantined from the gate and tracked in #80 — the harness catching it on first run is the point: it reaches code the JVM tests cannot. Deliverable 1 of #78 (native engine tests in CI). Deliverable 2 (gesture-replay test over recorded TraceRecorder fixtures) follows — it needs on-device-recorded trace fixtures.
* docs: add CHANGELOG, refresh README for the fork, bump to 3.9.0 - CHANGELOG.md: Keep-a-Changelog history of LeanTypeDual's own releases (3.8.1-3.8.6 from fastlane + the 3.9.0 work), with a coarse 'Upstream' baseline marker instead of per-line provenance. - README: lead with the fork's identity + purpose (two-thumb / dual-thumb typing — the namesake feature, previously undocumented), accurate HeliBoard -> LeanType -> LeanTypeDual lineage, and the current feature set incl. graduated trust, blocklist, undo-word, per-dictionary control. Credits now attribute LeanTypeDual (AsafMah) on top of LeanType (LeanBitLab); sponsor kept as-is. - Bump versionName 3.8.6 -> 3.9.0 (versionCode 3860 -> 3900) so the next release is a clean 3.9.0; add fastlane 3900.txt user-facing notes. - AGENTS.md: changelog/release convention (coarse provenance, versionCode scheme, fastlane note on release). Note: README Download buttons still point at LeanBitLab/HeliboardL and the banner SVG still reads 'LeanType' — left for a separate distribution/branding decision. * docs(branding): point downloads at AsafMah/LeanType, banner -> LeanTypeDual - README Download buttons now point at AsafMah/LeanType (GitHub releases + Obtainium); drop the F-Droid button (the fork ships as com.asafmah.leantypedual, not the upstream com.leanbitlab.leantype, and is not published on F-Droid). - Banner SVGs (light + dark) now read 'LeanTypeDual' (Lean / Type-purple / Dual); alt text updated. Verified rendering (no clipping). - Lineage/credit links to github.com/LeanBitLab kept (correct attribution).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release 3.9.0 (versionCode 3900).
Ships everything on
devsince 3.8.6 — see CHANGELOG.md / fastlane 3900.txt. Highlights:Merging this, then tag
v3.9.0triggers the signed-APK release CD.