Last updated: 2026-06-04, against the v1.8.246 codebase.
This file is the fast architectural map for contributors. It is intentionally
shorter than ROADMAP.md and more code-oriented than PROJECT_CONTEXT.md.
SwiftFloris is an Android input method editor (IME) forked from FlorisBoard and extended toward SwiftKey-class offline typing. The main app has three load-bearing constraints:
- No
INTERNETor equivalent network permission in:app. - Apache-2.0-compatible code and assets in
:app. - No closed-source native blobs.
Features that need network access, incompatible licenses, or large optional native runtimes belong in isolated addon APKs, not in the base app.
The active Gradle modules are declared in settings.gradle.kts.
| Module | Role |
|---|---|
:app |
IME service, Settings app, Android manifest, resources, addon facades, tests |
:lib:android |
Android utility extensions |
:lib:color |
Color utilities |
:lib:compose |
Shared Compose resource, layout, and UI helpers |
:lib:kotlin |
Pure Kotlin utility code |
:lib:snygg |
Snygg theme parser and runtime engine |
:benchmark |
AndroidX Macrobenchmark module plus adb-driven benchmark harness target |
:benchmark is included in settings for Macrobenchmark and adb performance
harnesses. Native runtimes for optional capabilities (LiteRT-LM smart compose,
Bergamot offline NMT, librime CJK input, ML Kit Digital Ink handwriting, Vosk
voice) ship as out-of-tree signed addon APKs per the addon enrolment contract
(AddonContract.Action.REGISTER_*); the base APK does not carry a native
module. The previous :lib:native placeholder + libnative/ Rust scaffold
were dropped in v1.8.185.
| Entrypoint | File | Responsibility |
|---|---|---|
| Application | app/src/main/kotlin/dev/patrickgold/florisboard/FlorisApplication.kt |
Installs crash handling, EmojiCompat, preference datastore, extension manager, clipboard manager, dictionary manager, adaptive touch model, and global manager singletons |
| IME service | app/src/main/kotlin/dev/patrickgold/florisboard/FlorisImeService.kt |
Owns the keyboard lifecycle, input connection, inline suggestions, IME window, voice switching, and settings launch bridge |
| Settings activity | app/src/main/kotlin/dev/patrickgold/florisboard/app/FlorisAppActivity.kt |
Hosts the Compose Settings app, deep links, import intents, setup flow, preview field, and Android edge-to-edge behavior |
| Spell checker service | app/src/main/AndroidManifest.xml |
Exposes the Android text-service binding |
| Tasker receiver | app/src/main/AndroidManifest.xml and ime/tasker/ |
Signature-protected automation endpoint for insert text, insert clip, switch layout, and trigger voice |
| Content providers | ime/clipboard/provider/ClipboardMediaProvider.kt, ime/media/sticker/StickerMediaProvider.kt |
Grant read-only rich-content URIs for clipboard media and stickers |
Most long-lived managers are lazy properties on FlorisApplication and are
accessed through context extension helpers such as context.keyboardManager(),
context.nlpManager(), and context.editorInstance().
Preferences are modeled in
app/src/main/kotlin/dev/patrickgold/florisboard/app/AppPrefs.kt with JetPref.
The generated datastore is initialized from FlorisApplication.init().
Use the existing preference groups when adding settings. A new top-level
preference class is appropriate only when the feature has a distinct lifecycle
or owner, such as clipboard, correction, sync, voice, or sticker.
Primary app code lives under
app/src/main/kotlin/dev/patrickgold/florisboard/.
| Package | Main responsibility |
|---|---|
app/ |
Settings activity, navigation, setup, preference model, import screens |
app/settings/ |
Compose Settings screens grouped by product area, plus pure policies for settings/editor validation |
ime/core/ |
Subtype and active-language state |
ime/editor/ |
Editor info, current input connection, rich-content commits, field guards |
ime/input/ |
Feedback, capitalization, haptics, input state |
ime/keyboard/ |
Keyboard orchestration and action dispatch |
ime/text/ |
Text keyboard layout, key data, gesture typing, popup mapping |
ime/nlp/ |
Suggestions, ranking, language blending, priors, inline autofill |
ime/dictionary/ |
Floris/user dictionary, encryption-adjacent storage hooks, dictionary import/export |
ime/bidi/, ime/indic/, ime/geez/, ime/cjk/ |
Script shaping, normalization, transliteration, and CJK facades |
ime/hardware/ |
Windows KLC, Keyman LDML, macOS .keylayout, and runtime hardware-keyboard mapping |
ime/media/ |
Emoji, emoticons, stickers, user-imported sticker folder support |
ime/clipboard/ |
Internal clipboard history, media sharing, content-provider bridge |
ime/voice/ |
FUTO Voice Input handoff, Vosk fallback, model-management surfaces |
ime/theme/ |
Theme loading, per-app accents, wallpaper receiver, Snygg integration |
ime/smartbar/ |
Candidate strip, quick actions, smartbar layout |
ime/smartcompose/, ime/translate/, ime/handwriting/, ime/passkey/, ime/wordstyles/ |
Offline-first facades for larger optional capabilities |
ime/addon/ |
Addon registry, manifest contracts, package enumeration |
ime/mcp/ |
Local MCP daemon discovery, AIDL client, service lifecycle, settings surface |
ime/sync/ |
User-chosen local sync channels and QR/manual pairing helpers |
ime/window/, ime/landscapeinput/, ime/sheet/, ime/popup/ |
IME window, extracted input, sheets, and key popups |
lib/ |
Shared app-local utility code |
The high-level typing path is:
- Android calls
FlorisImeServicewith editor lifecycle events. EditorInstancetracks the activeEditorInfo, input connection, selected text, MIME capabilities, and sensitive-field state.KeyboardManagerreceives key events from the Compose keyboard, hardware keyboard hooks, or automation entry points.- Text-key behavior delegates into
ime/text/,ime/input/,ime/keyboardpolicies, andime/nlp/depending on whether the action is raw text, a correction, a suggestion, a gesture trace, or rich content. NlpManagergathers dictionary, priors, user learning, multilingual scoring, and field guards beforeCandidateAutoCommitPolicyand the rankers decide auto-commit, quick-prediction, and rejection behavior.- Commits return through
EditorInstanceinto the AndroidInputConnection.
Keep deterministic rules pure where practical. The roadmap and improvement plan prefer extracting policy from Android-bound managers into JVM-testable helpers.
Emoji and sticker UI is under ime/media/. Clipboard media and stickers use
content-provider URIs instead of broad media-library permissions.
- Clipboard media is served through
ClipboardMediaProvider. - Bundled and imported stickers are served through
StickerMediaProvider. - User-imported sticker folders use a persisted, user-selected SAF tree URI and remain read-only unless a later explicit write flow is implemented.
- Rich content is committed through
EditorInstance.commitRichContent(...)after checking the target editor's declared MIME support.
The addon system is the architectural answer for optional or risky capability: large models, network-capable helpers, incompatible licenses, native runtimes, and language/theme/dictionary packs that should not live in the base APK.
The manifest declares the signature-protected
dev.patrickgold.florisboard.permission.REGISTER_ADDON permission and package
visibility queries for addon registration actions. Addon specs live under
docs/addons/.
Main-app code may define stable contracts and no-op/local facades for optional capabilities. The base APK must not quietly absorb an addon's risky dependency.
Key security files:
docs/THREAT_MODEL.mddocs/SECURITY.mddocs/PRIVACY_AND_AI.mddocs/REPRODUCIBLE_BUILDS.mdapp/build.gradle.kts
Important implementation anchors:
verifyNoInternetPermissioninapp/build.gradle.ktsfails the build if any app manifest declares a network permission.- Personal dictionary storage uses SQLCipher, with its passphrase protected by the Tink/AndroidKeystore wrapper.
- Sensitive fields are guarded before learning, clipboard writes, addon calls, and local AI/automation surfaces.
- Password fields get IME-side screenshot protection through
FLAG_SECURE. backup_rules.xmlexcludes sensitive local state from Android cloud backup.
Use the Gradle wrapper. The expected maintainer-host verification path is:
./gradlew.bat :app:verifyNoInternetPermission :app:testDebugUnitTest :app:lintDebug :app:assembleDebug
./gradlew.bat :app:verifyRoborazziDebugThe CI workflows under .github/workflows/ cover Android build/test/lint,
network-permission verification, OSV scanning, release builds, reproducible
build checks, repo hygiene, string validation, Crowdin upload, weekly
Dependabot version review, manual emulator settings-launch smoke, and
maintainer-triggered Roborazzi baseline capture. Android lint runs through
scripts/run-lint-debug-with-baseline-check.sh in CI so stale baseline entries
fail visibly, and scripts/check-repo-hygiene.sh keeps generated build/report
output out of committed history.
Fastlane changelog drafting guidance keeps the short store-facing release text
within the 480-character draft budget before the metadata script enforces the
hard limit.
Repo hygiene also documents that git rm --cached does not delete ignored
lib/<module>/build/ caches, so generated module output stays local unless a
small textual summary is committed under docs/.
The local VM used by some agent runs may not have Java or the Android SDK on PATH. In that case, record the exact Gradle command attempted and leave final verification for the maintainer build host.
Unit tests live under app/src/test/kotlin/ and mostly use Kotest. Screenshot
tests use Roborazzi and Robolectric. The active test strategy is:
- Pure policy classes get focused JVM tests.
- Android-facing managers get tests around extracted decisions when framework tests would be brittle.
- UI-affecting visual changes should add or update Roborazzi coverage once the baseline-capture window closes.
- Every bug fix should include a regression test unless the fix is purely documentation or build metadata.
- Product state and roadmap:
PROJECT_CONTEXT.md,ROADMAP.md - Archived planning snapshots:
docs/archive/ - Typing behavior contracts:
docs/AUTOCORRECT_LIFECYCLE.md - Research artifacts:
.ai/research/<YYYY-MM-DD>/ - Release notes: appended as a new
## vX.Y.Zsection in rootCHANGELOG.md - Contributor workflow:
CONTRIBUTING.md - Architecture: this file
- Security/privacy/rebuild docs:
docs/
When a change updates a load-bearing invariant, update the code, the gate, and the corresponding documentation in the same logical commit.