Merge upstream LeanType v3.8.6 (handwriting, llama.cpp/GGUF, touchpad gestures)#109
Merged
Conversation
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.
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
added a commit
that referenced
this pull request
Jul 4, 2026
* feat(spacing): per-keystroke spacing-policy signals (#24 foundation) (#92)
Phase 2 of the spacing-policy epic (#14), per docs/SPACING_POLICY.md. Computes
the two free signals from the existing suggestion results every keystroke (zero
extra native calls), in a pure static helper for testability:
- complete = typed word is a real dictionary word (mTypedWordValid AND
source != user_typed)
- prefixRichScore = fraction of candidates that are KIND_COMPLETION, [0..1]
computeSpacingSignals(SuggestedWords) -> SpacingSignals; wired into
setSuggestedWords to store mSpacingComplete / mSpacingPrefixRichScore. No
behavior change yet — the signal-driven graceMs + two-gate Assisted tier (and
the A11 insight readout) consume these next.
SpacingSignalsTest: 6 cases (complete for real-dict / not for user-typed /
not for invalid; prefix-rich fraction; empty). All green.
* test(native): gesture replay harness + fixture loader (#78 deliverable 2) (#96)
Add a native gesture-replay harness that can consume TraceRecorder JSON
fixtures and run inside the existing host CMake / ctest workflow.
New files:
tests/replay/trace_fixture.h
Header-only C++17 fixture loader (parseFixture / loadFixture).
Parses the TraceRecorder schema (version 1) into plain structs with
xCoordinates() / yCoordinates() / times() / pointerIds() accessors
ready to forward to Suggest::getSuggestions once blockers are lifted.
tests/replay/gesture_replay_test.cpp
TraceFixtureParserTest (9 enabled tests) — cover JSON parsing,
accessor arrays, monotonic timestamps, coordinate bounds, escape
handling, and empty-pointer edge cases.
DISABLED_GestureReplayTest.ReplayHelloQwerty — compile-checked
scaffold documenting the exact API seam and two concrete blockers.
tests/replay/fixtures/hello_qwerty.json
Seed fixture: 14-sample "hello" trace on a 1080×310 QWERTY keyboard
(en-US). Consumed by TraceFixtureParserTest.LoadsHelloQwertyFromFile.
CMakeLists.txt:
Add target_compile_definitions(FIXTURE_DIR=...) so the file-path test
resolves the fixture directory at compile time without network access.
Blockers documented in gesture_replay_test.cpp (DISABLED_ comment block):
1. ProximityInfo requires a live JNIEnv* — calls env->GetArrayLength()
unconditionally in its constructor; no non-JNI overload exists.
Fix: add a raw-pointer constructor or a fake-JNIEnv shim.
2. Dictionary requires a compiled binary .dict asset not in the tree.
Fix: bundle a small en_US dict via CMake FetchContent, or generate
one programmatically using the existing v4 writer classes.
ctest result: 76/76 pass, 1 disabled (GestureReplayTest.ReplayHelloQwerty)
* docs(spacing): practical playtest plan for tap/swipe timing (#103)
Split from the parked A11/tuning stack. Documents the actual axes discovered
on-device: tap-only vs swipe vs tap-then-swipe, and auto-finish vs auto-space
vs deferred-space timing. Includes concrete presets and playtest scenarios for
shortcut safety, swiped complete words, tap-then-swipe extension, correction
replacement, and punctuation/deferred spacing.
* test(native): make ProximityInfo host-constructible for replay harness (#78) (#104)
Follow-up proof spike after #96. Add a non-JNI ProximityInfo constructor that
accepts the same keyboard geometry as raw int/float arrays, so native host tests
can build a keyboard geometry object without a live JVM/JNIEnv.
Adds GestureReplayHostSeamTest.BuildsProximityInfoWithoutJNI: constructs a
minimal QWERTY ProximityInfo and verifies key lookup for the 'hello' trace.
Attempted to enable the full replay assertion with an in-memory dict; ASAN showed
the next blocker is more fundamental than a dict asset: the open-source tree has
no GestureSuggestPolicy implementation, only GestureSuggestPolicyFactory, whose
factory method is null in host builds. Dictionary::getSuggestions(...,
IS_GESTURE) therefore crashes at TRAVERSAL->getMaxSpatialDistance(). The disabled
replay scaffold now documents that real blocker.
Verification (WSL / ubuntu-like): native host ctest excluding the known
FormatUtils quarantine -> 77/77 enabled tests pass, 1 disabled replay assertion.
* Merge upstream LeanType v3.8.6 (handwriting, llama.cpp/GGUF, touchpad gestures) (#109)
* fix(settings): fix and optimize gboard import
Parse Gboard format header dynamically to fix missing words and swapped values. Optimize using bulkInsert to reduce database insertion IPC overhead.
* chore: bump version to 3.8.4
Set versionCode to 3840 and versionName to 3.8.4 in build.gradle.kts. Create Fastlane changelog metadata at 3840.txt.
* feat(settings): improve text expander ui/ux
Add responsive search filtering for text expansion shortcuts. Add quick placeholder selection row with rich cursor-based insert support to Add/Edit Dialog.
* feat(settings): polish text expander guide and list
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.
* feat(settings): clean up text expander guide
Remove redundant template placeholder explanation while retaining the list of supported placeholder tags.
* feat(settings): add more expander placeholders
Add support and UI representations for %month%, %month_short%, %year%, and %week% placeholders.
* feat(settings): add system template placeholders
Add and integrate support for %battery%, %device%, and %android% placeholders.
* feat(settings): add language placeholder
Add and integrate support for %language% placeholder, which expands to the current active keyboard display language.
* fix(perf): prevent OOM on background image decode
- 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
* fix(stability): replace force-unwrap !! in hot paths
- 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.
* fix(stability): unregister SharedPreferences listener in spell-checker
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.
* fix(stability): don't call Looper.prepare() on background thread
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.
* fix(stability): make score-limit cache update atomic in Suggest
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.
* fix(stability): use named lock for dictionary blacklist
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.
* perf(perf): add key= to Lazy* list items for stable identity
- 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.
* perf(perf): remember() expensive computations in Composables
- 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.
* perf(perf): avoid Paint allocation per recomposition in ColorPickerDialog
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.
* perf(perf): stream logcat to file instead of buffering in memory
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.
* perf(perf): make ReorderSwitchPreference data class stable
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
* fix(perf): remove top-level MutableStateFlow in AIIntegrationScreen
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).
* fix(perf): scope errorJob to the LayoutEditDialog composable
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.
* fix(perf): replace GlobalScope in toolbar preference listener
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.
* fix(perf): make SettingsNavHost navigateTo scope supervised
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.
* fix(stability): replace !! in colorFilter() helper
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.
* perf(perf): cache main-thread Handler in ClipboardHistoryManager
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.
* fix(stability): use SupervisorJob in RichInputMethodManager scope
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.
* fix(stability): use ContextCompat.registerReceiver with NOT_EXPORTED 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.
* fix(settings): update ai provider fields dynamically
Align provider preference source and observe changes dynamically to update UI fields immediately. Wrap preferences in key() to prevent Compose state reuse.
* feat: add standardOptimised flavor and disable r8
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.
* perf: add baseline profile for standardOptimised
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.
* feat: remove standardOptimised package suffix
Remove applicationIdSuffix from standardOptimised product flavor to share standard package name (com.leanbitlab.leantype).
* fix: dismiss emoji dialog and update wizard status
Close ConfirmationDialog on successful emoji dictionary download/load. Invoke onSuccess callback in WelcomeWizard to trigger recomposition and show the checkmark immediately.
* Update ar.txt
* Update build-debug-apk.yml
* arabic-popup-and-harakat-tweak
* arabic-popup-and-harakat-tweak V2
* ci: add badge update workflow
* chore: update README badges [skip ci]
* fix: strip leading v from version tag
* chore: update README badges [skip ci]
* fix(badges): adjust width and add viewBox attributes to prevent clipping
* chore: update README badges [skip ci]
* chore(badges): rename download badge label to version
* chore: update README badges [skip ci]
* fix: prevent duplicate screenshots in clipboard
* feat: add toggle for screenshot compression
* feat: improve text expander, gestures, and emoji scale fit
* chore: update README badges [skip ci]
* fix: persist toolbar customizer key toggles
* build: bump version to v3.8.5
* feat: toggle dictionaries individually
* chore: add changelog for v3.8.5
* fix: split emoji search keyboard layout
* chore: update changelog for emoji search fix
* added auto detect feature
* changed registration flag
* chore: update README badges [skip ci]
* Update ar.txt
* refactor: replace onnxruntime with llamacpp
Switches offline proofreader to llamacpp-kotlin GGUF and updates model settings UI to resolve 16 KB page alignment compatibility warnings.
* suggestion delete blacklist always. reload blacklist interface add.
* blocked words screen add. dictionary screen integration done. settings strings update.
* blacklist check case-insensitive. lowercase canonicalization added. user dictionary suggestion leak resolved.
* blacklist regex support added. compiled patterns cached. compile-time receiver errors resolved.
* SearchScreen remember key fix. filteredItems lambda dependency added. list auto-refresh working.
* fix(layout): align Arabic diacritics spacing
* chore: update README badges [skip ci]
* chore: update README badges [skip ci]
* chore: update README badges [skip ci]
* chore: update README badges [skip ci]
* chore: update README badges [skip ci]
* Allow for reasoning models; handle structured content arrays in API responses
Parse JSONArray content format with type and text fields. Extract reasoning_content when main content is blank. Fall back to firstChoice text field if content extraction fails.
* feat: add regex expander & fix dictionary crash
* feat(touchpad): double tap to select word & fix emoji popup preview
* feat(offline): add settings for custom sampling & prompt
* chore: update gitignore - add .env, .pi/ and remove duplicate
* docs: add F-Droid reproducibility delay notice
* chore: bump version to v3.8.6 and add changelog
* fix(touchpad): always select word on double tap and update docs
* feat(touchpad): implement multi-finger gestures and update docs
* feat(touchpad): reorganize gestures for intuitive rich text editing layout
* feat(touchpad): migrate gestures to 1 and 2 fingers
* docs: update features for llama.cpp migration
* docs: note model-dependent accuracy in features
* fix(touchpad): exit touchpad mode when opening clipboard or emoji
* perf(offline): optimize proofreading latency and load times
* fix(offline): improve GGUF prompt formatting and output cleaning
* fix(offline): truncate model output at template markers and add native stop sequences
* fix(offline): implement dynamic target-language-specific few-shot examples for GGUF translation
* feat(expander): immediate expand & fix revert
* feat: hold toolbar arrow keys to auto-repeat
* chore: update README badges [skip ci]
* chore: update README badges [skip ci]
* chore: update README badges [skip ci]
* feat: add toggle for insecure AI connections
Allows HTTP local endpoints and self-signed HTTPS connections only when explicitly enabled by the user.
* feat: add selective backup and restore
* fix: allow same word with different shortcuts
* feat: strip spaces before punctuation marks
* chore: update README badges [skip ci]
* chore: update README badges [skip ci]
* chore: update README badges [skip ci]
* chore: update README badges [skip ci]
* Change default popup key on letter ا in Persian language
* chore: update README badges [skip ci]
* feat: add handwriting input support
* docs: update 3.8.6 changelog
* fix: use wildcard mime type for file picker to avoid waydroid crash
* fix: clear code cache directory on plugin import/remove
* chore: add MD5 hash and size logging for loaded plugin
* feat(handwriting): fix crash and dynamic model downloading
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.
* feat: fix handwriting layout, theming and logic
* style: change handwriting toolbar icon color to white
* feat: show shortcut overlay on handwriting canvas when plugin missing
* fix(ai): resolve offline custom key token loss
Prevent token loss and hallucination in local models due to formatting and JNI bugs.
* feat(handwriting): add plugin downloader and refine blacklist
* build: limit abi filters to arm64-v8a
* docs: document handwriting and gguf features
* chore(release): bump to 3.10.0 (4000) + changelog
* fix(handwriting): don't cancel active keys when hiding inactive handwriting
---------
Co-authored-by: LeanBitLab <leanbitlab@users.noreply.github.com>
Co-authored-by: iBasim <57762287+iBasim@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Rohan Lodhi <42321434+RohanLodhi@users.noreply.github.com>
Co-authored-by: LeanBitLab <arjunathira2222@gmail.com>
Co-authored-by: David C <code@davidc.xyz>
Co-authored-by: nugraha-abd <62243267+nugraha-abd@users.noreply.github.com>
* Merge upstream LeanType v3.8.8 before 3.10.0 release (#111)
* fix(settings): fix and optimize gboard import
Parse Gboard format header dynamically to fix missing words and swapped values. Optimize using bulkInsert to reduce database insertion IPC overhead.
* chore: bump version to 3.8.4
Set versionCode to 3840 and versionName to 3.8.4 in build.gradle.kts. Create Fastlane changelog metadata at 3840.txt.
* feat(settings): improve text expander ui/ux
Add responsive search filtering for text expansion shortcuts. Add quick placeholder selection row with rich cursor-based insert support to Add/Edit Dialog.
* feat(settings): polish text expander guide and list
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.
* feat(settings): clean up text expander guide
Remove redundant template placeholder explanation while retaining the list of supported placeholder tags.
* feat(settings): add more expander placeholders
Add support and UI representations for %month%, %month_short%, %year%, and %week% placeholders.
* feat(settings): add system template placeholders
Add and integrate support for %battery%, %device%, and %android% placeholders.
* feat(settings): add language placeholder
Add and integrate support for %language% placeholder, which expands to the current active keyboard display language.
* fix(perf): prevent OOM on background image decode
- 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
* fix(stability): replace force-unwrap !! in hot paths
- 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.
* fix(stability): unregister SharedPreferences listener in spell-checker
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.
* fix(stability): don't call Looper.prepare() on background thread
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.
* fix(stability): make score-limit cache update atomic in Suggest
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.
* fix(stability): use named lock for dictionary blacklist
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.
* perf(perf): add key= to Lazy* list items for stable identity
- 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.
* perf(perf): remember() expensive computations in Composables
- 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.
* perf(perf): avoid Paint allocation per recomposition in ColorPickerDialog
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.
* perf(perf): stream logcat to file instead of buffering in memory
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.
* perf(perf): make ReorderSwitchPreference data class stable
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
* fix(perf): remove top-level MutableStateFlow in AIIntegrationScreen
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).
* fix(perf): scope errorJob to the LayoutEditDialog composable
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.
* fix(perf): replace GlobalScope in toolbar preference listener
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.
* fix(perf): make SettingsNavHost navigateTo scope supervised
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.
* fix(stability): replace !! in colorFilter() helper
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.
* perf(perf): cache main-thread Handler in ClipboardHistoryManager
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.
* fix(stability): use SupervisorJob in RichInputMethodManager scope
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.
* fix(stability): use ContextCompat.registerReceiver with NOT_EXPORTED 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.
* fix(settings): update ai provider fields dynamically
Align provider preference source and observe changes dynamically to update UI fields immediately. Wrap preferences in key() to prevent Compose state reuse.
* feat: add standardOptimised flavor and disable r8
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.
* perf: add baseline profile for standardOptimised
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.
* feat: remove standardOptimised package suffix
Remove applicationIdSuffix from standardOptimised product flavor to share standard package name (com.leanbitlab.leantype).
* fix: dismiss emoji dialog and update wizard status
Close ConfirmationDialog on successful emoji dictionary download/load. Invoke onSuccess callback in WelcomeWizard to trigger recomposition and show the checkmark immediately.
* Update ar.txt
* Update build-debug-apk.yml
* arabic-popup-and-harakat-tweak
* arabic-popup-and-harakat-tweak V2
* ci: add badge update workflow
* chore: update README badges [skip ci]
* fix: strip leading v from version tag
* chore: update README badges [skip ci]
* fix(badges): adjust width and add viewBox attributes to prevent clipping
* chore: update README badges [skip ci]
* chore(badges): rename download badge label to version
* chore: update README badges [skip ci]
* fix: prevent duplicate screenshots in clipboard
* feat: add toggle for screenshot compression
* feat: improve text expander, gestures, and emoji scale fit
* chore: update README badges [skip ci]
* fix: persist toolbar customizer key toggles
* build: bump version to v3.8.5
* feat: toggle dictionaries individually
* chore: add changelog for v3.8.5
* fix: split emoji search keyboard layout
* chore: update changelog for emoji search fix
* added auto detect feature
* changed registration flag
* chore: update README badges [skip ci]
* Update ar.txt
* refactor: replace onnxruntime with llamacpp
Switches offline proofreader to llamacpp-kotlin GGUF and updates model settings UI to resolve 16 KB page alignment compatibility warnings.
* suggestion delete blacklist always. reload blacklist interface add.
* blocked words screen add. dictionary screen integration done. settings strings update.
* blacklist check case-insensitive. lowercase canonicalization added. user dictionary suggestion leak resolved.
* blacklist regex support added. compiled patterns cached. compile-time receiver errors resolved.
* SearchScreen remember key fix. filteredItems lambda dependency added. list auto-refresh working.
* fix(layout): align Arabic diacritics spacing
* chore: update README badges [skip ci]
* chore: update README badges [skip ci]
* chore: update README badges [skip ci]
* chore: update README badges [skip ci]
* chore: update README badges [skip ci]
* Allow for reasoning models; handle structured content arrays in API responses
Parse JSONArray content format with type and text fields. Extract reasoning_content when main content is blank. Fall back to firstChoice text field if content extraction fails.
* feat: add regex expander & fix dictionary crash
* feat(touchpad): double tap to select word & fix emoji popup preview
* feat(offline): add settings for custom sampling & prompt
* chore: update gitignore - add .env, .pi/ and remove duplicate
* docs: add F-Droid reproducibility delay notice
* chore: bump version to v3.8.6 and add changelog
* fix(touchpad): always select word on double tap and update docs
* feat(touchpad): implement multi-finger gestures and update docs
* feat(touchpad): reorganize gestures for intuitive rich text editing layout
* feat(touchpad): migrate gestures to 1 and 2 fingers
* docs: update features for llama.cpp migration
* docs: note model-dependent accuracy in features
* fix(touchpad): exit touchpad mode when opening clipboard or emoji
* perf(offline): optimize proofreading latency and load times
* fix(offline): improve GGUF prompt formatting and output cleaning
* fix(offline): truncate model output at template markers and add native stop sequences
* fix(offline): implement dynamic target-language-specific few-shot examples for GGUF translation
* feat(expander): immediate expand & fix revert
* feat: hold toolbar arrow keys to auto-repeat
* chore: update README badges [skip ci]
* chore: update README badges [skip ci]
* chore: update README badges [skip ci]
* feat: add toggle for insecure AI connections
Allows HTTP local endpoints and self-signed HTTPS connections only when explicitly enabled by the user.
* feat: add selective backup and restore
* fix: allow same word with different shortcuts
* feat: strip spaces before punctuation marks
* chore: update README badges [skip ci]
* chore: update README badges [skip ci]
* chore: update README badges [skip ci]
* chore: update README badges [skip ci]
* Change default popup key on letter ا in Persian language
* chore: update README badges [skip ci]
* feat: add handwriting input support
* docs: update 3.8.6 changelog
* fix: use wildcard mime type for file picker to avoid waydroid crash
* fix: clear code cache directory on plugin import/remove
* chore: add MD5 hash and size logging for loaded plugin
* feat(handwriting): fix crash and dynamic model downloading
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.
* feat: fix handwriting layout, theming and logic
* style: change handwriting toolbar icon color to white
* feat: show shortcut overlay on handwriting canvas when plugin missing
* fix(ai): resolve offline custom key token loss
Prevent token loss and hallucination in local models due to formatting and JNI bugs.
* feat(handwriting): add plugin downloader and refine blacklist
* build: limit abi filters to arm64-v8a
* docs: document handwriting and gguf features
* chore: update README badges [skip ci]
* chore: update README badges [skip ci]
* fix(handwriting): avoid cancelling active keys when hidden
* build: update config, proguard rules, and blacklist parsing
* feat: tune double-tap shift timing and keep llamacpp proguard
* build: remove standardOptimised flavor
* fix: prevent handwriting suggestions from hiding
Also hide the redundant top toolbar on the handwriting panel during normal use, keeping it only for active download progress.
* chore: update README badges [skip ci]
* fix(toolbar): restore close/search on clipboard
* fix(settings): add label for clipboard_search key
* feat(settings): allow deleting handwriting model
* feat(dict): add dynamic dictionary downloader
* feat(dict): allow uninstalling downloaded dicts
* feat(dict): improve dynamic downloading flow
* fix: keep number row digits when keyboard is shifted
The number row layout used a shift_state_selector whose manualOrLocked
branch rendered the shifted symbol (!@#...) in place of the digit, so
engaging shift or shift-lock replaced 1234567890 with !@#$%^&*(). The
keys are now plain digit keys in every shift state, with the shifted
symbol kept as the first popup ahead of the existing fraction popups.
Fixes #180
* feat(dict): exclude non-en-US dictionaries from standard flavor assets
* feat(dict): show download button on toolbar if layout dictionary is not loaded
* fix: do not show disabled additional subtypes in dict settings list
* chore: add v3.8.7 and v3.8.8 changelogs
* fix: prevent WindowManager$BadTokenException in IME overlay dialog
* fix: only update split toolbar emoji recents when view is visible
* feat(emoji): close search on dictionary download
* feat(emoji): show download button in split toolbar
* chore: update changelog for 3.8.8
* chore: bump version to 3.8.8
* feat(handwriting): add download button to plugin required overlay
* chore: add handwriting plugin downloader to 3.8.8 changelog
* docs: temporarily hide F-Droid badge from README
* docs: remove F-Droid column from table to fix spacing
* docs: move download section above screenshots in README
* docs: remove fork AI feature description line from README
* docs: add Dynamic Downloader to README features
* docs: add Selective Backup, Blacklist, and OTP features to README
* docs: sort features by significance in README
* chore: update README badges [skip ci]
---------
Co-authored-by: LeanBitLab <leanbitlab@users.noreply.github.com>
Co-authored-by: iBasim <57762287+iBasim@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Rohan Lodhi <42321434+RohanLodhi@users.noreply.github.com>
Co-authored-by: LeanBitLab <arjunathira2222@gmail.com>
Co-authored-by: David C <code@davidc.xyz>
Co-authored-by: nugraha-abd <62243267+nugraha-abd@users.noreply.github.com>
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
* Merge upstream LeanType v3.8.9 before 3.10.0 release (#112)
* fix(settings): fix and optimize gboard import
Parse Gboard format header dynamically to fix missing words and swapped values. Optimize using bulkInsert to reduce database insertion IPC overhead.
* chore: bump version to 3.8.4
Set versionCode to 3840 and versionName to 3.8.4 in build.gradle.kts. Create Fastlane changelog metadata at 3840.txt.
* feat(settings): improve text expander ui/ux
Add responsive search filtering for text expansion shortcuts. Add quick placeholder selection row with rich cursor-based insert support to Add/Edit Dialog.
* feat(settings): polish text expander guide and list
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.
* feat(settings): clean up text expander guide
Remove redundant template placeholder explanation while retaining the list of supported placeholder tags.
* feat(settings): add more expander placeholders
Add support and UI representations for %month%, %month_short%, %year%, and %week% placeholders.
* feat(settings): add system template placeholders
Add and integrate support for %battery%, %device%, and %android% placeholders.
* feat(settings): add language placeholder
Add and integrate support for %language% placeholder, which expands to the current active keyboard display language.
* fix(perf): prevent OOM on background image decode
- 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
* fix(stability): replace force-unwrap !! in hot paths
- 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.
* fix(stability): unregister SharedPreferences listener in spell-checker
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.
* fix(stability): don't call Looper.prepare() on background thread
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.
* fix(stability): make score-limit cache update atomic in Suggest
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.
* fix(stability): use named lock for dictionary blacklist
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.
* perf(perf): add key= to Lazy* list items for stable identity
- 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.
* perf(perf): remember() expensive computations in Composables
- 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.
* perf(perf): avoid Paint allocation per recomposition in ColorPickerDialog
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.
* perf(perf): stream logcat to file instead of buffering in memory
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.
* perf(perf): make ReorderSwitchPreference data class stable
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
* fix(perf): remove top-level MutableStateFlow in AIIntegrationScreen
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).
* fix(perf): scope errorJob to the LayoutEditDialog composable
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.
* fix(perf): replace GlobalScope in toolbar preference listener
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.
* fix(perf): make SettingsNavHost navigateTo scope supervised
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.
* fix(stability): replace !! in colorFilter() helper
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.
* perf(perf): cache main-thread Handler in ClipboardHistoryManager
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.
* fix(stability): use SupervisorJob in RichInputMethodManager scope
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.
* fix(stability): use ContextCompat.registerReceiver with NOT_EXPORTED 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.
* fix(settings): update ai provider fields dynamically
Align provider preference source and observe changes dynamically to update UI fields immediately. Wrap preferences in key() to prevent Compose state reuse.
* feat: add standardOptimised flavor and disable r8
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.
* perf: add baseline profile for standardOptimised
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.
* feat: remove standardOptimised package suffix
Remove applicationIdSuffix from standardOptimised product flavor to share standard package name (com.leanbitlab.leantype).
* fix: dismiss emoji dialog and update wizard status
Close ConfirmationDialog on successful emoji dictionary download/load. Invoke onSuccess callback in WelcomeWizard to trigger recomposition and show the checkmark immediately.
* Update ar.txt
* Update build-debug-apk.yml
* arabic-popup-and-harakat-tweak
* arabic-popup-and-harakat-tweak V2
* ci: add badge update workflow
* chore: update README badges [skip ci]
* fix: strip leading v from version tag
* chore: update README badges [skip ci]
* fix(badges): adjust width and add viewBox attributes to prevent clipping
* chore: update README badges [skip ci]
* chore(badges): rename download badge label to version
* chore: update README badges [skip ci]
* fix: prevent duplicate screenshots in clipboard
* feat: add toggle for screenshot compression
* feat: improve text expander, gestures, and emoji scale fit
* chore: update README badges [skip ci]
* fix: persist toolbar customizer key toggles
* build: bump version to v3.8.5
* feat: toggle dictionaries individually
* chore: add changelog for v3.8.5
* fix: split emoji search keyboard layout
* chore: update changelog for emoji search fix
* added auto detect feature
* changed registration flag
* chore: update README badges [skip ci]
* Update ar.txt
* refactor: replace onnxruntime with llamacpp
Switches offline proofreader to llamacpp-kotlin GGUF and updates model settings UI to resolve 16 KB page alignment compatibility warnings.
* suggestion delete blacklist always. reload blacklist interface add.
* blocked words screen add. dictionary screen integration done. settings strings update.
* blacklist check case-insensitive. lowercase canonicalization added. user dictionary suggestion leak resolved.
* blacklist regex support added. compiled patterns cached. compile-time receiver errors resolved.
* SearchScreen remember key fix. filteredItems lambda dependency added. list auto-refresh working.
* fix(layout): align Arabic diacritics spacing
* chore: update README badges [skip ci]
* chore: update README badges [skip ci]
* chore: update README badges [skip ci]
* chore: update README badges [skip ci]
* chore: update README badges [skip ci]
* Allow for reasoning models; handle structured content arrays in API responses
Parse JSONArray content format with type and text fields. Extract reasoning_content when main content is blank. Fall back to firstChoice text field if content extraction fails.
* feat: add regex expander & fix dictionary crash
* feat(touchpad): double tap to select word & fix emoji popup preview
* feat(offline): add settings for custom sampling & prompt
* chore: update gitignore - add .env, .pi/ and remove duplicate
* docs: add F-Droid reproducibility delay notice
* chore: bump version to v3.8.6 and add changelog
* fix(touchpad): always select word on double tap and update docs
* feat(touchpad): implement multi-finger gestures and update docs
* feat(touchpad): reorganize gestures for intuitive rich text editing layout
* feat(touchpad): migrate gestures to 1 and 2 fingers
* docs: update features for llama.cpp migration
* docs: note model-dependent accuracy in features
* fix(touchpad): exit touchpad mode when opening clipboard or emoji
* perf(offline): optimize proofreading latency and load times
* fix(offline): improve GGUF prompt formatting and output cleaning
* fix(offline): truncate model output at template markers and add native stop sequences
* fix(offline): implement dynamic target-language-specific few-shot examples for GGUF translation
* feat(expander): immediate expand & fix revert
* feat: hold toolbar arrow keys to auto-repeat
* chore: update README badges [skip ci]
* chore: update README badges [skip ci]
* chore: update README badges [skip ci]
* feat: add toggle for insecure AI connections
Allows HTTP local endpoints and self-signed HTTPS connections only when explicitly enabled by the user.
* feat: add selective backup and restore
* fix: allow same word with different shortcuts
* feat: strip spaces before punctuation marks
* chore: update README badges [skip ci]
* chore: update README badges [skip ci]
* chore: update README badges [skip ci]
* chore: update README badges [skip ci]
* Change default popup key on letter ا in Persian language
* chore: update README badges [skip ci]
* feat: add handwriting input support
* docs: update 3.8.6 changelog
* fix: use wildcard mime type for file picker to avoid waydroid crash
* fix: clear code cache directory on plugin import/remove
* chore: add MD5 hash and size logging for loaded plugin
* feat(handwriting): fix crash and dynamic model downloading
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.
* feat: fix handwriting layout, theming and logic
* style: change handwriting toolbar icon color to white
* feat: show shortcut overlay on handwriting canvas when plugin missing
* fix(ai): resolve offline custom key token loss
Prevent token loss and hallucination in local models due to formatting and JNI bugs.
* feat(handwriting): add plugin downloader and refine blacklist
* build: limit abi filters to arm64-v8a
* docs: document handwriting and gguf features
* chore: update README badges [skip ci]
* chore: update README badges [skip ci]
* fix(handwriting): avoid cancelling active keys when hidden
* build: update config, proguard rules, and blacklist parsing
* feat: tune double-tap shift timing and keep llamacpp proguard
* build: remove standardOptimised flavor
* fix: prevent handwriting suggestions from hiding
Also hide the redundant top toolbar on the handwriting panel during normal use, keeping it only for active download progress.
* chore: update README badges [skip ci]
* fix(toolbar): restore close/search on clipboard
* fix(settings): add label for clipboard_search key
* feat(settings): allow deleting handwriting model
* feat(dict): add dynamic dictionary downloader
* feat(dict): allow uninstalling downloaded dicts
* feat(dict): improve dynamic downloading flow
* fix: keep number row digits when keyboard is shifted
The number row layout used a shift_state_selector whose manualOrLocked
branch rendered the shifted symbol (!@#...) in place of the digit, so
engaging shift or shift-lock replaced 1234567890 with !@#$%^&*(). The
keys are now plain digit keys in every shift state, with the shifted
symbol kept as the first popup ahead of the existing fraction popups.
Fixes #180
* feat(dict): exclude non-en-US dictionaries from standard flavor assets
* feat(dict): show download button on toolbar if layout dictionary is not loaded
* fix: do not show disabled additional subtypes in dict settings list
* chore: add v3.8.7 and v3.8.8 changelogs
* fix: prevent WindowManager$BadTokenException in IME overlay dialog
* fix: only update split toolbar emoji recents when view is visible
* feat(emoji): close search on dictionary download
* feat(emoji): show download button in split toolbar
* chore: update changelog for 3.8.8
* chore: bump version to 3.8.8
* feat(handwriting): add download button to plugin required overlay
* chore: add handwriting plugin downloader to 3.8.8 changelog
* docs: temporarily hide F-Droid badge from README
* docs: remove F-Droid column from table to fix spacing
* docs: move download section above screenshots in README
* docs: remove fork AI feature description line from README
* docs: add Dynamic Downloader to README features
* docs: add Selective Backup, Blacklist, and OTP features to README
* docs: sort features by significance in README
* chore: update README badges [skip ci]
* fix: make entire suggestion word a delete target on long-press
Previously the bin icon hit area was limited to the icon's intrinsic
pixel bounds, causing accidental word selection when trying to delete.
Now tapping anywhere on the word after long-press triggers deletion,
with a 3s auto-dismiss timeout to restore normal behavior.
* feat: add back armeabi-v7a to ABI filters
Restores 32-bit ARM native library packaging so libjni_latinime.so
is included in the APK for older ARM devices.
* chore: update README badges [skip ci]
* chore: update README badges [skip ci]
* chore: update README badges [skip ci]
* feat: split standard flavor and enable predictions
Separate standard flavor into FOSS-compliant standard and standardfull to pass F-Droid scanner. Query suggest engine for active predictions when composer is empty.
* chore: add active suggestions to v3.8.9 changelog
* docs: update v3.8.9 changelog with popup contrast fix
* ci: integrate docs release notes script into workflow
* feat(dict): support dynamic bigrams via space-separated personal dictionary entries
* feat(settings): support shortcut-specific prefixes for text expander
* docs(changelog): document dynamic bigrams and text expander shortcut-specific prefixes
* ci(workflow): automate github release creation on tag push
* fix(suggestions): color active prediction suggestion bright
* fix: remove fallback suggestions in split mode
* fix: toggle downloaded dicts & resolve path clash
* style: polish dictionary screen UI & theme support
* style: uniform size for dictionary screen icons
* style: show clean badge label for downloaded dict
* fix: support parent language dictionary fallback
* style: support downloaded emoji dict toggling
* style: unify dictionary toggling and remove buttons
* style: skip redundant cards for disabled language variants
* fix: support multi-part locales download path
* fix: persist clipboard dismiss and reduce timeout
* feat: implement text editing mode toolbar key and overlay
* feat: add separate Touchpad settings section and fullscreen mode
* feat: add close button to touchpad view
* fix: correct text expander suffix matching with shortcut specific prefixes
* style: layout prefix above shortcut in text expander dialog
* docs: update v3.8.9 changelog in fastlane
* docs: update v3.8.9 release notes
* docs: compact v3.8.9 changelog in fastlane
* docs: update release notes python script and gitignore
* fix: correct keystore path in build release workflow
* chore: update README badges [skip ci]
---------
Co-authored-by: LeanBitLab <leanbitlab@users.noreply.github.com>
Co-authored-by: iBasim <57762287+iBasim@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Rohan Lodhi <42321434+RohanLodhi@users.noreply.github.com>
Co-authored-by: LeanBitLab <arjunathira2222@gmail.com>
Co-authored-by: David C <code@davidc.xyz>
Co-authored-by: nugraha-abd <62243267+nugraha-abd@users.noreply.github.com>
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
* fix(keyboard): lay out Text Edit overlay in one-handed mode (#113)
---------
Co-authored-by: LeanBitLab <leanbitlab@users.noreply.github.com>
Co-authored-by: iBasim <57762287+iBasim@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Rohan Lodhi <42321434+RohanLodhi@users.noreply.github.com>
Co-authored-by: LeanBitLab <arjunathira2222@gmail.com>
Co-authored-by: David C <code@davidc.xyz>
Co-authored-by: nugraha-abd <62243267+nugraha-abd@users.noreply.github.com>
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
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.
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:RECEIVE_SMS)Conflict resolution — 29 conflicts across 20 files
Fork identity preserved throughout: appId
com.asafmah.leantypedual, version3.10.0/4000, LeanTypeDual branding,INTERNETstandard-only.build.gradle.ktsstandardOptimisedflavor block; adopt upstream arm64-only ABI, offlineminSdk 26, llama.cpp + ML Kit depsToolbarUtils/LayoutType/Defaults/strings.xmlHANDWRITING+ auto-read-OTPProofreadService(all flavors) +AIIntegrationScreenprefsproperty (per the prior v3.8.5 precedent); offline keeps its llama.cpp implDictionaryFacilitatorImplmContext+ emoji-dict cache invalidation); keep the fork's blocklist anti-resurrection logicInputLogicREADME/FEATURES/CHANGELOG/ fastlaneUpstream 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-rowMainKeyboardView, which callscancelAllOngoingEvents()and globally cancels activePointerTrackers. 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 asPRESSING/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.
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.armeabi-v7a; adopted here.minSdkraised to 26 (llama.cpp);offlinelitestays minSdk 21.AGENTS.mdstill says ABIsarmeabi-v7a, arm64-v8a/minSdk 21generally — left as-is but now slightly stale for offline.git diff --checkwarnings); left as-upstream.Regressions found by verification & fixed in this PR
DictionaryGroup.add/removeFromBlacklistskippedrebuildCompiledPatterns()when no blacklist file exists, so the merged regex-basedisBlacklistednever saw new entries → now rebuilds in-memory patterns regardless of file. (fixes 3DictionaryGroupTest)TextCorrectionScreenhad a duplicatePREF_COMPRESS_SCREENSHOTSSetting →SettingsContainerduplicate-key throw. Deduped. (fixes 12SettingsContainerTest)isShown()guard and confirmed on-device.Verification
compileOfflineRunTestsKotlinpasses).INTERNETconfirmed only insrc/standard/AndroidManifest.xml; offline/offlinelite inherit the network-freemainmanifest.standardDebug3.10.0 on a Samsung SM-S936B over wireless adb.