Skip to content

Merge upstream LeanType v3.8.6 (handwriting, llama.cpp/GGUF, touchpad gestures)#109

Merged
AsafMah merged 124 commits into
devfrom
merge/upstream-v3.8.6
Jun 22, 2026
Merged

Merge upstream LeanType v3.8.6 (handwriting, llama.cpp/GGUF, touchpad gestures)#109
AsafMah merged 124 commits into
devfrom
merge/upstream-v3.8.6

Conversation

@AsafMah

@AsafMah AsafMah commented Jun 20, 2026

Copy link
Copy Markdown
Owner

What this is

Merges LeanBitLab/LeanType v3.8.6 into the fork and bumps the fork release to LeanTypeDual 3.10.0 (versionCode 4000). New upstream capabilities brought in:

  • Handwriting input (Standard builds) via a downloadable ML Kit plugin + dedicated bottom-row layout/toolbar key
  • Offline AI: ONNX → llama.cpp / GGUF with configurable sampling (temp/top-p/top-k/min-p)
  • Rich touchpad gestures (1-/2-finger navigation, selection, clipboard, undo/redo, hold-to-backspace)
  • Auto-read OTP from SMS suggestions (runtime, opt-in RECEIVE_SMS)
  • Regex shortcuts in Text Expander

Note: upstream/main is the v3.8.6 release plus one docs-only commit (3fc71c9b "docs: document handwriting and gguf features").

Conflict resolution — 29 conflicts across 20 files

Fork identity preserved throughout: appId com.asafmah.leantypedual, version 3.10.0/4000, LeanTypeDual branding, INTERNET standard-only.

Area Decision
build.gradle.kts Keep fork appId; bump fork version to 3.10.0 / 4000; removed a duplicate standardOptimised flavor block; adopt upstream arm64-only ABI, offline minSdk 26, llama.cpp + ML Kit deps
ToolbarUtils / LayoutType / Defaults / strings.xml Union fork keys (shortcut rows, autospace/auto-cap, join/undo-word) with upstream HANDWRITING + auto-read-OTP
ProofreadService (all flavors) + AIIntegrationScreen Unify on the prefs property (per the prior v3.8.5 precedent); offline keeps its llama.cpp impl
DictionaryFacilitatorImpl Take upstream supersets (adds mContext + emoji-dict cache invalidation); keep the fork's blocklist anti-resurrection logic
InputLogic Keep the fork's combining/two-thumb hooks in place, then run upstream's TextExpander immediate-expansion
README / FEATURES / CHANGELOG / fastlane Keep fork branding; document GGUF, handwriting, regex; add release note for 3.10.0

Upstream regression fixed here

This PR also fixes upstream bug LeanBitLab#186 (sticky Shift / single-tap caps-lock in v3.8.6 Standard).

Root cause: the upstream handwriting integration called mHandwritingView.stopHandwriting() on every main-keyboard frame switch, even when handwriting was hidden. stopHandwriting() closes handwriting's bottom-row MainKeyboardView, which calls cancelAllOngoingEvents() and globally cancels active PointerTrackers. Pressing Shift switches keyboard state while the Shift pointer is still down, so the hidden handwriting cleanup swallowed Shift's release; the next letter saw Shift as PRESSING/CHORDING, leaving the keyboard uppercase.

Fix: only stop handwriting when the handwriting view is actually shown; otherwise just keep it hidden. This same one-file fix was also opened upstream as LeanBitLab#194.

⚠️ Needs maintainer eyes

  • Touchpad double-tap behavior change. Adopted upstream's double-tap = select word (KeyboardActionListenerImpl + FEATURES), replacing the fork's previous double-tap = delete selection (documented in CHANGELOG 3.8.4). Chosen for coherence with the rest of upstream's gesture suite, but it's user-visible.
  • arm64-only ABI — upstream dropped armeabi-v7a; adopted here.
  • Offline minSdk raised to 26 (llama.cpp); offlinelite stays minSdk 21.
  • AGENTS.md still says ABIs armeabi-v7a, arm64-v8a / minSdk 21 generally — left as-is but now slightly stale for offline.
  • A few upstream incoming files carry trailing whitespace (git diff --check warnings); left as-upstream.

Regressions found by verification & fixed in this PR

  1. DictionaryGroup.add/removeFromBlacklist skipped rebuildCompiledPatterns() when no blacklist file exists, so the merged regex-based isBlacklisted never saw new entries → now rebuilds in-memory patterns regardless of file. (fixes 3 DictionaryGroupTest)
  2. TextCorrectionScreen had a duplicate PREF_COMPRESS_SCREENSHOTS SettingSettingsContainer duplicate-key throw. Deduped. (fixes 12 SettingsContainerTest)
  3. Upstream sticky Shift bug from handwriting cleanup cancelling active pointer trackers. Fixed with the isShown() guard and confirmed on-device.

Verification

  • All four flavors compile (CI gate compileOfflineRunTestsKotlin passes).
  • Offline unit suite: 12 failures, all pre-existing known failures (KeyboardParser ×5, XLink ×2, InputLogic Hangul/autocorrect-revert/autospace-indicator ×3, StringUtils-emoji ×2 per AGENTS.md). The merge-induced regressions above were caught here and fixed; net new failures: 0.
  • INTERNET confirmed only in src/standard/AndroidManifest.xml; offline/offlinelite inherit the network-free main manifest.
  • ✅ Installed standardDebug 3.10.0 on a Samsung SM-S936B over wireless adb.
  • ✅ Sticky Shift repro confirmed fixed on-device after the handwriting cleanup guard.

LeanBitLab added 30 commits June 1, 2026 12:20
Parse Gboard format header dynamically to fix missing words and swapped values. Optimize using bulkInsert to reduce database insertion IPC overhead.
Set versionCode to 3840 and versionName to 3.8.4 in build.gradle.kts. Create Fastlane changelog metadata at 3840.txt.
Add responsive search filtering for text expansion shortcuts. Add quick placeholder selection row with rich cursor-based insert support to Add/Edit Dialog.
Redesign Quick Feature Guide card with styled step badges. Redesign custom shortcuts list items with premium keyword badges and chevrons. Redesign empty state with card illustration layout.
Remove redundant template placeholder explanation while retaining the list of supported placeholder tags.
Add support and UI representations for %month%, %month_short%, %year%, and %week% placeholders.
Add and integrate support for %battery%, %device%, and %android% placeholders.
Add and integrate support for %language% placeholder, which expands to the current active keyboard display language.
- Add BitmapUtils.decodeSampledBitmap() with two-pass decode and inSampleSize
- Use RGB_565 config for non-PNG images to halve memory usage
- Use BitmapFactory.decodeStream (with InputStream) instead of decodeFile
- Cap background bitmap at 2048px max dimension
- Recycle temp bitmap after validation in setBackgroundImage

Fixes: Settings.java:527, BackgroundImagePreference.kt:122
- Colors.kt: use 'let' smart cast and 'error' instead of NPE on missing keyBackground
- FloatingKeyboardManager.kt: safe-call on overlayRoot, early return on null
- SuggestionStripView.kt: early return true on missing drawable

Prevents IME process crashes from null drawables/bitmaps in keyboard rendering paths.
AndroidSpellCheckerService.onDestroy() now unregisters the
OnSharedPreferenceChangeListener. Without this, the SharedPreferences
implementation kept a strong reference to the service, leaking it
through every spell-check session the system bound/unbound.
BackupRestorePreference.kt was calling Looper.prepare() from the
ScheduledThreadPool executor, leaking a Looper per restore and posting
UI work onto an unreliable thread. Use Handler(Looper.getMainLooper())
to dispatch the FeedbackManager.message call to the UI thread.
The previous implementation had a non-atomic read-then-write of
mLastScoreLimitUpdateTime and mCachedScoreLimitForAutocorrect across
threads (suggestion lookup can happen on background threads via
SuggestionSpan / TextClassifier). Two threads could both miss the
interval check and recompute, with the second write overwriting the
first. Wrap the cache update in synchronized(this) to make the check
and update atomic.
Three blacklist operations in DictionaryGroup used
`<outer>.apply { scope.launch { synchronized(this) { ... } } }`,
which re-bound `this` to the HashSet / CoroutineScope inside the
synchronized block. Two threads could enter the critical section
concurrently because they were locking on different objects.

Add an explicit `blacklistLock: Any` and synchronize on it.
- SearchScreen: key groups by titleRes, items by toString()
- ListPickerDialog, MultiListPickerDialog: key items by toString()
- LayoutPickerDialog: key by layout name
- ToolbarKeysCustomizer: key by enum name
- ColorThemePickerDialog: key by color name

Without keys, LazyColumn uses positional keys, causing every visible
item to be recomposed (and its remember slots discarded) on every
search keystroke or list mutation.
- SearchScreen: cache filteredItems(searchText.text) so it doesn't
  re-run the search filter on every parent recomposition (only when
  the search text actually changes)
- MainSettingsScreen: cache SubtypeSettings.getEnabledSubtypes() and
  its joinToString() output so the description string is not rebuilt
  on every recomposition

Both lists are otherwise recomputed on every pref change, every
parent state change, and every scroll-induced recomposition.
…alog

Wrap the Paint in remember { } and assign to the controller inside a
LaunchedEffect so the Paint is created once and not allocated on every
recomposition. Also avoids re-assigning the controller's wheelPaint
on every recomposition.
AboutScreen's 'Save log' was reading the entire logcat buffer into a
single String via readText(), then writing it out. For a long-running
device this can be several MB and compete with the IME process for
memory, risking OOM on low-RAM devices.

Use useLines { } to iterate line by line and write each one
directly to the output stream. The internal log is now also
streamed with a for loop and explicit toString() instead of a
joinToString() that builds the entire list as a single String.
The private KeyAndState class had var fields that were mutated in the
Switch.onCheckedChange callback, defeating Compose stability and
forcing LazyColumn items to be rebuilt on every recomposition.

- Make KeyAndState an immutable data class annotated with @immutable
- Hold the checked state in rememberSaveable(item.name) so the value
  survives recomposition but is per-item
- Remove the in-place mutation of item.state in the Switch callback
- rememberSaveable the items list so it's not re-parsed on every
  recomposition when the dialog is open
The previous top-level 'providerState' MutableStateFlow lived for the
process lifetime and was mutated during composition. The state can
be derived from the service on every composition (the service reads
from SharedPreferences, which is cheap).

- Replace top-level MutableStateFlow with a simple val read
- Remove the no-op updateProviderState() function
- Remove its call site in AdvancedScreen.kt

The AIIntegrationScreen will pick up provider changes on the next
composition (e.g. when the user navigates to it after changing the
provider on the AdvancedScreen).
The top-level 'private var errorJob: Job?' was shared between any
two simultaneous instances of LayoutEditDialog, so opening a second
dialog would cancel the first dialog's pending error feedback job.
On configuration change the coroutine scope could be cancelled while
the top-level job reference was leaked.

Move the job into a per-composable remember { mutableStateOf<Job?>(null) }
and cancel/assign through errorJob.value.
setToolbarButtonsActivatedStateOnPrefChange used GlobalScope.launch to
defer a UI update by 10 ms, waiting for SettingsValues to reload after
a SharedPreferences change. GlobalScope is uncancellable and its
default exception handler converts failures into silent crashes.

Replace it with a process-wide scope that uses SupervisorJob (so one
failure cannot tear down sibling preference updates) and a logging
CoroutineExceptionHandler. The function still hops to Dispatchers.Main
before touching the view tree.
The CoroutineScope backing the navigateTo() helper used a plain Job,
so a single child failure would cancel the scope permanently. Add
SupervisorJob so unrelated navigation hops keep working.
createBlendModeColorFilterCompat returns a nullable ColorFilter, but
the helper is only ever called with the supported BlendModeCompat
modes (MODULATE, SRC_IN). Replace the !! with a Kotlin error() that
throws IllegalStateException with a useful message if a new
unsupported mode is ever introduced.
ClipboardHistoryManager is a singleton scoped to the IME service, but
it was creating a fresh Handler(Looper.getMainLooper()) on every
postDelayed() and on every ContentObserver registration. The main
Looper is process-wide and lives for the lifetime of the app, so a
single cached Handler is enough.

Replace the two ad-hoc Handler allocations in registerMediaStoreObserver
and in the post-paste clip restoration path with a single 'mainHandler'
field on the manager.
The CoroutineScope backing updateShortcutIme, onSubtypeChanged and
related fire-and-forget coroutines was using a plain Job. A single
exception in any of those coroutines would cancel the scope and stop
all subsequent subtype lookups for the lifetime of the IME process.

Add SupervisorJob() so a single failure cannot tear down the rest
of the lookups.
…flag

LatinIME.onCreate was using the deprecated registerReceiver(receiver,
filter) overload for the ringer mode, package add/remove and user
unlocked broadcasts. On Android 13+ this throws SecurityException
unless the receiver is registered with an explicit exported flag.

Switch the three call sites to ContextCompat.registerReceiver with
RECEIVER_NOT_EXPORTED, matching the existing style used for
DICTIONARY_DUMP_INTENT_ACTION. The exported flag stays set for the
NEW_DICTIONARY_INTENT_ACTION receiver, as documented in the existing
comment, because the sender app may not be this one.
Align provider preference source and observe changes dynamically to update UI fields immediately. Wrap preferences in key() to prevent Compose state reuse.
Add standardOptimised flavor to allow non-reproducible optimizations like R8 fullMode and baseline profiles. Turn off R8 fullMode globally to restore reproducibility for standard flavor on F-Droid. Clean APK metadata and restore global V2/V3 signing.
Add manual wildcard-based precompilation rules in baseline-prof.txt for standardOptimised to optimize startup, typing reaction, and suggestions. Fix dynamic property injection in settings.gradle.
LeanBitLab and others added 28 commits June 16, 2026 00:25
Allows HTTP local endpoints and self-signed HTTPS connections only when explicitly enabled by the user.
fix(layout): change default popup key on letter ا in Persian language
Move model readiness checks to background thread to prevent main thread blocking exceptions. Add ML Kit client dependencies to standard build flavor for native library alignment. Auto-upgrade toolbar preferences to discover new keys without factory resets.
Prevent token loss and hallucination in local models due to formatting and JNI bugs.
… gestures)

Merges LeanBitLab/LeanType v3.8.6 into dev (merge base v3.8.3, 121 upstream
commits, 104 files). Brings handwriting input (downloadable ML Kit plugin), the
llama.cpp/GGUF offline AI backend (replacing ONNX), the richer touchpad-gesture
suite, SMS one-time-code suggestions, and regex Text Expander shortcuts.

Note: upstream/main is the v3.8.6 release plus one docs-only commit
(3fc71c9 "docs: document handwriting and gguf features").

Conflict resolutions (29 across 20 files) preserve fork identity and the fork's
two-thumb/combining state machine while adopting upstream's features:

- build.gradle.kts: keep fork version 3.9.1/3910 and appId
  com.asafmah.leantypedual; remove a duplicate standardOptimised flavor block;
  adopt arm64-only ABI, offline minSdk 26, and the llama.cpp/ML Kit deps.
- ToolbarUtils/LayoutType/Defaults/strings: union fork keys (shortcut rows,
  autospace/auto-cap, join/undo-word) with upstream HANDWRITING + auto-read-OTP.
- ProofreadService (all flavors) + AIIntegrationScreen: unify on the `prefs`
  property (offline keeps its llama.cpp implementation).
- DictionaryFacilitatorImpl: take upstream supersets (adds mContext + emoji-dict
  invalidation); keep the fork's blocklist anti-resurrection behaviour.
- InputLogic: keep the fork's combining/two-thumb hooks, then run upstream's
  TextExpander immediate-expansion.
- KeyboardActionListenerImpl + FEATURES: adopt upstream touchpad double-tap =
  select word (was the fork's delete-on-selection).
- README/FEATURES/CHANGELOG: keep fork branding; document GGUF, handwriting,
  regex; add an Upstream marker.

Two merge-induced regressions found by the unit suite and fixed:
- DictionaryGroup.add/removeFromBlacklist now rebuild the compiled blacklist
  patterns even when no blacklist file exists (the no-context test path), so
  in-memory blocking works.
- TextCorrectionScreen: removed a duplicate PREF_COMPRESS_SCREENSHOTS Setting
  that broke SettingsContainer with a duplicate key.

Verification: all four flavors compile (offline/standard/standardOptimised/
offlinelite); the offline unit suite has only the 12 pre-existing known failures
(KeyboardParser/XLink/StringUtils-emoji/InputLogic-Hangul), no net new failures.
INTERNET permission remains standard-only.
@AsafMah AsafMah merged commit 062fab2 into dev Jun 22, 2026
1 check passed
@AsafMah AsafMah deleted the merge/upstream-v3.8.6 branch June 22, 2026 06:45
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.

6 participants