Skip to content

Releases: SysAdminDoc/SwiftFloris

SwiftFloris v1.9.59

Choose a tag to compare

@SysAdminDoc SysAdminDoc released this 12 Aug 11:42

Add-on and MCP enrolment is now screened against a permission allowlist, so SMS, Bluetooth and nearby-device transports are refused alongside network ones. Advanced Protection Mode pauses learning, clipboard history and new enrolment. Replacing a word no longer duplicates text in rich-text and web editors. Every shipped language has a translation route, and the bundled emoji data is correctly declared as Emoji 17.0.

v1.9.56

Choose a tag to compare

@SysAdminDoc SysAdminDoc released this 09 Jul 11:44

Roadmap hardening: Han DB access serialization, typed-text log redaction, SQLCipher refresh and freshness gate, bounded sync and translation addon bridges with typed failure/timeout/cancel copy, runtime addon capability gates, and fixture-tested legacy preference migrations.

v1.9.55

Choose a tag to compare

@SysAdminDoc SysAdminDoc released this 06 Jul 02:05

Smart-compose ghost text no longer blocks while reading personal trigram/bigram storage. The heuristic provider and cache now expose a suspend-aware prediction path used by the NLP worker, reducing per-keystroke stalls when heuristic smart compose is enabled.

SwiftFloris v1.9.53

Choose a tag to compare

@SysAdminDoc SysAdminDoc released this 25 Jun 21:23

SwiftFloris — a privacy-first, SwiftKey-style on-screen keyboard. No INTERNET permission; everything stays on-device.

What's new in v1.9.53

Typing performance:

  • Stale suggestion cancellation — fast typing now cancels the previous in-flight suggestion coroutine before starting a new one, so rapid keystrokes no longer run multiple parallel pipelines fighting for CPU time
  • SymSpell index pre-warming — the spell-correction index is now built in the background as soon as the dictionary loads, eliminating a 100-500ms stall on the first misspelled word
  • Zero-copy overlay reads — the user dictionary overlay no longer creates a defensive HashMap copy on every keystroke, reducing per-keystroke allocation pressure
  • SymSpell OOM guard for 256 MB heap devices — quadratic budget scaling, periodic free-heap checks during index build, incremental builder-to-frozen freeze (halves peak memory), and OOM catch at the lazy-init callsite so the keyboard degrades to no-correction instead of crashing

Trust & CI hardening:

  • Release-channel freshness is now a blocking CI gate (--strict mode) — merged code that claims a version not published on GitHub Releases fails CI immediately
  • Data-extraction rules verified by XML-parsed domain/path pairs (not substring matching) across both cloud-backup and device-transfer sections, now covering n-gram stores, trace logs, sync identity, and diagnostics
  • Public trust docs (THREAT_MODEL.md, PRIVACY_AND_AI.md, CONTRIBUTING.md, addon specs) are now tracked in git and subject to a link-drift gate
  • Keyboard layout JSON assets validated in CI (139 layouts, JSONC-aware parser)

Security & reliability:

  • Tink Android 1.21.0 → 1.22.0 — security-relevant bump for the AndroidKeystore wrapping path
  • Destructive clipboard Room migrations replaced with row-preserving explicit migrations — pinned clipboard text and provider media metadata are no longer silently erased on schema gaps
  • EditorInfo sensitive-field replay tests (16 new tests covering password/PIN suppression, incognito, NO_PERSONALIZED_LEARNING, and touch-evidence gates)

Accessibility:

  • Deprecated announceForAccessibility migrated to Compose semantics live-region — TalkBack glide-word announcements use the Android 16+ recommended pattern

Features:

  • Addon provenance export — tap an installed addon in Settings → Addons to copy its JSON provenance report (package, type, signer SHA-256, no-network attestation, license)
  • One-tap privacy proof export — Privacy posture screen can copy a stable JSON bundle with version, signer fingerprint, INTERNET permission status, and addon counts
  • Snippet management Settings screen — import Espanso YAML files via SAF, preview loaded triggers and replacements, delete individual files or clear all; accessible from Settings → Typing → Snippets and via settings search

Install

  1. Download app-release.apk below.
  2. Enable "Install unknown apps" for your browser/file manager if prompted.
  3. Tap the APK to install. Requires Android 8.0+ (minSdk 26).
  4. Enable it under Settings → System → Languages & input → On-screen keyboard.
  5. (Optional) Verify integrity against SHA256SUMS.

SwiftFloris v1.9.48

Choose a tag to compare

@SysAdminDoc SysAdminDoc released this 14 Jun 07:31

SwiftFloris — a privacy-first, SwiftKey-style on-screen keyboard. No INTERNET permission; everything stays on-device.

What's new since the last public release

  • CJK mixed-script spacing (opt-in) — boundary space between Han characters and adjacent Latin words or digits (安装 App, 第 3 章).
  • Touch calibration profiles — Conservative / Normal / Rescue-heavy tuning for gap-rescue dead zones and adaptive-touch neighbour correction (Normal = unchanged default).
  • One-tap "Full backup" — ticks every section and writes a dated archive in a single tap, fully on-device.
  • Audit hardening pass — the exported share-to-clipboard handler now rejects file:// URIs (confused-deputy guard); MCP daemon discovery bounds untrusted catalog reads (OOM guard); incognito FLAG_SECURE reapplies reliably across keyboard restarts; corrected an inverted SERA vowel order in the Ge'ez/Tigrinya transliterators; plus settings UX/accessibility/microcopy polish.

Install

  1. Download app-release.apk below.
  2. Enable "Install unknown apps" for your browser/file manager if prompted.
  3. Tap the APK to install. Requires Android 8.0+ (minSdk 26).
  4. Enable it under Settings → System → Languages & input → On-screen keyboard.
  5. (Optional) Verify integrity against SHA256SUMS.

⚠️ Signing-key notice (read if upgrading)

This APK is signed with the SwiftFloris self-host release key (CN=SwiftFloris Self-Host), which is a different key than earlier GitHub releases (≤ v1.8.172). Android refuses to update an app in place when the signing key differs, so:

  • New users: installs cleanly, nothing to do.
  • Upgrading from an older GitHub build: export a backup first (Settings → Advanced → Backup), uninstall the old SwiftFloris, install this APK, then restore your backup. Future self-host releases will keep using this same key, so this is a one-time step.

SwiftFloris v1.8.172

Choose a tag to compare

@SysAdminDoc SysAdminDoc released this 19 May 00:50

Glide Trail Visual Overhaul + Trail Themes + Performance

Trail Rendering

  • Path-based strokes replace the old dotted-circle trail — smooth, continuous line with StrokeCap.Round
  • Alpha gradient — tail fades from 15% to 90% at the fingertip via sqrt(progress) curve
  • Tapered width — thinner tail, thicker at the finger
  • Head dot — 3-layer glow/core/highlight at the fingertip
  • Key highlight — subtle rounded-rect overlay on the key under the finger during glide
  • Trail duration 200ms to 500ms so gradients actually spread

7 Trail Themes (Settings > Gestures > Trail theme)

  • Accent — Theme primary color (with transparent fallback guard)
  • Rainbow — Animated full-spectrum hue cycling
  • Fire — Dark crimson > red > orange > yellow > white-hot with heat shimmer
  • Ice — Dark indigo > electric blue > cyan > white frost
  • Aurora — Northern lights: cycling greens/teals/purples
  • Galaxy — Deep purple > electric blue > hot pink > lavender with hue shift
  • Neon — Pulsing bright green

Bug Fixes

  • Invisible trail fixed — the draw block was gated behind a language check that returned false for unlisted locales even though gesture detection worked fine. Removed the redundant gate.
  • Accent color guard — if the theme resolves --primary to transparent, falls back to green.

Performance

  • Cancel stale preview coroutines — prevents pile-up during long gestures
  • Cache ideal gestures — resampled+normalized ideal gesture paths cached in bounded LruCache(512) instead of regenerated per classification call
  • 8 draw calls/frame — down from 40 (removed separate glow layer, cut segments from 20 to 8)
  • Reduced classifier work for previews — only computes needed count instead of always 8

Install

Download app-release.apk below. Signed with the SwiftFloris release key (v1+v2+v3 schemes).

SwiftFloris v1.7.8

Choose a tag to compare

@SysAdminDoc SysAdminDoc released this 13 May 19:54

SwiftKey-style keyboard parity polish.

Changes:

  • Removes the top-strip sticky microphone so the suggestion row matches SwiftKey's organization.
  • Keeps voice input on the comma key and keeps the spacebar blank.
  • Removes suggestion dividers and the empty right-side smartbar slot.
  • Adjusts bottom-row utility key widths and label sizing for the SwiftKey-style number-row layout.
  • Bumps the Android package to version 1.7.8 / code 178.

Verification:

  • ./gradlew.bat :app:compileDebugKotlin
  • ./gradlew.bat :app:assembleRelease
  • apksigner verify: v2/v3 signatures valid
  • Installed on device R5CY34G070L via adb after uninstalling existing SwiftFloris package

APK SHA-256: 8190B60104D279CEA28F509B1A61EAA8494949705E29E56BFEB0D87CF6F95904

SwiftFloris v1.7.7

Choose a tag to compare

@SysAdminDoc SysAdminDoc released this 13 May 17:43

SwiftFloris v1.7.7 - Premium UX polish

Released: 2026-05-13
Type: Product polish / UX refinement.

This release focuses on making the settings and extension-management experience feel more deliberate, legible, and trustworthy after the v1.7.6 hardening pass.

Highlights

  • Refined the main settings experience with clearer hierarchy, calmer status cards, and more useful action labels.
  • Improved first-run setup with stronger privacy framing and clearer recovery expectations.
  • Polished voice input setup and status messaging so unavailable, retryable, and enabled states are easier to understand.
  • Improved backup and restore copy for destructive or trust-sensitive flows.
  • Reworked extension import states with clearer empty, review, skipped-file, and technical-detail surfaces.
  • Upgraded extension detail pages with overview, metadata, and management sections instead of flat metadata rows.
  • Replaced debug-style component output with structured component metadata rows for themes and language packs.

Verification

  • :app:compileDebugKotlin
  • :app:verifyNoInternetPermission
  • :app:lintDebug
  • :app:testDebugUnitTest
  • :app:assembleDebug
  • :app:assembleRelease
  • Fresh adb uninstall/install smoke on a connected phone

SwiftFloris v1.7.6 — Hardening pass

Choose a tag to compare

@SysAdminDoc SysAdminDoc released this 13 May 14:48

SwiftFloris v1.7.6 — Hardening pass

Released: 2026-05-13
Type: Maintenance / hardening — no new features, no UI changes.

Three rounds of targeted bug-hunting and privacy hardening, all addressing real defects with confirmed impact on correctness, data integrity, leak surface, or the project's "100% offline, zero cloud" privacy posture. Every fix ships with the existing public API unchanged.


Round 1 — correctness, leaks, races (commit 920da85)

Clipboard history data corruption — ClipboardHistoryManager

The hand-rolled JSON parser in the encrypted clipboard layer silently corrupted control characters on save→load roundtrip. A clipboard entry containing a newline, tab, or carriage return came back with those characters replaced by literal n / t / r letters. Replaced with kotlinx.serialization (@Serializable data class) so all control characters, quotes, backslashes, and non-ASCII text survive a roundtrip cleanly. Added a coarse-grained lock so two concurrent producers can no longer drop each other's entries via a race in read-modify-write. Added a graceful fallback to an in-memory store when the Android Keystore is unavailable, so a corrupted keystore can no longer prevent the IME from instantiating.

Theme disk leak — ThemeManager

Every theme reload (including each keystroke in the theme editor) created cacheDir/loaded/<UUID>/ and never deleted it. Long-running keyboards accumulated megabytes of stale extracted theme assets indefinitely. Now sweeps stale dirs on init, deletes the on-disk dir for each evicted cached ThemeInfo, and spares the dir backing the currently-active theme (composition still reads from it).

Han index race — HanShapeBasedLanguageProvider

connectedLanguagePacks / languagePackItems / keyCode were mutated from one coroutine (create(), preload()) and read concurrently from another (suggest(), determineLocalComposing()) without synchronization. Marked @Volatile (snapshots are already immutable) and added a synchronized(loadLock) around LanguagePack.load(context) so two coroutines for different subtypes can't race on the SQLite handle.

IME teardown crash — FlorisImeService.onDestroy

unregisterReceiver(wallpaperChangeReceiver) was unconditional. If onCreate threw before registration completed, onDestroy raised IllegalArgumentException — masking the real init error and aborting the rest of teardown, leaking the static WeakReference. Now tracks wallpaperReceiverRegistered, guards each teardown step independently.


Round 2 — privacy & performance (commit 346aa89)

Backup leak — backup_rules.xml, xml-v31/backup_rules.xml

The user dictionary table (floris_user_dictionary) records every word the personal-learning pipeline has promoted from the user's typing — names, addresses, codenames, vocabulary. The project's stated posture is "Zero cloud processing. Zero telemetry. All features work offline." Auto Backup was silently uploading the dictionary to the user's Google Drive on both pre-API-31 (<full-backup-content>) and API 31+ (<cloud-backup>). Removed it from both cloud paths. Kept it in <device-transfer> (API 31+) so explicit phone-to-phone migration still preserves the adaptive vocabulary — that flow is a deliberate user action, not a silent upload.

Profileable shipped to release — AndroidManifest.xml

<profileable android:shell="true"/> was in the main manifest and shipped in release/beta APKs. On a privacy keyboard, a shell-attached profiler (simpleperf, perfetto) can heap-dump the IME process and expose freshly-typed text including passwords. Moved to app/src/debug/AndroidManifest.xml and app/src/benchmark/AndroidManifest.xml variant overlays so release and beta builds no longer advertise as profileable. Benchmark and debug variants still get full profiling.

Unbounded frequency cache — NlpManager.frequencyCache

Was a raw ConcurrentHashMap<String,Double> keyed by ${subtype}-$word. StatisticalGlideTypingClassifier:235 calls it inside the per-candidate inner loop of every gesture, so a heavy gesture-typing session would accumulate every unique word forever in a long-lived IME process. Switched to LruCache(5000) — bounded warm-vocabulary cache for a single session.

Double Room query per stats refresh — TypingStatsScreen

Was calling DictionaryManager.queryAll() twice (size, then sorted-top-10) per refresh — two full Room table scans on what can be a 10k+ row table. Cached once. Also wrapped AdaptiveTouchModel.totalSampleCount() in remember(refreshTick) so it isn't walked under a @Synchronized block on every recomposition.

.gitignore narrowed

The Rust debug/ pattern shadowed app/src/debug/. Narrowed to /target/ and **/target/{debug,release}/ so the Android variant tree is no longer hidden.


Round 3 — leaks & OOM (commit 5543baf)

Feedback-scope leak — InputFeedbackController

Held a CoroutineScope(SupervisorJob) that was never cancelled on IME teardown. Each keypress launched a coroutine on that scope that captured ims.window?.window?.decorView, so when the InputMethodService was destroyed mid-typing the in-flight haptic and audio coroutines kept the entire window's view tree reachable past service death. Added dispose() and call it from FlorisImeService.onDestroy alongside the existing teardown steps.

Copy-to-clipboard OOM — FlorisCopyToClipboardActivity

The legacy bounds-decode fallback called MediaStore.Images.Media.getBitmap(), which decodes at full resolution. An attacker-supplied content:// URI to a multi-megapixel image could OOM the IME process. Replaced with sampled BitmapFactory.decodeStream(inSampleSize=2) that bounds peak memory. Also fixed a redundant bitmap!!.asImageBitmap() non-null assertion to use the let parameter bmp.


Verification

  • :app:compileDebugKotlin
  • :app:testDebugUnitTest — all tests pass ✓
  • :app:verifyNoInternetPermission — privacy gate green ✓
  • :app:processDebugManifest / :app:processReleaseManifest — variant overlays confirmed (<profileable> in debug-only) ✓

Known follow-ups

  • README clipboard claim ("AES-256 GCM, military-grade protection") references the ClipboardHistoryManager AES path, but the wired clipboard is still the Room-backed ClipboardManager (Android FBE at rest, not AES-256-GCM at the app layer). Either wire ClipboardHistoryManager into the active flow or soften the README. Out of scope for this hardening pass.

SwiftFloris v1.7.5 — SwiftKey indistinguishability wave

Choose a tag to compare

@SysAdminDoc SysAdminDoc released this 10 May 01:18

SwiftFloris v1.7.5 — SwiftKey indistinguishability wave

Released: 2026-05-09
Versioning: 1.7.4 → 1.7.5 (versionCode 174 → 175)

This release closes the new N12 "SwiftKey indistinguishability" roadmap section plus a chunk of the original Next-1 SymSpell item — twelve commits, all on-device, no Copilot, no cloud, no account. The goal: a user can't tell whether they're typing on SwiftKey or on SwiftFloris.

See SWIFTKEY_PARITY_RESEARCH.md for the underlying research.


What's new

Surface fixes (the two paper-cuts you'd notice in 30 seconds)

  • Auto-space after punctuation defaults to ON. Period / comma / ? / ! now insert a trailing space without a settings tweak. Existing user overrides still win, so already-installed users toggle in Settings → Typing.
  • Suggestion-tap haptic. Tapping an autocorrect / suggestion strip word now fires the same key-press vibration as tapping a letter key, with keyLongPress() on the long-press branch.

N12.1 — Adaptive touch model

New AdaptiveTouchModel keeps per-subtype, per-key Welford-online stats of the user's actual tap-offset distribution (normalised by key half-size). After ≥30 samples per key, hit-tests bias toward where the user actually taps using a 2D-Gaussian log-likelihood — the SwiftKey "feels accurate" effect, all on-device, no offsets ever written to disk.

N12.2 — Next-word predictions via PersonalBigramStore

New per-locale bigram counter persisted to <filesDir>/personal_bigrams_<localeTag>.tsv. Caps: 2,000 prev words per locale, 16 next words per prev, max count 1,000, MIN_COUNT=2. The suggestion strip is no longer empty after a space — it shows the top bigram completions for the previous word.

N12.3 — Multi-language hot-switch

When a subtype has secondary locales enrolled (SubtypeEditorScreen already supports this), LatinLanguageProvider.suggest queries every enrolled locale's dictionary and merges per-locale candidates with a prior of 1.0 for any locale that recognised the typed word and 0.4 for those that didn't. isEligibleForAutoCommit is gated to recognising locales — no more wrong-language autocorrect mid-sentence.

N12.4 — Flow Through Space

GlideTypingGesture.Detector.signalWordBoundary() snapshots and resets the trace mid-stroke; the controller fires it when the trace re-enters the SPACE key after first leaving it. Phantom-space inserts the " " between committed words, classifier resets between words, trail-fade visually punctuates each. Glide a word, drag finger across the space bar, glide the next word — all without lifting.

N12.5 — Trigram next-word predictor

New PersonalTrigramStore is a per-locale (prev2, prev1) → next counter persisted to <filesDir>/personal_trigrams_<localeTag>.tsv. Caps: 4,000 contexts, 12 next words per context. KeyboardManager.learnIfAllowed now learns both bigrams and trigrams via a sliding two-word window. After typing the quick brown fox a couple of times, typing the quick surfaces brown as the top suggestion.

N12.6 — Typing stats screen

New Settings → Typing → "Typing stats" screen reads three on-device numbers off-thread:

  • Words-learned count + top-10 personal-dictionary entries by frequency
  • Total bigram-store size on disk
  • Adaptive-touch-model session sample count

No data leaves the device.

N12.7 — Cold-start bootstrap from dictionary frequency

LatinDictionarySnapshot.topByFrequency(n) lazily caches the top-64 high-frequency dictionary words. Suggestions now layer Tier 0 (trigram, 0.80–0.45) → Tier 1 (bigram, 0.55–0.20) → Tier 2 (dict bootstrap, 0.30–0.55). Result: never-empty suggestion strip on cold-start and after sentence-ending punctuation. Sentence-start detection auto-capitalises the first letter.

N12.8 — Adaptive touch model feeds the glide classifier

AdaptiveTouchModel.adjustedCenter(...) returns user-personalised pixel centers. StatisticalGlideTypingClassifier.findNClosestKeys (matching) and Pruner.generateIdealGestures (template) both consult adjustedCenter() instead of key.visibleBounds.center. Bias clamped to ±0.5×half so a heavily-skewed learner can't drag the template outside the visible key. Gives glide the same per-user spatial bias N12.1 already gives taps.

N12.9 — Sentence-case suggestions

After ., !, or ? (or empty input), every next-word suggestion's first letter is capitalised. SwiftKey-parity at sentence start.

N12.10 — Long-press suggestion to forget

WordSuggestionCandidate from next-word predictions and personal-dict suggestions now both ship isEligibleForUserRemoval = true. New DictionaryManager.forgetWord, PersonalBigramStore.forget, PersonalTrigramStore.forget are all consulted by LatinLanguageProvider.removeSuggestion. Long-press a noisy suggestion → it's gone from personal dict, bigrams, and trigrams in one stroke.

Next-1.A — SymSpell delete-index for distance-1 corrections

New pure-Kotlin SymSpellIndex.kt. LatinDictionarySnapshot.symSpellIndex is by lazy so the build (~100–300 ms over the 117k-word EN dict) lands on first correction call. LatinDictionarySuggester.knownEdits1 now calls dictionary.symSpellIndex.candidatesAtDistance1(input) instead of generating Norvig's L · 54 candidate strings per call — ~50× speedup on the per-keystroke correction path.

Next-1.B — Distance-2 high-frequency auto-commit

New AutoCommitMinFrequencyDistance2 = 0.92 threshold. Distance-2 corrections now auto-commit on space when the candidate is in the very common bucket (~top 3k SCOWL words). Closes the long-word-typo gap: recieved → received, tommorrow → tomorrow, seperate → separate, definately → definitely.


Settings reference

Every new behavior is gated behind a pref so power-users can opt out:

  • Typing → Adaptive touch model (default on)
  • Typing → Predict the next word (default on)
  • Typing → Multilingual suggestions (default on)
  • Gestures → Flow through space (default on)
  • Typing → Typing stats (link to the new screen)

Out of scope (explicit non-goals)

  • Microsoft Copilot / Editor / Tone
  • DALL-E sticker / Designer
  • Microsoft account login or sync
  • Federated learning aggregator
  • Anything that requires the INTERNET permission

Roadmap status

  • N12 SwiftKey indistinguishability — 10/10 ticked (12.5 absorbed into the trigram tier)
  • Next-1 SymSpell — 1.A and 1.B ticked, 1.C (full d2 SymSpell index) deferred pending field data
  • L1 On-device LLM (Gemma 3 270M Q4) — still the next big jump; multi-week
  • Next-3.1 Pre-trained KenLM 5-gram bootstrap — would give first-time users rich predictions before they've typed anything

Verification

  • ./gradlew :app:assembleDebug — green
  • ./gradlew :app:assembleRelease — green (signed when SIGNING_KEYSTORE_BASE64 is set; otherwise falls through to debug signing as documented in N6.2)
  • :app:verifyNoInternetPermission — green (no INTERNET permission added by any item)
  • adb-installed on local Pixel-class device, smoke-tested across all five new prefs