Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,8 @@ docs/superpowers/
.agents
.kilo/
.antigravitycli/

.env

# AI agent config (personal, not shared)
.pi/
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,40 @@ and this project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html)

## [Unreleased]

## [3.10.0] - 2026-06-20

### Added
- **Handwriting input** (Standard builds) — write characters on a recognition canvas using a
downloadable plugin, with a dedicated bottom-row layout and a toolbar key.
- **Auto-read OTP from SMS** — a one-time code from an incoming SMS is offered in the suggestion
strip while the keyboard is open; tap to insert. Uses a runtime, opt-in SMS permission.
- **Regex shortcuts in Text Expander** — expansion triggers can be matched by regular expression.
- **Dynamic dictionary/plugin downloader** — Standard builds can fetch layout dictionaries, emoji dictionaries, and handwriting plugins on demand.
- **Selective backup and restore** — backup/restore settings, dictionaries, and AI prompt configuration more granularly.

### Changed
- **Offline AI backend switched from ONNX Runtime to llama.cpp (GGUF).** The Offline build now
loads compact quantized **GGUF** models on-device with configurable sampling
(temperature / top-p / top-k / min-p); it now requires Android 8 (API 26).
- **Touchpad gestures reworked** into a fuller one-/two-finger suite (word select, word-by-word
navigation, space, copy/paste, cut/select-all, undo/redo, hold-to-backspace). Single-finger
double-tap now **selects the word** (previously deleted the selection).
- Release builds now target the **arm64-v8a** ABI only.
- Standard builds now exclude non-en-US dictionary assets and download optional dictionaries dynamically.

### Fixed
- **Sticky Shift from upstream handwriting cleanup** — upstream v3.8.6 stopped the hidden handwriting
bottom row on every keyboard-frame switch, which globally cancelled the active Shift pointer before
release. We keep the upstream handwriting feature but only stop handwriting when it is actually
shown. (Upstream bug LeanBitLab/LeanType#186; upstream PR #194.)

### Upstream
- Merged **LeanBitLab/LeanType v3.8.8** (from v3.8.3, including v3.8.7 and two post-tag docs/badge
commits) — the source of the handwriting, llama.cpp/GGUF, dynamic downloader, touchpad-gesture,
SMS-OTP, selective-backup, and dictionary-downloader changes above. Fork identity (LeanTypeDual,
distinct `applicationId`, two-thumb typing, the Gemini standard-AI layer, and the privacy tiers) is
preserved.

## [3.9.1] - 2026-06-11

### Fixed
Expand Down
22 changes: 14 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,27 @@ Type with **both thumbs gliding at the same time**: LeanTypeDual aggregates mult
### On top of that — LeanType's AI layer and quality-of-life features

- **[🤖 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.
- **[🛡️ Offline AI](docs/FEATURES.md#5-offline-proofreading-privacy-focused)** - Private, on-device proofreading and translation using ONNX models (Offline build only).
- **[🛡️ 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).
- **🌐 AI Translation** - Translate selected text using your chosen provider, with a separate model selector.
- **[✍️ 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)).
- **[🧠 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.
- **📝 Text Expander** - Shortcut → expansion with dynamic placeholders (`%clipboard%`, `%day%`, `%time12%`, `%cursor%`, lists), backspace-to-revert, and a guide.
- **📝 Text Expander** - Shortcut → expansion with dynamic placeholders (`%clipboard%`, `%day%`, `%time12%`, `%cursor%`, lists), regex shortcuts, backspace-to-revert, and a guide.
- **🧠 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.
- **↩️ Undo word** - a toolbar key that reverts the last committed word back to its suggestion alternatives.
- **🗂️ Per-dictionary control** - enable or disable individual built-in and custom dictionaries.
- **📥 Dynamic Downloader** - Standard builds can download layout dictionaries, emoji dictionaries, and handwriting plugins on demand, keeping the initial app smaller.
- **🪟 Floating Keyboard** - Detach the keyboard into a draggable, resizable window (true OS-level overlay), with an optional persistent mode.
- **⌨️ Dual Toolbar / Split Suggestions** - Split the suggestion strip and toolbar for easier reach.
- **🖱️ Touchpad Mode** - Swipe the spacebar up for a cursor touchpad with sensitivity controls and edge-scroll acceleration, including a full-screen laptop-style mode.
- **🎨 Modern UI** - "Squircle" key backgrounds, refined icons, and polished aesthetics.
- **🔄 Google Dictionary Import** - Import your personal dictionary words.
- **🔍 Clipboard Search & Undo** - Search clipboard history from the toolbar, undo accidental deletions, and fold pinned items by default.
- **📸 Screenshot Suggestion & Clipboard** - Recently-taken screenshots are offered in the suggestion strip and saved to clipboard history.
- **✉️ Auto-Read OTP** - Incoming one-time codes can appear in the suggestion strip for quick insertion.
- **💾 Selective Backup & Restore** - Backup and restore settings, dictionaries, and AI prompt configuration selectively.
- **🔎 Emoji Search** - Search emojis by name. *Requires loading an Emoji Dictionary.*
- **⚙️ Enhanced Customization** - Force auto-capitalization, fine-grained haptics, distinct incognito icon, reorganized settings, and more.
- **🔒 Privacy Choices** - Choose **Standard** (opt-in AI), **Offline** (network hard-disabled, offline model), or **Offline Lite** (no AI, ~20 MB).
- **🔒 Privacy Choices** - Choose **Standard** (opt-in AI, handwriting), **Offline** (network hard-disabled, offline GGUF model), or **Offline Lite** (no AI, ~20 MB).



Expand Down Expand Up @@ -73,20 +77,22 @@ Type with **both thumbs gliding at the same time**: LeanTypeDual aggregates mult
</tr>
</table>

> **⚠️ Note:** F-Droid releases might be delayed or stuck again due to reproducibility verification issues. For the latest version, use GitHub Releases or Obtainium.

### 📦 Choose Your Version

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

#### 2. Offline Version (`-offline-release.apk`)
* **Features:** All UI/UX enhancements and **Offline Neural Proofreading** (ONNX).
* **Features:** All UI/UX enhancements and **Offline Neural Proofreading** (via `llama.cpp` using local **GGUF models**).
* **Permissions:** **NO INTERNET PERMISSION**. Guaranteed at OS level.
* **Best For:** Privacy purists.
* **Manual Setup Required:**
* **Gesture Typing:** [Download library manually](https://github.com/erkserkserks/openboard/tree/46fdf2b550035ca69299ce312fa158e7ade36967/app/src/main/jniLibs) and load via *Settings > Gesture typing*.
* **Offline AI:** Download ONNX models and load via *Settings > AI Integration*. 👉 **[See Offline Setup Instructions](docs/FEATURES.md#3-offline-proofreading-privacy-focused)**
* **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)**

#### 3. Offline Lite Version (`-offlinelite-release.apk`)
* **Features:** All UI/UX enhancements but **NO AI FEATURES**.
Expand Down
67 changes: 39 additions & 28 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,35 +16,32 @@ if (keystorePropertiesFile.exists()) {
}

android {
compileSdk = 35
compileSdk = 36

defaultConfig {
applicationId = "com.asafmah.leantypedual"
minSdk = 21
targetSdk = 35
versionCode = 3910
versionName = "3.9.1"
versionCode = 4000
versionName = "3.10.0"

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

ndk {
abiFilters.addAll(arrayOf("armeabi-v7a", "arm64-v8a"))
abiFilters.addAll(arrayOf("arm64-v8a"))
}
}

// ONNX Runtime is used instead of llama.cpp native build

flavorDimensions += "privacy"
productFlavors {
create("standard") {
dimension = "privacy"
}
create("standardOptimised") {
dimension = "privacy"
minSdk = 23
}
create("offline") {
dimension = "privacy"
applicationIdSuffix = ".offline"
minSdk = 26
}
create("offlinelite") {
dimension = "privacy"
Expand Down Expand Up @@ -107,7 +104,6 @@ android {
"standard" -> "1"
"offline" -> "2"
"offlinelite" -> "3"
"standardOptimised" -> "4"
else -> ""
}
if (number.isNotEmpty()) {
Expand All @@ -120,13 +116,28 @@ android {
}
// got a little too big for GitHub after some dependency upgrades, so we remove the largest dictionary
androidComponents.onVariants { variant: ApplicationVariant ->
val patterns = mutableListOf<String>()
if (variant.buildType == "debug") {
variant.androidResources.ignoreAssetsPatterns = listOf("main_ro.dict")
patterns.add("main_ro.dict")
variant.proguardFiles = emptyList()
//noinspection ProguardAndroidTxtUsage we intentionally use the "normal" file here
variant.proguardFiles.add(project.layout.buildDirectory.file(getDefaultProguardFile("proguard-android.txt").absolutePath))
variant.proguardFiles.add(project.layout.buildDirectory.file(project.buildFile.parent + "/proguard-rules.pro"))
}
if (variant.flavorName == "standard") {
// ponytail: dynamically find all dict files to ignore in standard flavor except main_en-US.dict
val dictsDir = project.file("src/main/assets/dicts")
if (dictsDir.exists() && dictsDir.isDirectory) {
dictsDir.listFiles()?.forEach { file ->
if (file.name.endsWith(".dict") && file.name != "main_en-US.dict") {
patterns.add(file.name)
}
}
}
}
if (patterns.isNotEmpty()) {
variant.androidResources.ignoreAssetsPatterns = patterns
}
}
}

Expand All @@ -141,7 +152,7 @@ android {
path = File("src/main/jni/Android.mk")
}
}
// ndkVersion = "28.0.13004108"
ndkVersion = "28.0.13004108"

packaging {
jniLibs {
Expand Down Expand Up @@ -195,14 +206,6 @@ android {
// these orphaned strings are harmlessly stripped by R8 during minification.
disable += "ExtraTranslation"
}

sourceSets {
getByName("standardOptimised") {
java.srcDirs("src/standard/java")
res.srcDirs("src/standard/res")
manifest.srcFile("src/standard/AndroidManifest.xml")
}
}
}

dependencies {
Expand Down Expand Up @@ -231,12 +234,22 @@ dependencies {
// gemini ai proofreading
"standardImplementation"("com.google.ai.client.generativeai:generativeai:0.9.0")
"standardImplementation"("androidx.security:security-crypto:1.1.0-alpha06") // for encrypted API key storage
"standardOptimisedImplementation"("com.google.ai.client.generativeai:generativeai:0.9.0")
"standardOptimisedImplementation"("androidx.security:security-crypto:1.1.0-alpha06")

// local llm proofreading (offline)
// ONNX Runtime for T5 encoder-decoder grammar models
"offlineImplementation"("com.microsoft.onnxruntime:onnxruntime-android:1.17.3")
"offlineImplementation"("io.github.ljcamargo:llamacpp-kotlin:0.4.0")

// Force 16 KB page-aligned version of graphics-path
implementation("androidx.graphics:graphics-path:1.1.0")

// WorkManager — required by ML Kit Digital Ink plugin (loaded via DexClassLoader).
// ML Kit internally calls WorkManager.getInstance(context) using the host app context,
// so the host app must have WorkManagerInitializer registered in its manifest.
implementation("androidx.work:work-runtime-ktx:2.10.1")

// ML Kit Digital Ink Recognition — required by the handwriting plugin.
// ML Kit's internal asset manager and native library loader use the host app context,
// so the host app must compile and include the client library resources/libraries.
"standardImplementation"("com.google.mlkit:digital-ink-recognition:19.0.0")

// test
testImplementation(kotlin("test"))
Expand All @@ -255,11 +268,9 @@ dependencies {
"runTestsImplementation"("androidx.compose.ui:ui-test-manifest")
}

// Disable baseline/ART profile tasks to guarantee deterministic reproducible builds (except for standardOptimised)
// Disable baseline/ART profile tasks to guarantee deterministic reproducible builds
tasks.configureEach {
if (name.contains("ArtProfile", ignoreCase = true)) {
if (!name.contains("StandardOptimised", ignoreCase = true)) {
enabled = false
}
enabled = false
}
}
22 changes: 19 additions & 3 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,9 @@

# Keep java-llama.cpp classes
-keep class de.kherud.llama.** { *; }
-keep class org.nehuatl.llamacpp.** { *; }


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

# Fix correct service name
-keep class helium314.keyboard.latin.utils.ProofreadService { *; }
Expand All @@ -39,3 +38,20 @@
-dontwarn com.google.api.client.**
-dontwarn java.lang.management.**
-dontwarn org.joda.time.**

# Keep handwriting plugin interface and listener to prevent parameter removal/signature optimization
-keep interface helium314.keyboard.latin.handwriting.HandwritingRecognizer {
<methods>;
}
-keep interface helium314.keyboard.latin.handwriting.ModelDownloadListener {
<methods>;
}

# Keep ML Kit, GMS Tasks, and Firebase components for handwriting plugin dynamic linkage
-keep class com.google.mlkit.** { *; }
-keep class com.google.android.gms.tasks.** { *; }
-keep class com.google.firebase.components.** { *; }

# Keep Kotlin standard library for dynamically loaded plugins
# ponytail: keep kotlin stdlib classes to prevent NoSuchMethodError in plugin loading
-keep class kotlin.** { *; }
34 changes: 33 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">


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

<uses-permission android:name="android.permission.READ_USER_DICTIONARY" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WRITE_USER_DICTIONARY" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<!-- Optional: only requested at runtime when the user enables the "Auto-read OTP" feature. -->
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
Expand All @@ -27,6 +29,8 @@ SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
android:allowBackup="false"
android:defaultToDeviceProtectedStorage="true"
android:directBootAware="true"
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="true"
tools:remove="android:appComponentFactory"
tools:targetApi="p">

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

<!-- Disable WorkManager auto-initializer; App implements Configuration.Provider instead.
Required for ML Kit Digital Ink plugin loaded via DexClassLoader. -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>

<!-- Service for ML Kit component discovery.
Since the ML Kit libraries are loaded dynamically from the plugin APK,
we must register these in the host app manifest so ML Kit can find them. -->
<service
android:name="com.google.mlkit.common.internal.MlKitComponentDiscoveryService"
android:directBootAware="true"
android:exported="false">
<meta-data
android:name="com.google.firebase.components:com.google.mlkit.common.internal.CommonComponentRegistrar"
android:value="com.google.firebase.components.ComponentRegistrar" />
<meta-data
android:name="com.google.firebase.components:com.google.mlkit.vision.digitalink.internal.DigitalInkRecognitionRegistrar"
android:value="com.google.firebase.components.ComponentRegistrar" />
</service>

</application>

<queries>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[
[
{ "label": "alpha", "width": 0.15 },
{ "label": "clear_handwriting", "width": 0.15 },
{ "label": "space", "width": -1 },
{ "label": "delete", "width": 0.15 }
]
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[
[
{ "label": "alpha", "width": 0.15 },
{ "label": "clear_handwriting", "width": 0.15 },
{ "label": "space", "width": -1 },
{ "label": "delete", "width": 0.15 },
{ "label": "action", "width": 0.15 }
]
]
Loading