The opt-in encrypted cache crash-loops on launch for any user who has enabled it, on the first cold start after enabling (e.g. an app upgrade):
FATAL EXCEPTION: arch_disk_io
java.lang.UnsatisfiedLinkError: No implementation found for long
net.zetetic.database.sqlcipher.SQLiteConnection.nativeOpen(...) — is the library loaded, e.g. System.loadLibrary?
at ...SupportHelper.getWritableDatabase(...) ← Room opening the encrypted cache
Root cause
System.loadLibrary("sqlcipher") (in DatabaseEncryption.ensureNativeLibraryLoaded()) is only called as a side effect of:
an actual plaintext↔encrypted conversion (DatabaseEncryption.migrate), or
the one-time #111 account migration (AccountDataMigrator.copyAccountTables).
On a steady-state start the cache is already encrypted (so ensureEncrypted early-returns) and the account migration is already done (so it no-ops), so nothing loads the native library before Room opens the keyed DB via SupportOpenHelperFactory → nativeOpen. The previous process only survived because an earlier conversion had loaded the .so in-memory; the next cold start (an upgrade, or any force-stop + relaunch) crashes.
Not an ABI/packaging problem — lib/arm64-v8a/libsqlcipher.so is present in the APK and loads fine; it was simply never asked to load.
Fix
Load the library explicitly in DatabaseProvisioner whenever it commits to an encrypted open (idempotent; no-ops when already loaded), so the invariant "prepareCache() returns Encrypted ⇒ the native lib is loaded" holds for every consumer.
Regression assertions added to DatabaseProvisionerTest: the encrypted path must load the lib; the plaintext path must not.
Verified on a Pixel 10 Pro XL: rebuilt the minified release, adb install -r (in-place update preserving the already-encrypted cache); the app now launches to MainActivity with a stable PID and an empty crash buffer instead of crash-looping.
Closes #210
## Problem
The opt-in encrypted cache crash-loops on launch for any user who has enabled it, on the first cold start after enabling (e.g. an app upgrade):
```
FATAL EXCEPTION: arch_disk_io
java.lang.UnsatisfiedLinkError: No implementation found for long
net.zetetic.database.sqlcipher.SQLiteConnection.nativeOpen(...) — is the library loaded, e.g. System.loadLibrary?
at ...SupportHelper.getWritableDatabase(...) ← Room opening the encrypted cache
```
## Root cause
`System.loadLibrary("sqlcipher")` (in `DatabaseEncryption.ensureNativeLibraryLoaded()`) is only called as a **side effect** of:
- an actual plaintext↔encrypted conversion (`DatabaseEncryption.migrate`), or
- the one-time #111 account migration (`AccountDataMigrator.copyAccountTables`).
On a steady-state start the cache is **already** encrypted (so `ensureEncrypted` early-returns) and the account migration is **already** done (so it no-ops), so nothing loads the native library before Room opens the keyed DB via `SupportOpenHelperFactory` → `nativeOpen`. The previous process only survived because an earlier conversion had loaded the `.so` in-memory; the next cold start (an upgrade, or any force-stop + relaunch) crashes.
Not an ABI/packaging problem — `lib/arm64-v8a/libsqlcipher.so` is present in the APK and loads fine; it was simply never asked to load.
## Fix
Load the library explicitly in `DatabaseProvisioner` whenever it commits to an encrypted open (idempotent; no-ops when already loaded), so the invariant *"`prepareCache()` returns `Encrypted` ⇒ the native lib is loaded"* holds for every consumer.
Regression assertions added to `DatabaseProvisionerTest`: the encrypted path **must** load the lib; the plaintext path **must not**.
## Testing
- **Preflight green**: `assembleDebug`, `testDebugUnitTest`, `lintDebug`, `ktlintCheck`, `detekt` (+ `androidTest` compiles).
- **Verified on a Pixel 10 Pro XL**: rebuilt the minified release, `adb install -r` (in-place update preserving the already-encrypted cache); the app now launches to `MainActivity` with a stable PID and an empty crash buffer instead of crash-looping.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #210
Problem
The opt-in encrypted cache crash-loops on launch for any user who has enabled it, on the first cold start after enabling (e.g. an app upgrade):
Root cause
System.loadLibrary("sqlcipher")(inDatabaseEncryption.ensureNativeLibraryLoaded()) is only called as a side effect of:DatabaseEncryption.migrate), orAccountDataMigrator.copyAccountTables).On a steady-state start the cache is already encrypted (so
ensureEncryptedearly-returns) and the account migration is already done (so it no-ops), so nothing loads the native library before Room opens the keyed DB viaSupportOpenHelperFactory→nativeOpen. The previous process only survived because an earlier conversion had loaded the.soin-memory; the next cold start (an upgrade, or any force-stop + relaunch) crashes.Not an ABI/packaging problem —
lib/arm64-v8a/libsqlcipher.sois present in the APK and loads fine; it was simply never asked to load.Fix
Load the library explicitly in
DatabaseProvisionerwhenever it commits to an encrypted open (idempotent; no-ops when already loaded), so the invariant "prepareCache()returnsEncrypted⇒ the native lib is loaded" holds for every consumer.Regression assertions added to
DatabaseProvisionerTest: the encrypted path must load the lib; the plaintext path must not.Testing
assembleDebug,testDebugUnitTest,lintDebug,ktlintCheck,detekt(+androidTestcompiles).adb install -r(in-place update preserving the already-encrypted cache); the app now launches toMainActivitywith a stable PID and an empty crash buffer instead of crash-looping.🤖 Generated with Claude Code