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
64 changes: 64 additions & 0 deletions .github/workflows/build-release-apk.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Build Release APKs

on:
workflow_dispatch:
push:
tags:
- 'v*'

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4

- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3

- name: Decode Keystore
env:
RELEASE_KEYSTORE: ${{ secrets.RELEASE_KEYSTORE }}
if: ${{ env.RELEASE_KEYSTORE != '' }}
run: |
echo "${{ secrets.RELEASE_KEYSTORE }}" | base64 -d > leantype-release.jks
echo "keyAlias=${{ secrets.RELEASE_KEY_ALIAS }}" > keystore.properties
echo "keyPassword=${{ secrets.RELEASE_KEY_PASSWORD }}" >> keystore.properties
echo "storeFile=leantype-release.jks" >> keystore.properties
echo "storePassword=${{ secrets.RELEASE_STORE_PASSWORD }}" >> keystore.properties

- name: Grant execute permission for gradlew
run: chmod +x gradlew

- name: Build Release APKs
run: ./gradlew assembleStandardRelease assembleStandardfullRelease assembleOfflineRelease assembleOfflineliteRelease

- name: Generate Release Notes
# ponytail: generate release notes from changelog during build
run: python3 docs/scripts/generate_release_notes.py

- name: Upload APKs and Release Notes
uses: actions/upload-artifact@v4
with:
name: LeanType-Release-APKs
path: |
app/build/outputs/apk/**/*.apk
docs/releasenote/release_notes_v*.md

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/v')
with:
body_path: docs/releasenote/release_notes_temp.md
files: |
app/build/outputs/apk/standard/release/*.apk
app/build/outputs/apk/standardfull/release/*.apk
app/build/outputs/apk/offline/release/*.apk
app/build/outputs/apk/offlinelite/release/*.apk
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,11 @@ docs/superpowers/
.agents
.kilo/
.antigravitycli/

.env

# AI agent config (personal, not shared)
.pi/

# Temporary generated files
docs/releasenote/release_notes_temp.md
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.9** (from v3.8.3, including v3.8.7/v3.8.8 and one post-tag docs/badge
commit) — the source of the handwriting, llama.cpp/GGUF, dynamic downloader, text-editing mode, 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
23 changes: 15 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,28 @@ 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.
- **✍️ Text editing mode** - A toolbar key opens a text-editing overlay for selection, cursor movement, and clipboard actions.
- **🎨 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 +78,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
64 changes: 44 additions & 20 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ 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")

Expand All @@ -32,19 +32,20 @@ android {
}
}

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

flavorDimensions += "privacy"
productFlavors {
create("standard") {
dimension = "privacy"
minSdk = 23
}
create("standardOptimised") {
create("standardfull") {
dimension = "privacy"
minSdk = 23
}
create("offline") {
dimension = "privacy"
applicationIdSuffix = ".offline"
minSdk = 26
}
create("offlinelite") {
dimension = "privacy"
Expand Down Expand Up @@ -105,9 +106,9 @@ android {
val flavor = productFlavors.firstOrNull()?.name ?: ""
val number = when(flavor) {
"standard" -> "1"
"standardfull" -> "1"
"offline" -> "2"
"offlinelite" -> "3"
"standardOptimised" -> "4"
else -> ""
}
if (number.isNotEmpty()) {
Expand All @@ -120,13 +121,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" || variant.flavorName == "standardfull") {
// 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 +157,7 @@ android {
path = File("src/main/jni/Android.mk")
}
}
// ndkVersion = "28.0.13004108"
ndkVersion = "28.0.13004108"

packaging {
jniLibs {
Expand Down Expand Up @@ -197,10 +213,8 @@ android {
}

sourceSets {
getByName("standardOptimised") {
getByName("standardfull") {
java.srcDirs("src/standard/java")
res.srcDirs("src/standard/res")
manifest.srcFile("src/standard/AndroidManifest.xml")
}
}
}
Expand Down Expand Up @@ -231,12 +245,24 @@ 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")
"standardfullImplementation"("com.google.ai.client.generativeai:generativeai:0.9.0")
"standardfullImplementation"("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.
"standardfullImplementation"("com.google.mlkit:digital-ink-recognition:19.0.0")

// test
testImplementation(kotlin("test"))
Expand All @@ -255,11 +281,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.** { *; }
Loading