Repository navigation
Conversation
Room uses the androidx.sqlite driver API (SealiteDriver) instead of sqlcipher-android's open-helper factory. The key is the same raw DEK, so vaults written by earlier releases open unchanged; EarlierReleaseVaultTest writes one with sqlcipher-android 4.19 and opens it with the driver. Migrations, the trigger callback and backup transactions use SQLiteConnection.
… and UUID Stored timestamps stay epoch milliseconds. Localized formatting (month names, field order, 12/24 h) stays in one platform function, LocalizedDate. The importer's date parser accepts the same inputs as before (ImportTimeTest pins 149 of them).
…stead of Gson The stored JSON is unchanged byte for byte (BlockJsonFormatTest pins every block kind, Gson's HTML-safe escapes included), and reading stays as lenient as before.
Encly only turns 128 random bits into 12 words and checks phrases; it never derives a BIP-39 seed, so that part is not implemented. Bip39GoldenTest runs the official trezor vectors and pins the keys Encly derives from a phrase; Bip39WordlistTest pins the wordlist's SHA-256.
The archive readers, the Markdown export and the My Notes hand-off read and write kotlinx-io Sources and Sinks; java.util.zip stays behind small zip interfaces in one file (JvmZip). Every limit and check is kept: unsafe paths, entry count, entry and total sizes, zip bombs, CRCs, strict UTF-8. The Markdown export is pinned byte for byte.
AES-256-GCM, HKDF-SHA-256, PBKDF2, SHA-256 and the CSPRNG go through core/crypto/Crypto; JcaCrypto is the only file behind it that imports javax.crypto or java.security, so another platform replaces one file. Hygiene stays as before: a fresh Cipher and Mac per call, the PIN reaches PBKDF2 as characters and is cleared, HKDF wipes its PRK. CryptoGoldenTest pins every slot and the backup format, unchanged from the previous code.
Coverage
|
One compile-time AppGraph: singletons are @singlein(AppScope), the former modules are binding containers contributed to it, ViewModels are contributed into the Metro ViewModel factory (assisted for the three that take a SavedStateHandle), and the Activities inject themselves from the one graph in onCreate. Optional bindings are disabled, so a missing binding fails the build instead of falling back to a default. AppGraphTest checks that every security singleton is a single instance and that every ViewModel can be created.
Everything the editor needs from the app now comes through its own extension points (EditorStrings, EditorTheme, EditorClipboard, LinkOpener, EditorLogger, EditorController), so com.pasich.encly.editor imports nothing from the app; EditorBoundaryTest keeps it that way. The package is gated at 100% line and branch coverage. Backspace in an empty link address now removes the block from a hardware keyboard too (onPreviewKeyEvent, like the other fields). Stored note JSON is unchanged.
…layers Domain, repositories, backup, import, export, hand-off and the security core no longer import android.* or java.*: SharedPreferences, the main-thread timer, biometrics, the Keystore PIN factor, device lock, app strings and the vault file I/O sit behind small interfaces with one Android implementation each. The vault slot file keeps its DataOutputStream format byte for byte (VaultStoreLegacyFormatTest) and its fsync and atomic replace (JvmVaultFileIo). PlatformBoundaryTest keeps the layers platform-free.
…ion plugins build-logic holds the convention plugins (KMP library, Compose Multiplatform, quality, spotless). :editor targets Android, desktop JVM and iOS; its sources moved unchanged into commonMain, with one expect/actual for IDN hosts. It keeps 100% line and branch coverage, a checked ABI dump and Dokka docs; CI compiles the iOS targets on macOS.
:core:database holds the Room database, its entities, DAOs, migrations and SecureDatabaseManager for Android, desktop and iOS. The platform gives only the file location and the builder (DatabasePlatform); on Android the files stay where they are (database.db, vault_b.db). Desktop tests open, migrate and reject vaults encrypted with Sealite's JVM natives; the instrumented tests run from the module.
:core:common (platform interfaces, UiText, wiping), :core:crypto (the Crypto API with JCA behind it for Android and desktop, BIP-39) and :core:security (vault, PIN and recovery slots, session lock, backups). Locks move to atomicfu with the same critical sections (the same monitor bytecode on the JVM). Crypto and security have no iOS target yet: public CommonCrypto has no AES-GCM, and no weaker stand-in is shipped. :core:crypto is gated at 100 % line and branch coverage.
:core:resources holds every string and plural of the nine languages. The translation files stay in Android format, byte for byte, so Weblate keeps working; a build step converts them into Compose resources (escapes, bare %d and %% included), and AndroidStringsConversionTest checks every string and plural against the Android values. The in-app language follows through the process locale. Biometric prompts get their texts as plain strings, loaded before the first prompt.
:core:keystore gives desktop the same PIN hardware factor contract as Android: HMAC-SHA256 over salt and PIN with a random 32-byte secret that lives only in the OS store of the user (DPAPI on Windows, the login Keychain on macOS, the Secret Service on Linux), one per key slot. A missing or unreadable secret behaves like a lost Keystore key. SECURITY.md describes the weaker, software-bound threat model. The keystore-it job runs the real backends on Windows, macOS and Linux.
…orm module :shared holds the screens, ViewModels, navigation, design system, domain and platform-free data code for Android and desktop. :app keeps the activities and the Android adapters behind small platform interfaces (documents and pickers, links, toasts, sensitive clipboard, window security, biometric host, locale, app info). SharedBoundaryTest keeps Android and JVM APIs out of the common code. No test was dropped (1314 -> 1362).
:desktopApp runs the shared UI in a Compose Desktop window with its own Metro graph: the vault in an owner-only app-data folder, the PIN factor in the OS secret store, native file dialogs (XDG portal on Linux), a native menu bar with shortcuts, drag and drop of backups and imports, remembered window state, a single instance per user. Security parity: capture exclusion where the OS allows it, lock on OS screen lock, minimise and idle, clipboard wiped after 60 s. Installers: MSI, DMG and DEB, built unsigned in CI.
Hand cursor and a keyboard-only focus ring on every interactive element, tooltips on hover as well as on long-press, context menus from a right click, the Menu key or Shift+F10 on notes, tasks and editor blocks (never from a finger), themed scrollbars on desktop, and app shortcuts with the platform's modifier (new note, find, lock, tabs, delete, Esc). Touch behaviour on Android is unchanged; the editor keeps 100 % coverage.
Compact windows keep today's phone UI exactly. Medium windows get a navigation rail and centred content; expanded windows a permanent drawer, the notes list beside the editor and tabs for open notes (#85). Sheets become centred dialogs on wide windows, and a tighter spacing set applies on pointer-first desktops. Relock clears every pane and tab before the lock screen shows, and reopens the active note as a tab after unlock.
The keyboard host at the app root and the menu bar share one key map and one router, so every chord has a single owner. Note cards get a context menu and a current-note highlight on pointer devices, tabs close on middle-click and open in the background on Ctrl/Cmd+click, and the long pages and lists show scrollbars. The database vault and the view model factory move into the shared module, and the menu titles are translated.
The Linux packaging job also builds a Flatpak from the app image, with no network access and only the bus names the key store and the lock watcher use. The macOS DMG is signed, notarized and stapled and the Windows MSI is signed only when their secrets exist and the run is not a pull request; otherwise the installers stay unsigned as before.
Tables with Tab, Enter and arrow navigation and a cell menu, written to Markdown as pipe tables; code blocks in a monospace font without corrections, toggled with the < > button; and Settings → Appearance → Editor toolbar to choose and order the buttons of the bar and its More menu, kept in backups. A copy of a note whose content could not be read keeps the stored text instead of an empty one. The store listings say the PIN can have 6 to 12 digits. Refs: #76
The password unlock method is removed again: issue #95 asked to keep the PIN after a recovery-phrase unlock, not for a second secret. The PIN of 6 to 12 digits and "Keep my PIN" stay; a vault with a leftover password entry from a development build opens with its PIN. On Linux an unlock was refused while a screen locker had left the lock flag set; the state is now read live from logind or the screen saver, and a refused unlock shows a message. A failed PIN derivation wipes its intermediate keys. Refs: #95
…ked checklists stay locked Cut put escaped Markdown on the clipboard and paste never read it back, so a cut and paste changed the text and turned tables into lines of pipes. The editor now keeps what it copied and pastes those blocks back when the clipboard still holds them. Typing in a long list or a large table redraws only the row or cell being edited, a selection that touches a locked checklist leaves it unchanged, dragging into or out of a table follows the selection rules, and empty tables and code blocks are kept. Refs: #76
Notes can be sorted by title A to Z or Z to A, compared in the order of the app's language, with untitled notes last. The unlock reveal takes about half a second instead of one and is skipped when the system has animations turned off.
…int prompt in onboarding The editor's fade during the open and close transition rendered its content into an offscreen surface of a new size on every frame, and the GPU kept them all: on a Pixel 7a graphics memory grew from 65 MB to about 200 MB after a few notes. The fade now modulates the alpha of each draw instead, and graphics memory stays at about 80 MB. In onboarding the fingerprint prompt runs through the same guard as on the lock screen, a cancel stays a cancel, and a button offers it again.
…r database connections Screens faded in and out through a full-window offscreen layer each; they now modulate the alpha of each draw, like the note transition. The database runs its queries on at most two threads, so the pool holds two readers and the writer instead of four and the writer, and each connection keeps a 1 MiB page cache instead of 2 MiB. The editor reuses the theme's fonts instead of loading its own set. On a Pixel 7a the app uses about 140 MB after opening notes, down from 155 MB, with 58 MB of graphics memory instead of 80 MB.
The tasks screen read open and done tasks through two queries combined into one state, so a task that was just checked or unchecked could sit in both lists for a moment and the list crashed on the repeated key. A hidden tag's view did the same with its live and archived notes. Each list now comes from one query.
8 of 9 tasks
note_tags (noteId, tagId) replaces notes.tagId in schema 8. Migration 7 -> 8 copies every note's tag into it, drops tags that point at no tag, and rebuilds notes without the column, keeping its index, uid triggers and id counter. Refs: #109
The backup payload goes to schema 6: a note lists its tags as tagUids. Files of schema 1 to 5 are read with the note's one tag as the list. Importers keep all of a note's tags instead of the first, Markdown front matter lists them all (a quoted tag with a comma reads back whole), and the PDF names them. Refs: #109
A tag's view lists every note with that tag, in one query. A note with any hidden tag stays out of every list but a hidden tag's view. The list sheet puts on or takes off one tag at a time, and the editor saves only the tags it changed, so a tag changed elsewhere meanwhile stays. Tag counts come from one query. Refs: #109
The note sheet's tag chips and the editor's tag sheet toggle each tag and stay open; No tag clears them. A card names the first two tags and counts the rest, and the editor's overline shows every tag. Refs: #109
…le 9.8.2 and gradle actions
This was referenced Nov 10, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Encly moves to Kotlin Multiplatform and ships on every platform as 3.0.0.
Modules
:shared:editor:core:databaseSecureDatabaseManageron the Sealite driver (SQLCipher 4.20); schema 7:core:security:core:crypto:core:keystore:core:common,:core:resources:app:desktopAppiosApp/Replaced: sqlcipher-android → Sealite, Hilt → Metro, Gson → kotlinx.serialization, kotlin-bip39 → own encoding,
java.time/UUID→ kotlinx-datetime /kotlin.uuid,java.io/java.util.zip→ kotlinx-io,R.string→ Compose resources (translations stay in Android format for Weblate).Issues closed by this release: #83 (selection across blocks, block selection), #85 / #106 (desktop), #93 (settings in backups), #94 (screenshots until the app locks), #95 (PIN of 6–12 digits, keep the PIN after a recovery unlock), #97 (trash empties after 28 days), #98 (5 and 10 minute auto-lock), #103 (creation date), #104 (biometric unlock after a background lock), #105 (Arabic and RTL, hide previews, what's new), #107 (iOS), and the open items of #76 (nested lists, drag to reorder, tables, code blocks, custom toolbar, checklist lock button).
From the code review: the duress vault no longer sees the real vault's tag; a lost flags file can no longer lead to onboarding over an existing vault; a lock saves the open note first; the editor forgets its undo text on a lock; link cards refuse hosts browsers resolve differently; a biometric answer after an activity stop is kept; F-Droid reproducibility is checked in CI.
Why
Desktop (#85, #54) and iOS need the logic and UI in common code, and Room's multiplatform API needs a driver sqlcipher-android does not provide. The issues above were reported by users of 2.3.x.
Behaviour changes
trashedAt); notes already in the trash start their 28 days with the update.Testing
CI runs fourteen jobs: unit and host tests of every module, Kover gates, detekt, ABI dumps, Dokka, R8 release builds and lint, instrumented tests on API 26 and 35 (Room migrations, a vault written by 2.3), iOS tests on the simulator and an unsigned
xcodebuild, the desktop installers and a Flatpak bundle, the PIN factor against the real DPAPI, Keychain and Secret Service, and an F-Droid reproducibility check (two builds in different directories).Manually: upgrade from 2.3.1 to the new build (release, minified, same signing key) on an emulator — PIN unlock, notes and archive intact, what's new shown, settings working.
Not verified: the new editor features and PIN screens by hand on a device; desktop installers on real Windows, macOS and Linux machines (clipboard history, sleep, Secret Service with KWallet/KeePassXC, the logind live lock state); iOS on a device (Secure Enclave, Face ID, keyboard, clipboard).
References
#76, #83, #85, #93, #94, #95, #97, #98, #103, #104, #105, #106, #107