Skip to content

Commit 062fab2

Browse files
AsafMahLeanBitLabiBasimgithub-actions[bot]rohanlodhi
authored
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>
1 parent 8e7621e commit 062fab2

72 files changed

Lines changed: 3627 additions & 717 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
name: Update README Badges
2+
3+
on:
4+
schedule:
5+
- cron: '0 0 * * *' # Midnight UTC
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
update-badges:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- name: Fetch GitHub stats
18+
id: stats
19+
env:
20+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
21+
run: |
22+
REPO="LeanBitLab/HeliboardL"
23+
24+
# Latest version
25+
VERSION=$(gh api repos/$REPO/releases/latest --jq '.tag_name' | sed 's/^v//' 2>/dev/null || echo "N/A")
26+
echo "version=$VERSION" >> $GITHUB_OUTPUT
27+
28+
# Total downloads
29+
DOWNLOADS=$(gh api repos/$REPO/releases --jq '[.[].assets[]?.download_count] | add // 0' 2>/dev/null || echo "0")
30+
echo "downloads=$DOWNLOADS" >> $GITHUB_OUTPUT
31+
32+
# Stars
33+
STARS=$(gh api repos/$REPO --jq '.stargazers_count' 2>/dev/null || echo "0")
34+
echo "stars=$STARS" >> $GITHUB_OUTPUT
35+
36+
- name: Generate badge SVGs
37+
env:
38+
VERSION: ${{ steps.stats.outputs.version }}
39+
DOWNLOADS: ${{ steps.stats.outputs.downloads }}
40+
STARS: ${{ steps.stats.outputs.stars }}
41+
run: |
42+
mkdir -p docs/badges
43+
44+
# Format numbers with commas
45+
DOWNLOADS_FMT=$(printf "%'d" "$DOWNLOADS" 2>/dev/null || echo "$DOWNLOADS")
46+
STARS_FMT=$(printf "%'d" "$STARS" 2>/dev/null || echo "$STARS")
47+
48+
# Download version badge
49+
cat > docs/badges/download.svg << EOF
50+
<svg xmlns="http://www.w3.org/2000/svg" width="94" height="28" viewBox="0 0 94 28"><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#fff" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="94" height="28" rx="4" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="50" height="28" fill="#555"/><rect x="50" width="44" height="28" fill="#7C4DFF"/><rect width="94" height="28" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11"><text x="25" y="19" fill="#010101" fill-opacity=".3">Version</text><text x="25" y="18">Version</text><text x="72" y="19" fill="#010101" fill-opacity=".3">v${VERSION}</text><text x="72" y="18">v${VERSION}</text></g></svg>
51+
EOF
52+
53+
# Downloads count badge
54+
cat > docs/badges/downloads.svg << EOF
55+
<svg xmlns="http://www.w3.org/2000/svg" width="105" height="28" viewBox="0 0 105 28"><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#fff" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="105" height="28" rx="4" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="64" height="28" fill="#555"/><rect x="64" width="41" height="28" fill="#7C4DFF"/><rect width="105" height="28" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11"><text x="32" y="19" fill="#010101" fill-opacity=".3">Downloads</text><text x="32" y="18">Downloads</text><text x="84.5" y="19" fill="#010101" fill-opacity=".3">${DOWNLOADS_FMT}</text><text x="84.5" y="18">${DOWNLOADS_FMT}</text></g></svg>
56+
EOF
57+
58+
# Stars badge
59+
cat > docs/badges/stars.svg << EOF
60+
<svg xmlns="http://www.w3.org/2000/svg" width="72" height="28" viewBox="0 0 72 28"><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#fff" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="72" height="28" rx="4" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="38" height="28" fill="#555"/><rect x="38" width="34" height="28" fill="#7C4DFF"/><rect width="72" height="28" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11"><text x="19" y="19" fill="#010101" fill-opacity=".3">Stars</text><text x="19" y="18">Stars</text><text x="55" y="19" fill="#010101" fill-opacity=".3">${STARS_FMT}</text><text x="55" y="18">${STARS_FMT}</text></g></svg>
61+
EOF
62+
63+
echo "Generated: v$VERSION | $DOWNLOADS_FMT downloads | $STARS_FMT stars"
64+
65+
- name: Update README badge URLs
66+
run: |
67+
# Replace shields.io URLs with local badge paths
68+
sed -i 's|https://img.shields.io/github/v/release/LeanBitLab/HeliboardL?label=Download\&style=for-the-badge\&color=7C4DFF|docs/badges/download.svg|g' README.md
69+
sed -i 's|https://img.shields.io/github/downloads/LeanBitLab/HeliboardL/total?style=for-the-badge\&color=7C4DFF\&label=Downloads|docs/badges/downloads.svg|g' README.md
70+
sed -i 's|https://img.shields.io/github/stars/LeanBitLab/HeliboardL?style=for-the-badge\&color=7C4DFF|docs/badges/stars.svg|g' README.md
71+
72+
- name: Commit changes
73+
run: |
74+
git config user.name "github-actions[bot]"
75+
git config user.email "github-actions[bot]@users.noreply.github.com"
76+
git add docs/badges/ README.md
77+
git diff --staged --quiet || git commit -m "chore: update README badges [skip ci]"
78+
git push

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,8 @@ docs/superpowers/
7777
.agents
7878
.kilo/
7979
.antigravitycli/
80+
81+
.env
82+
83+
# AI agent config (personal, not shared)
84+
.pi/

CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,30 @@ and this project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
1414
1515
## [Unreleased]
1616

17+
## [3.10.0] - 2026-06-20
18+
19+
### Added
20+
- **Handwriting input** (Standard builds) — write characters on a recognition canvas using a
21+
downloadable plugin, with a dedicated bottom-row layout and a toolbar key.
22+
- **Auto-read OTP from SMS** — a one-time code from an incoming SMS is offered in the suggestion
23+
strip while the keyboard is open; tap to insert. Uses a runtime, opt-in SMS permission.
24+
- **Regex shortcuts in Text Expander** — expansion triggers can be matched by regular expression.
25+
26+
### Changed
27+
- **Offline AI backend switched from ONNX Runtime to llama.cpp (GGUF).** The Offline build now
28+
loads compact quantized **GGUF** models on-device with configurable sampling
29+
(temperature / top-p / top-k / min-p); it now requires Android 8 (API 26).
30+
- **Touchpad gestures reworked** into a fuller one-/two-finger suite (word select, word-by-word
31+
navigation, space, copy/paste, cut/select-all, undo/redo, hold-to-backspace). Single-finger
32+
double-tap now **selects the word** (previously deleted the selection).
33+
- Release builds now target the **arm64-v8a** ABI only.
34+
35+
### Upstream
36+
- Merged **LeanBitLab/LeanType v3.8.6** (from v3.8.3) — the source of the handwriting,
37+
llama.cpp/GGUF, touchpad-gesture, and SMS-OTP changes above. Fork identity (LeanTypeDual, distinct
38+
`applicationId`, two-thumb typing, the Gemini standard-AI layer, and the privacy tiers) is
39+
preserved.
40+
1741
## [3.9.1] - 2026-06-11
1842

1943
### Fixed

README.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@ Type with **both thumbs gliding at the same time**: LeanTypeDual aggregates mult
2222
### On top of that — LeanType's AI layer and quality-of-life features
2323

2424
- **[🤖 Multi-Provider AI](docs/FEATURES.md#supported-ai-providers)** - Proofread using **Gemini**, **Groq** (Llama 3, Mixtral), or **OpenAI-compatible** providers, with dynamic fetching of the latest models.
25-
- **[🛡️ Offline AI](docs/FEATURES.md#5-offline-proofreading-privacy-focused)** - Private, on-device proofreading and translation using ONNX models (Offline build only).
25+
- **[🛡️ Offline AI (GGUF)](docs/FEATURES.md#5-offline-proofreading-privacy-focused)** - Private, on-device proofreading and translation using local **GGUF models** powered by `llama.cpp` (Offline build only).
2626
- **🌐 AI Translation** - Translate selected text using your chosen provider, with a separate model selector.
27+
- **[✍️ Handwriting Input](docs/FEATURES.md#8-handwriting-input)** - Draw characters directly on a handwriting recognition canvas (Standard version, requires [Leantype-Handwriting-Plugin](https://github.com/LeanBitLab/Leantype-Handwriting-Plugin)).
2728
- **[🧠 Custom AI Keys](docs/FEATURES.md#4-custom-ai-keys--keywords)** - Assign custom prompts, personas (#editor, #proofread), and labels/tags (themed capsules) to 10 customizable toolbar keys.
28-
- **📝 Text Expander** - Shortcut → expansion with dynamic placeholders (`%clipboard%`, `%day%`, `%time12%`, `%cursor%`, lists), backspace-to-revert, and a guide.
29+
- **📝 Text Expander** - Shortcut → expansion with dynamic placeholders (`%clipboard%`, `%day%`, `%time12%`, `%cursor%`, lists), regex shortcuts, backspace-to-revert, and a guide.
2930
- **🧠 Smarter learned words** - *graduated trust* keeps a just-learned word below real-dictionary suggestions until you've used it a few times (no premature autocorrect to half-typed words); flag unknown words to **Add** or **Block** them via a Blocklist screen.
3031
- **↩️ Undo word** - a toolbar key that reverts the last committed word back to its suggestion alternatives.
3132
- **🗂️ Per-dictionary control** - enable or disable individual built-in and custom dictionaries.
@@ -38,7 +39,7 @@ Type with **both thumbs gliding at the same time**: LeanTypeDual aggregates mult
3839
- **📸 Screenshot Suggestion & Clipboard** - Recently-taken screenshots are offered in the suggestion strip and saved to clipboard history.
3940
- **🔎 Emoji Search** - Search emojis by name. *Requires loading an Emoji Dictionary.*
4041
- **⚙️ Enhanced Customization** - Force auto-capitalization, fine-grained haptics, distinct incognito icon, reorganized settings, and more.
41-
- **🔒 Privacy Choices** - Choose **Standard** (opt-in AI), **Offline** (network hard-disabled, offline model), or **Offline Lite** (no AI, ~20 MB).
42+
- **🔒 Privacy Choices** - Choose **Standard** (opt-in AI, handwriting), **Offline** (network hard-disabled, offline GGUF model), or **Offline Lite** (no AI, ~20 MB).
4243

4344

4445

@@ -73,20 +74,22 @@ Type with **both thumbs gliding at the same time**: LeanTypeDual aggregates mult
7374
</tr>
7475
</table>
7576

77+
> **⚠️ Note:** F-Droid releases might be delayed or stuck again due to reproducibility verification issues. For the latest version, use GitHub Releases or Obtainium.
78+
7679
### 📦 Choose Your Version
7780

7881
#### 1. Standard Version (`-standard-release.apk`)
79-
* **Features:** Full suite including **AI Proofreading**, **AI Translation**, and **Gesture Library Downloader**.
80-
* **Permissions:** Request `INTERNET` permission (used *only* when you explicitly use AI features).
81-
* **Setup:** Use the built-in downloader for Gesture Typing. Configure AI keys in Settings.
82+
* **Features:** Full suite including **AI Proofreading**, **AI Translation**, **Handwriting Input**, and **Gesture Library Downloader**.
83+
* **Permissions:** Request `INTERNET` permission (used *only* when you explicitly use AI features, download plugins, or update libraries).
84+
* **Setup:** Use the built-in downloader for Gesture Typing and Handwriting Input. Configure AI keys in Settings.
8285

8386
#### 2. Offline Version (`-offline-release.apk`)
84-
* **Features:** All UI/UX enhancements and **Offline Neural Proofreading** (ONNX).
87+
* **Features:** All UI/UX enhancements and **Offline Neural Proofreading** (via `llama.cpp` using local **GGUF models**).
8588
* **Permissions:** **NO INTERNET PERMISSION**. Guaranteed at OS level.
8689
* **Best For:** Privacy purists.
8790
* **Manual Setup Required:**
8891
* **Gesture Typing:** [Download library manually](https://github.com/erkserkserks/openboard/tree/46fdf2b550035ca69299ce312fa158e7ade36967/app/src/main/jniLibs) and load via *Settings > Gesture typing*.
89-
* **Offline AI:** Download ONNX models and load via *Settings > AI Integration*. 👉 **[See Offline Setup Instructions](docs/FEATURES.md#3-offline-proofreading-privacy-focused)**
92+
* **Offline AI:** Download GGUF models and load via *Settings > Advanced > GGUF Model (.gguf)*. 👉 **[See Offline Setup Instructions](docs/FEATURES.md#5-offline-proofreading-privacy-focused)**
9093

9194
#### 3. Offline Lite Version (`-offlinelite-release.apk`)
9295
* **Features:** All UI/UX enhancements but **NO AI FEATURES**.

app/build.gradle.kts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,22 @@ if (keystorePropertiesFile.exists()) {
1616
}
1717

1818
android {
19-
compileSdk = 35
19+
compileSdk = 36
2020

2121
defaultConfig {
2222
applicationId = "com.asafmah.leantypedual"
2323
minSdk = 21
2424
targetSdk = 35
25-
versionCode = 3910
26-
versionName = "3.9.1"
25+
versionCode = 4000
26+
versionName = "3.10.0"
2727

2828
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
2929

3030
ndk {
31-
abiFilters.addAll(arrayOf("armeabi-v7a", "arm64-v8a"))
31+
abiFilters.addAll(arrayOf("arm64-v8a"))
3232
}
3333
}
3434

35-
// ONNX Runtime is used instead of llama.cpp native build
36-
3735
flavorDimensions += "privacy"
3836
productFlavors {
3937
create("standard") {
@@ -45,6 +43,7 @@ android {
4543
create("offline") {
4644
dimension = "privacy"
4745
applicationIdSuffix = ".offline"
46+
minSdk = 26
4847
}
4948
create("offlinelite") {
5049
dimension = "privacy"
@@ -141,7 +140,7 @@ android {
141140
path = File("src/main/jni/Android.mk")
142141
}
143142
}
144-
// ndkVersion = "28.0.13004108"
143+
ndkVersion = "28.0.13004108"
145144

146145
packaging {
147146
jniLibs {
@@ -235,8 +234,21 @@ dependencies {
235234
"standardOptimisedImplementation"("androidx.security:security-crypto:1.1.0-alpha06")
236235

237236
// local llm proofreading (offline)
238-
// ONNX Runtime for T5 encoder-decoder grammar models
239-
"offlineImplementation"("com.microsoft.onnxruntime:onnxruntime-android:1.17.3")
237+
"offlineImplementation"("io.github.ljcamargo:llamacpp-kotlin:0.4.0")
238+
239+
// Force 16 KB page-aligned version of graphics-path
240+
implementation("androidx.graphics:graphics-path:1.1.0")
241+
242+
// WorkManager — required by ML Kit Digital Ink plugin (loaded via DexClassLoader).
243+
// ML Kit internally calls WorkManager.getInstance(context) using the host app context,
244+
// so the host app must have WorkManagerInitializer registered in its manifest.
245+
implementation("androidx.work:work-runtime-ktx:2.10.1")
246+
247+
// ML Kit Digital Ink Recognition — required by the handwriting plugin.
248+
// ML Kit's internal asset manager and native library loader use the host app context,
249+
// so the host app must compile and include the client library resources/libraries.
250+
"standardImplementation"("com.google.mlkit:digital-ink-recognition:19.0.0")
251+
"standardOptimisedImplementation"("com.google.mlkit:digital-ink-recognition:19.0.0")
240252

241253
// test
242254
testImplementation(kotlin("test"))

app/proguard-rules.pro

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,6 @@
2828
# Keep java-llama.cpp classes
2929
-keep class de.kherud.llama.** { *; }
3030

31-
# ONNX Runtime configurations
32-
-dontwarn com.google.protobuf.**
33-
-keep class ai.onnxruntime.** { *; }
3431

3532
# Fix correct service name
3633
-keep class helium314.keyboard.latin.utils.ProofreadService { *; }

app/src/main/AndroidManifest.xml

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
88
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
99
xmlns:tools="http://schemas.android.com/tools">
1010

11-
11+
<uses-sdk tools:overrideLibrary="androidx.graphics.path,com.google.mlkit.vision.digitalink.recognition,com.google.mlkit.digitalink.common" />
1212

1313
<uses-permission android:name="android.permission.READ_USER_DICTIONARY" />
1414
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
1515
<uses-permission android:name="android.permission.VIBRATE" />
1616
<uses-permission android:name="android.permission.WRITE_USER_DICTIONARY" />
1717
<uses-permission android:name="android.permission.READ_CONTACTS" />
18+
<!-- Optional: only requested at runtime when the user enables the "Auto-read OTP" feature. -->
19+
<uses-permission android:name="android.permission.RECEIVE_SMS" />
1820
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
1921
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
2022
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
@@ -27,6 +29,8 @@ SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
2729
android:allowBackup="false"
2830
android:defaultToDeviceProtectedStorage="true"
2931
android:directBootAware="true"
32+
android:networkSecurityConfig="@xml/network_security_config"
33+
android:usesCleartextTraffic="true"
3034
tools:remove="android:appComponentFactory"
3135
tools:targetApi="p">
3236

@@ -122,6 +126,34 @@ SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
122126
android:resource="@xml/provider_paths" />
123127
</provider>
124128

129+
<!-- Disable WorkManager auto-initializer; App implements Configuration.Provider instead.
130+
Required for ML Kit Digital Ink plugin loaded via DexClassLoader. -->
131+
<provider
132+
android:name="androidx.startup.InitializationProvider"
133+
android:authorities="${applicationId}.androidx-startup"
134+
android:exported="false"
135+
tools:node="merge">
136+
<meta-data
137+
android:name="androidx.work.WorkManagerInitializer"
138+
android:value="androidx.startup"
139+
tools:node="remove" />
140+
</provider>
141+
142+
<!-- Service for ML Kit component discovery.
143+
Since the ML Kit libraries are loaded dynamically from the plugin APK,
144+
we must register these in the host app manifest so ML Kit can find them. -->
145+
<service
146+
android:name="com.google.mlkit.common.internal.MlKitComponentDiscoveryService"
147+
android:directBootAware="true"
148+
android:exported="false">
149+
<meta-data
150+
android:name="com.google.firebase.components:com.google.mlkit.common.internal.CommonComponentRegistrar"
151+
android:value="com.google.firebase.components.ComponentRegistrar" />
152+
<meta-data
153+
android:name="com.google.firebase.components:com.google.mlkit.vision.digitalink.internal.DigitalInkRecognitionRegistrar"
154+
android:value="com.google.firebase.components.ComponentRegistrar" />
155+
</service>
156+
125157
</application>
126158

127159
<queries>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[
2+
[
3+
{ "label": "alpha", "width": 0.15 },
4+
{ "label": "clear_handwriting", "width": 0.15 },
5+
{ "label": "space", "width": -1 },
6+
{ "label": "delete", "width": 0.15 }
7+
]
8+
]
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[
2+
[
3+
{ "label": "alpha", "width": 0.15 },
4+
{ "label": "clear_handwriting", "width": 0.15 },
5+
{ "label": "space", "width": -1 },
6+
{ "label": "delete", "width": 0.15 },
7+
{ "label": "action", "width": 0.15 }
8+
]
9+
]

app/src/main/assets/locale_key_texts/ar.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
ه ﻫ|ه‍
55
ج چ
66
ش ڜ
7-
ي ئ ى
7+
ي ى ئ
88
ب پ
99
ل ﻻ|لا ﻷ|لأ ﻹ|لإ ﻵ|لآ
10-
ا !fixedOrder!5 آ ء أ إ ٱ
10+
ا !fixedOrder!5 أ ٱ إ آ ء
1111
ك گ ک
1212
ى ئ
1313
ز ژ
1414
و ؤ
15-
punctuation !fixedOrder!7 ٕ|ٕ ٔ|ٔ ْ|ْ ٍ ٌ|ٌ ً ّ|ّ ٖ|ٖ ٰ ٓ|ٓ ِ|ِ ُ|ُ َ|َ ـــ|ـ
15+
punctuation !fixedOrder!7 ّ◌|ّ ْ◌|ْ َ◌|َ ِ◌|ِ ُ◌|ُ ٍ◌ً◌ٌ◌|ٌ ٓ◌|ٓ ٰ◌ٕ◌|ٕ ٔ◌|ٔ ٖ◌|ٖ ـــ|ـ
1616
« „ “ ”
1717
» ‚ ‘ ’ ‹ ›
1818

0 commit comments

Comments
 (0)