Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
123 commits
Select commit Hold shift + click to select a range
2d8a2d7
chore: update README badges [skip ci]
github-actions[bot] Aug 30, 2026
b60796a
chore: update README badges [skip ci]
github-actions[bot] Aug 31, 2026
e49574a
chore: update README badges [skip ci]
github-actions[bot] Sep 1, 2026
c24efc1
chore: update README badges [skip ci]
github-actions[bot] Sep 2, 2026
afa02a6
chore: update README badges [skip ci]
github-actions[bot] Sep 3, 2026
f768037
chore: update README badges [skip ci]
github-actions[bot] Sep 4, 2026
ec749d8
feat(ocr): in-keyboard offline OCR camera viewfinder and screenshot t…
LeanBitLab Sep 3, 2026
f71deb4
fix(ocr): reposition flash and close buttons to top bar and hide stri…
LeanBitLab Sep 3, 2026
02c42c3
fix(ocr): group flash and close buttons into top-end strap
LeanBitLab Sep 3, 2026
4b18995
fix(ocr): enforce exact keyboardHeight onMeasure to prevent PreviewVi…
LeanBitLab Sep 3, 2026
7826182
fix(ocr): enforce explicit LayoutParams height on OCR views in Keyboa…
LeanBitLab Sep 3, 2026
89c5094
fix(ocr): place flash on top-left and close on top-right of camera vi…
LeanBitLab Sep 3, 2026
e0773be
fix(ocr): add btn_circle_translucent drawable and fix icon rendering …
LeanBitLab Sep 3, 2026
628a3aa
feat(ocr): align flash, shutter, and close into unified bottom contro…
LeanBitLab Sep 3, 2026
fca1b4f
feat(ocr): move result actions to dedicated ocr toolbar strip and max…
LeanBitLab Sep 3, 2026
aab0382
feat(ocr): add two-tone loading animation and natural sequential tool…
LeanBitLab Sep 3, 2026
74668c7
feat(ocr): black & white loading ring, fixed-width non-spanned keys, …
LeanBitLab Sep 3, 2026
2e3d622
fix(ocr): apply setToolbarButtonActivatedState and exact non-spanned …
LeanBitLab Sep 3, 2026
95a9278
feat(ocr): integrate auto-spanning and toolbar keys alignment into OC…
LeanBitLab Sep 3, 2026
de00d55
refactor(ocr): simplify plugin loader and preferences to unified full…
LeanBitLab Sep 3, 2026
71e1ed2
fix(suggestion): restore standard Screenshot chip label and remove re…
LeanBitLab Sep 3, 2026
c8c2b18
feat(suggestion): render screenshot as clean bordered thumbnail witho…
LeanBitLab Sep 3, 2026
07d1602
feat(suggestion): wider center-cropped screenshot thumbnail and dedic…
LeanBitLab Sep 3, 2026
3570768
feat(ocr): unified ocr icon, pulsing controls border loading animatio…
LeanBitLab Sep 3, 2026
ca021e3
fix(toolbar): reliable swipe down from toolbar to close keyboard via …
LeanBitLab Sep 3, 2026
aeecfb9
style(toolbar): replace asymmetric hint dot with centered modern micr…
LeanBitLab Sep 3, 2026
b6990e2
fix(toolbar): elevate base icon and position high-contrast micro-pill…
LeanBitLab Sep 3, 2026
6f44a10
style(toolbar): set micro-pill contrast to 50% opacity of keys
LeanBitLab Sep 3, 2026
51bab74
style(toolbar): refine micro-pill opacity to 25% contrast
LeanBitLab Sep 3, 2026
56cc3a0
docs(settings): update toolbar long press hint preference title and s…
LeanBitLab Sep 3, 2026
c8bce78
feat(settings): expand OCR customization settings with whitespace tri…
LeanBitLab Sep 3, 2026
3719bfd
feat(ocr): advanced text extraction & formatting options including ca…
LeanBitLab Sep 3, 2026
c9e9a7b
style(voice): standardize permissions card in VoiceSettingsScreen to …
LeanBitLab Sep 3, 2026
807ed4e
style(voice): place Plugin Management first and Permissions card seco…
LeanBitLab Sep 3, 2026
1b42481
Optimize battery & thermal footprint: scope screenshot observer & cac…
LeanBitLab Sep 4, 2026
be1cf44
fix(voice): prevent crashes and gracefully handle missing voice input…
LeanBitLab Sep 4, 2026
f02cb8a
fix(haptics): enable haptic feedback on key repeat for backspace and …
LeanBitLab Sep 4, 2026
644282b
fix(ai): add truncation safeguard to prevent data loss and ensure cor…
LeanBitLab Sep 4, 2026
044ae19
fix(autocorrect): prevent learned capitalized words from replacing re…
LeanBitLab Sep 4, 2026
ad05c6a
fix(expander): support immediate text expansion when backspacing into…
LeanBitLab Sep 4, 2026
add4810
fix(theme): guarantee high-contrast dynamic colors on light themes (#…
LeanBitLab Sep 4, 2026
eba2d5d
fix(gesture): suppress emoji suggestions during gesture typing (#470)
LeanBitLab Sep 4, 2026
47fcb71
fix(autocap): improve auto-capitalization in chat and multiline field…
LeanBitLab Sep 4, 2026
cea7b22
feat(sound): implement custom sound pack repo architecture and lifecy…
LeanBitLab Sep 4, 2026
f31718c
feat(sound): unbundle asset sound packs, move sound settings to plugi…
LeanBitLab Sep 4, 2026
7e8eb12
fix(sound): update remote sound pack catalog with 10 synthesized packs
LeanBitLab Sep 4, 2026
503a489
feat(sound): add 6 musical instrument packs to fallback catalog
LeanBitLab Sep 4, 2026
0d7ae63
fix(sound): use compact, minimal names and descriptions for sound packs
LeanBitLab Sep 4, 2026
a2b88bb
fix(sound): synchronize fallback catalog metadata with physical model…
LeanBitLab Sep 4, 2026
7e3b3f6
fix(sound): auto-cleanup legacy sound pack directories on duplicate i…
LeanBitLab Sep 4, 2026
0c38ec2
feat(settings): move sound pack settings to dedicated sound screen un…
LeanBitLab Sep 4, 2026
cf79daf
chore(ui): shorten keypress audio title in plugins and settings
LeanBitLab Sep 4, 2026
8cddc6a
feat(sound): comprehensive keypress audio customization suite
LeanBitLab Sep 4, 2026
2346103
fix: commit typed word and finish composing text on editor actions
LeanBitLab Sep 4, 2026
0a39ac8
fix(suggest): fix next-word prediction ngram context and add autocorr…
LeanBitLab Sep 4, 2026
02a05c4
fix(keyboard): restore fast double-tap on Shift to lock Caps Lock
LeanBitLab Sep 4, 2026
022f8c3
feat(ai): add configurable cloud AI max tokens setting and fix offlin…
LeanBitLab Sep 4, 2026
ce81018
docs: clarify screenshot suggestion dependency for OCR screenshot tex…
LeanBitLab Sep 4, 2026
8e82b6a
fix(ocr): add toast feedback and safe intent handling for offline plu…
LeanBitLab Sep 4, 2026
3d27e66
feat(calculator): add full-featured calculator keyboard mode and inli…
LeanBitLab Sep 4, 2026
26b2c78
fix(calculator): resolve toolbar key migration, view insets, and inli…
LeanBitLab Sep 4, 2026
fb71bde
fix(calculator): resolve service init NPE and view cast types
LeanBitLab Sep 4, 2026
0073d24
fix(ocr): retain plugin classloader to prevent UnsatisfiedLinkError o…
LeanBitLab Sep 4, 2026
d17d17f
refactor(calculator): remove dedicated calculator mode and retain inl…
LeanBitLab Sep 4, 2026
41a7c4c
feat(calculator): instant math suggestions on equals and add text cor…
LeanBitLab Sep 4, 2026
120ff9e
refactor(calculator): display clean answer in suggestion chip without…
LeanBitLab Sep 4, 2026
d9e5f83
refactor(suggestions): unify screenshot and ocr suggestion chip spaci…
LeanBitLab Sep 4, 2026
cbda86f
chore(release): bump version to 4.1.9 (4109)
LeanBitLab Sep 4, 2026
03d172c
docs: update README and FEATURES.md with OCR, inline math, and sound …
LeanBitLab Sep 4, 2026
ad8f359
docs: enrich v4.1.9 release notes with full bug fixes and issue refer…
LeanBitLab Sep 4, 2026
0908b05
fix(voice): use AudioSource.MIC with DEFAULT fallback for AudioRecord
LeanBitLab Sep 4, 2026
5d5b3ea
Revert "fix(voice): prevent crashes and gracefully handle missing voi…
LeanBitLab Sep 4, 2026
432a7de
fix(voice): add MODIFY_AUDIO_SETTINGS permission and context attribut…
LeanBitLab Sep 4, 2026
8a2104a
docs: update stars count badge color to red
LeanBitLab Sep 5, 2026
f671ddb
chore: update README badges [skip ci]
github-actions[bot] Sep 5, 2026
86804d0
fix(ocr): add ProGuard keep rules and invalidate cached classloader o…
LeanBitLab Sep 5, 2026
81171a6
feat(ai): show toast notifications when AI output is truncated
LeanBitLab Sep 5, 2026
1383390
chore: bump version to 4.2.0 (4200)
LeanBitLab Sep 5, 2026
0ab4172
Merge LeanBitLab/LeanType v4.2.0 into stabilized dev
AsafMah Sep 5, 2026
e84c415
fix(plugins): preserve offline and nouserlib addon boundaries
AsafMah Sep 5, 2026
8073222
fix(suggest): preserve explicit short shortcut autocorrection
AsafMah Sep 5, 2026
4273800
fix(settings): retain sound search after plugin screen migration
AsafMah Sep 5, 2026
69fba04
fix(input): honor editor exclusions when forcing autocaps
AsafMah Sep 5, 2026
b1b5e8e
fix(ai): reject token-truncated cloud responses before replacement
AsafMah Sep 5, 2026
70ae2c7
fix(ai): honor cloud token limits for Gemini requests
AsafMah Sep 5, 2026
f3dec97
fix(keyboard): preserve taps drafts and microphone privacy
AsafMah Sep 5, 2026
b376f5a
docs: record stabilized base and pending v4.2.0 integration
AsafMah Sep 5, 2026
2525181
Merge approved v4.2 integration corrections
AsafMah Sep 5, 2026
4cb5c4a
ci: run gates when their inputs or workflows change
AsafMah Sep 5, 2026
842cc5c
fix(suggest): honor the inline math preference
AsafMah Sep 5, 2026
31bc813
fix(settings): gate AI and OCR entrypoints by capability
AsafMah Sep 5, 2026
3e2ce86
docs: record reviewed corrections and remaining AI risks
AsafMah Sep 5, 2026
c48f27d
fix(import): validate sound paths and stage OCR replacements
AsafMah Sep 5, 2026
a74b8dd
feat: add recognition script preference for universal OCR plugin
LeanBitLab Sep 5, 2026
7aaae62
fix(ocr): normalize semver comparison in LoadOcrPluginPreference to p…
LeanBitLab Sep 5, 2026
58db0c3
fix(ocr): bind asynchronous work to the active input session
AsafMah Sep 5, 2026
9f1951d
fix(math): guard private editors and replace exact expressions
AsafMah Sep 5, 2026
9b006c8
test(ocr): cover preference workflows and document reliability fixes
AsafMah Sep 5, 2026
363db3c
fix(settings): complete validated restores asynchronously (#152)
AsafMah Sep 5, 2026
fefbafa
fix(dictionary): publish mutation-aware suggestion caches (#152)
AsafMah Sep 5, 2026
a487f32
feat: automatically restart process on plugin load/removal to prevent…
LeanBitLab Sep 5, 2026
580d1a1
Enhance OCR camera viewfinder UX: larger shutter, prominent processin…
LeanBitLab Sep 5, 2026
292564c
Fix OCR viewfinder touch leakage and remove unwanted processing statu…
LeanBitLab Sep 5, 2026
152f28b
feat(ocr): expand camera viewfinder height for improved capture usabi…
LeanBitLab Sep 5, 2026
b662fd3
chore: update README badges [skip ci]
github-actions[bot] Sep 6, 2026
a8d3580
Integrate completed keyboard reliability corrections
AsafMah Sep 6, 2026
938997b
Integrate completed settings and dictionary state corrections
AsafMah Sep 6, 2026
b576072
Remove dead two-thumb and duplicate settings scaffolding
AsafMah Sep 6, 2026
4db01ca
Record the local stabilization candidate and remaining limits
AsafMah Sep 6, 2026
2a3a1e4
Record requested EXP phone installation
AsafMah Sep 6, 2026
0e52846
fix(ai): bind results and queued delivery to their editor request (#154)
AsafMah Sep 6, 2026
790cd7e
fix(ai): reconcile offline translation language preferences (#154)
AsafMah Sep 6, 2026
79d659f
fix(ai): serialize offline generation and isolate request feedback (#…
AsafMah Sep 6, 2026
1839c4c
Use canonical suggestion words for Add and Block actions (#40)
AsafMah Sep 6, 2026
38fe4ab
fix(ai): report rejected editor actions and settle current-session lo…
AsafMah Sep 6, 2026
0304f22
Record AI stabilization and canonical suggestion curation progress
AsafMah Sep 6, 2026
84099d3
Record AI-corrected EXP candidate installation
AsafMah Sep 6, 2026
52567c0
chore: update README badges [skip ci]
github-actions[bot] Sep 7, 2026
f1c0fb7
Merge latest LeanBitLab upstream at 52567c0
AsafMah Sep 7, 2026
b039822
Retire retained upstream engines and distributions
AsafMah Sep 8, 2026
b1694d8
Record initial feedback on upstream-aligned EXP
AsafMah Sep 8, 2026
7b8527b
Bound Robolectric test worker memory for the complete suite
AsafMah Sep 8, 2026
143416b
Share and reset the Robolectric input-method fixture
AsafMah Sep 8, 2026
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
4 changes: 4 additions & 0 deletions .github/workflows/build-test-auto.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ on:
- '.github/workflows/release.yml'
- 'tools/check_fork_invariants.py'
- 'tools/check_apk_invariants.py'
- 'tools/check_test_results.py'
- 'tools/test_baselines/**'
- 'tools/tests/**'
- 'fastlane/metadata/android/en-US/changelogs/**'
push:
Expand All @@ -30,6 +32,8 @@ on:
- '.github/workflows/release.yml'
- 'tools/check_fork_invariants.py'
- 'tools/check_apk_invariants.py'
- 'tools/check_test_results.py'
- 'tools/test_baselines/**'
- 'tools/tests/**'
- 'fastlane/metadata/android/en-US/changelogs/**'
workflow_dispatch:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/native-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ name: Native tests
on:
push:
branches: [dev]
paths: ['app/src/main/jni/**']
paths: ['app/src/main/jni/**', '.github/workflows/native-tests.yml']
pull_request:
branches: [dev, main]
paths: ['app/src/main/jni/**']
paths: ['app/src/main/jni/**', '.github/workflows/native-tests.yml']

jobs:
native-host-tests:
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Builds the signed release APKs (all four flavors) and drafts a GitHub Release.
# Builds the signed release APKs (all three flavors) and drafts a GitHub Release.
#
# Triggers on pushing a version tag (e.g. `git tag v0.1.0 && git push origin v0.1.0`),
# or manually via "Run workflow" (workflow_dispatch) for a signing/build dry run that
Expand Down Expand Up @@ -60,7 +60,7 @@ jobs:
EOF

- name: Build signed release APKs (all flavors)
run: ./gradlew :app:assembleStandardRelease :app:assembleStandardfullRelease :app:assembleOfflineRelease :app:assembleOfflineliteRelease
run: ./gradlew :app:assembleStandardRelease :app:assembleStandardfullRelease :app:assembleOfflineRelease

- name: Verify packaged LeanTypeDual invariants
run: |
Expand All @@ -77,14 +77,14 @@ jobs:
for apk in app/build/outputs/apk/*/release/*.apk; do
"$APKSIGNER" verify --verbose --print-certs "$apk"
case "$apk" in
*-standard-release.apk|*-standardfull-release.apk|*-offlinelite-release.apk)
*-standard-release.apk|*-standardfull-release.apk|*-offline-release.apk)
"$APKSIGNER" verify --verbose --min-sdk-version 21 --max-sdk-version 23 "$apk"
legacy_count=$((legacy_count + 1))
;;
esac
count=$((count + 1))
done
test "$count" -eq 4
test "$count" -eq 3
test "$legacy_count" -eq 3

- name: Generate release notes
Expand Down
20 changes: 10 additions & 10 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Repository Guidelines

## Project Overview
LeanType is an Android keyboard (an `InputMethodService` app), forked from HeliBoard/OpenBoard/AOSP LatinIME. On top of the upstream keyboard it adds AI proofreading & translation (cloud and on-device ONNX), Nintype-style two-thumb typing, custom AI toolbar keys, and a floating keyboard. The legacy input engine is **Java**; newer logic, settings, and AI code are **Kotlin**. Settings UI is **Jetpack Compose**. A native **C++** engine (under `app/src/main/jni/`) does dictionary lookup and gesture/glide scoring.
LeanTypeDual is an Android keyboard (an `InputMethodService` app), forked from LeanBitLab/LeanType and ultimately HeliBoard/OpenBoard/AOSP LatinIME. Its main fork addition is Nintype-style two-thumb typing. AI, OCR, handwriting and the floating keyboard are inherited upstream features, not evidence of fork authorship. The legacy input engine is **Java**; newer logic, settings, and AI code are **Kotlin**. Settings UI is **Jetpack Compose**. The bundled native **C++** engine (under `app/src/main/jni/`) handles dictionaries; gesture recognition requires a separately supplied compatible native library.

**Fork lineage & "upstream":** the chain is HeliBoard (`Helium314/HeliBoard`, the original) → **`LeanBitLab/LeanType`** (a fork of HeliBoard) → **this repo, `AsafMah/LeanType`** (a fork of LeanBitLab/LeanType). When the maintainer says **"upstream" they mean `LeanBitLab/LeanType`** (`upstream/main`) — NOT HeliBoard. This fork ships as its own distinct, installable app, **"LeanTypeDual"** (its own `applicationId`, so it installs *alongside* the upstream LeanType instead of colliding with it). "Make it distinct" therefore means distinct from `LeanBitLab/LeanType`, not from HeliBoard.

Expand All @@ -24,20 +24,20 @@ Strict **view → logic → engine** split.
- `app/src/main/jni/` — native C++ dictionary/suggestion engine (`Android.mk`, `ndkBuild`)
- `app/src/main/assets/layouts/` — layout files (subfolders are `LayoutType`: `main/`, `symbols/`, `functional/`)
- `app/src/main/assets/locale_key_texts/` — per-locale popup keys (`en.txt`, …)
- `app/src/{standard,offline,offlinelite}/` — flavor-only sources (e.g. three `ProofreadService.kt` impls)
- `app/src/{standard,offline}/` — cloud and plugin-based offline sources; `standardfull` reuses standard sources. The retired offlinelite source set is forbidden by the product gate.
- `app/src/test/` — JVM unit tests · `docs/` · `tools/`

## Development Commands
Requires **JDK 17 or 21** and the Android SDK. On Windows use `gradlew.bat` and set `JAVA_HOME` + `ANDROID_HOME` (e.g. `C:\Android\Sdk`).

```bash
# Build an APK (per flavor)
./gradlew :app:assembleStandardDebug # also assembleOfflineDebug, assembleOfflineliteDebug
./gradlew :app:assembleStandardDebug # also assembleStandardfullDebug, assembleOfflineDebug
# Fast CI compile check (no APK) — what PR CI runs
./gradlew compileOfflineRunTestsKotlin
# Fast fork-identity/product gate (run before and after upstream merges)
python tools/check_fork_invariants.py
# Packaged release gate (after all four release APKs are assembled)
# Packaged release gate (after all three release APKs are assembled)
python tools/check_apk_invariants.py --apk-dir app/build/outputs/apk
# Unit tests for one flavor
./gradlew :app:testOfflineDebugUnitTest
Expand All @@ -62,7 +62,7 @@ $env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.7-hotspot"
5. `settings/screens/<Screen>.kt` — a `Setting{…}` entry added to the screen list
`SettingsContainer` auto-aggregates the per-screen lists.
- **State / config access:** `Settings.getValues()` returns a cached `SettingsValues` (read once, not per keystroke). Some cross-pointer state is `static` in `PointerTracker` (`sInGesture`, aggregated pointers).
- **Flavor isolation:** prefer **source-set separation** (`app/src/standard` vs `offline` vs `offlinelite`) over `BuildConfig.FLAVOR` checks. **Never** add the `INTERNET` permission to the `offline`/`offlinelite` manifests — all network activity is `standard`-only and opt-in.
- **Flavor isolation:** prefer **source-set separation** (`app/src/standard` vs `offline`) over `BuildConfig.FLAVOR` checks. **Never** add the `INTERNET` permission to the `offline` manifest. Network features are opt-in and standard/standardfull-only; Offline retains manual plugin/dictionary import.
- **Performance:** the key-input and suggestion paths run on the main thread; avoid allocations in hot paths.
- **IME dialogs:** an `AlertDialog` `EditText` cannot reliably receive typed input inside the IME process — intercept `onCodeInput`/`onTextInput` into a `TextView` instead (see clipboard/emoji search modes).

Expand All @@ -71,7 +71,7 @@ $env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.7-hotspot"
- Input core: `latin/inputlogic/InputLogic.java`, `keyboard/PointerTracker.java`, `latin/RichInputConnection.java`
- Dictionaries / suggestions: `latin/DictionaryFacilitatorImpl.kt`, `latin/Suggest.kt`, `app/src/main/jni/`
- Settings: `latin/settings/Settings.java`, `Defaults.kt`, `SettingsValues.java`, `settings/screens/*.kt`
- Flavor AI: `app/src/standard/.../ProofreadService.kt` (Gemini), `app/src/offline/.../ProofreadService.kt` (ONNX)
- Flavor AI: `app/src/standard/.../ProofreadService.kt` (cloud providers), `app/src/offline/.../ProofreadService.kt` (upstream Offline AI plugin)
- Build: `app/build.gradle.kts`, `build.gradle.kts`, `gradle.properties`, `app/proguard-rules.pro`
- Docs: `docs/FEATURES.md`, `docs/TWO_THUMB_TYPING_INTERNALS.md`, `docs/IMPROVEMENT_PLAN.md`, `layouts.md`, `CONTRIBUTING.md`

Expand All @@ -81,16 +81,16 @@ $env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.7-hotspot"
- **Native:** ABIs `armeabi-v7a`, `arm64-v8a`; built via `ndkBuild` (`app/src/main/jni/Android.mk`).
- **Flavors** (dimension `privacy`, appId base `com.asafmah.leantypedual`):
- `standard` — cloud AI (Gemini, `generativeai`), has `INTERNET`.
- `standardfull` — cloud AI plus handwriting, has `INTERNET`.
- `offline` — on-device llama.cpp / GGUF, **no** `INTERNET`; appId `+.offline`, minSdk 26.
- `offlinelite` — no AI, smallest; **no** `INTERNET`; appId `+.offlinelite`.
- `standardfull` — upstream Full flavor, standard sources and plugin architecture, has `INTERNET`.
- `offline` — optional upstream Offline AI plugin / GGUF, **no** `INTERNET`; appId `+.offline`, minSdk 21 (AI requires API 26).
All three exclude packaged dictionaries and use upstream's optional plugins. No bundled llama backend, retained Java gesture fallback, or separate offlinelite distribution remains. Existing imported dictionaries, models and preferences are not deleted.
- **Build types:** `debug` (no minify, `+.debug`), `release` (minify + shrink + signed via `keystore.properties`), `runTests` (CI variant that skips known-failing tests), `debugNoMinify` (fast IDE builds).
- **CI:** `.github/workflows/build-test-auto.yml` runs `compileOfflineRunTestsKotlin` on PRs touching `app/src/main/java**`; `build-debug-apk.yml` runs `assembleDebug` on manual dispatch. Release chores live in `tools/release.py`.

## Testing & QA
- **JVM-only** (no `androidTest`/device): JUnit4 + **Robolectric 4.14.1** (simulates `LatinIME`/`Context`/prefs/key events on the JVM) + **Mockito 5.17.0**. Tests live in `app/src/test/java/helium314/keyboard/`. `testOptions.unitTests.isIncludeAndroidResources = true`.
- **Run:** `./gradlew :app:testOfflineDebugUnitTest` (add `--tests "*ClassName"` for one class).
- **Upstream-merge gates:** run `python tools/check_fork_invariants.py` before and after resolving an upstream merge. Unit-test CI uses it as a fast source/configuration prefilter and fails with the specific LeanTypeDual invariant that was lost. Release CI also runs `tools/check_apk_invariants.py` after assembling all four APKs to verify the effective package IDs, minSdk values, INTERNET permissions, recursive dictionary contents, and exact artifact set. Their fixture/mutation tests run via `python -m unittest discover -s tools/tests`.
- **Upstream-merge gates:** run `python tools/check_fork_invariants.py` before and after resolving an upstream merge. The gate protects identity, privacy and dual-thumb integration while rejecting retired backends/distributions. Release CI runs `tools/check_apk_invariants.py` after assembling all three APKs to verify effective package IDs, minSdk values, INTERNET permissions, absence of bundled dictionaries, and the exact artifact set. Their fixture/mutation tests run via `python -m unittest discover -s tools/tests`.
- **Key tests:** `InputLogicTest.kt` (typing/autocorrect/combining-mode/Hangul), `SuggestTest.kt`, `WordComposerTest.java`, `DictionaryGroupTest.kt` (reflection + Mockito on the package-internal `DictionaryGroup`), `SettingsContainerTest.kt` (settings wiring), `KeyboardParserTest.kt`, `ClipboardDaoTest.kt`.
- **Conventions:** `@Test`; method names use camelCase or backtick form; obtain `Context` via Robolectric; package-internal classes are exercised via reflection (`Class.forName(...).declaredConstructors`).
- **Known failures:** the full debug unit suite has ~11 pre-existing failures (in `KeyboardParserTest`, `XLinkTest`, `StringUtilsTest` emoji, and `InputLogicTest` Hangul/autocorrect-revert/autospace-indicator) that are environment/data-dependent and usually unrelated to a change. The `runTests` build type exists to skip these on CI. **Verify a change by diffing failures against an `origin/main` baseline run, not by absolute pass count.**
Expand Down
Loading
Loading