Skip to content

release: LeanTypeDual v3.8.5#55

Merged
AsafMah merged 9 commits into
mainfrom
dev
Jun 6, 2026
Merged

release: LeanTypeDual v3.8.5#55
AsafMah merged 9 commits into
mainfrom
dev

Conversation

@AsafMah

@AsafMah AsafMah commented Jun 6, 2026

Copy link
Copy Markdown
Owner

Promotes devmain for the first LeanTypeDual release (the dev-integration workflow's release step). main was release-only and far behind; this brings it up to the current integrated state.

What ships

Before merging (release checklist, your call)

  • Confirm on-device dogfooding (esp. backspace after the upstream text-expander merge).
  • Changelog / fastlane release notes for v3.8.5 if you want them refreshed.
  • After merge: tag the release and build the signed APK via tools/release.py.

This is the deliberate release gate — leaving it for you to review + merge.

AsafMah and others added 9 commits June 6, 2026 12:24
* 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.

* 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

* fix(merge): resolve getPrefs() platform clash across ProofreadService flavors

The v3.8.5 merge combined our cached 'private val prefs' (offline) with upstream's added 'fun getPrefs()' (ce755f4), clashing on the getPrefs() JVM signature. Unify all three flavors on a public 'prefs' property and update the single caller (AIIntegrationScreen) to 'service.prefs'.

---------

Co-authored-by: LeanBitLab <leanbitlab@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Builds on the existing gesture-debug overlay (PREF_GESTURE_DEBUG_DRAW_POINTS, relabeled 'Typing insight'): rounded/glowing trail, a join ring + haptic tick when a gesture/tap extends the current word (fires on extendExistingCompose/usedMergedTrail), and clears the trail on a new word start and on backspace. Removes the redundant result pill.
…n) (#46)

Adds a golden-master replay corpus to InputLogicTest. Each Scenario is an
ordered list of input events (Type / Gesture / Func / GraceExpire) replayed
through the existing Robolectric pipeline, asserting the resulting editor
text. Adding a two-thumb spacing/combining regression case is now a data row,
not a new test method.

Seeded with the core scenarios: plain typing, combining-grace autospace,
force-next-space (no double space, and during combining), and join-next
resuming the word for the next gesture.

Scope note: gesture results are fed deterministically by the test harness
(ShadowFacilitator), so this covers the input/spacing/combining LOGIC, not
native glide recognition. Replaying recorded *pointer* traces through the
native recognizer needs on-device instrumented tests -- the on-device
recorder (#20) and CI wiring (#22) remain.

Verified (JDK 21): replayTwoThumbCorpus passes; InputLogicTest = 85 tests
with the same 3 pre-existing failures as main, 0 new. Change is test-only.

Part of #21 (epic #13).
* docs: normalize agents guide to AGENTS.md (Repository Guidelines)

Replaces the long lowercase Agents.md with the concise, standard-format AGENTS.md (synthesized earlier from parallel scouts) and removes the case-variant duplicate. Includes the rubber_duck Review Workflow section.

* docs: clarify fork lineage and what 'upstream' means

HeliBoard -> LeanBitLab/LeanType (upstream) -> AsafMah/LeanType (this repo). 'upstream' = LeanBitLab/LeanType. This fork ships as a distinct app 'LeanTypeDual' (own applicationId, installs alongside upstream).
…Id) (#53)

Currently the build carried the upstream LeanBitLab/LeanType identity (applicationId com.leanbitlab.leantype, name 'LeanType'), so it was not a distinct app and could not install alongside upstream. Rebrand to LeanTypeDual: applicationId -> com.asafmah.leantypedual, IME/launcher name -> LeanTypeDual, APK filename, GITHUB link -> AsafMah/LeanType, fastlane title, and the stale Bengali HeliBoard strings. Java package (helium314.keyboard) unchanged.
* ci: run unit tests (runTests variant) on PRs to dev/main, not just compile

Closes the #12 gap (CI only compiled). Runs :app:testOfflineRunTestsUnitTest, which uses the runTests build type whose tests self-skip the network/data tests known to fail on CI. Triggers on PRs to dev+main and pushes to dev.

* ci: run the test suite non-blocking + always upload reports (until #12 triaged)
@AsafMah
AsafMah merged commit 0a51b13 into main Jun 6, 2026
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.

1 participant