Skip to content

5.6.0 — Toolchain upgrade, encrypted backup, auto-lock, forced update - #2

Open
aditya-I0063 wants to merge 9 commits into
aditya-190:masterfrom
aditya-I0063:release/5.6.0-hardening
Open

aditya-I0063 wants to merge 9 commits into
aditya-190:masterfrom
aditya-I0063:release/5.6.0-hardening

Conversation

@aditya-I0063

Copy link
Copy Markdown

Second of three stacked PRs. Merge #1 first — this branches from it, so until then the diff here includes #1's commits.

Toolchain

Gradle 9.7.1, AGP 9.4.0, Kotlin 2.3.21, Java 21, Compose BOM 2026.09.00, compileSdk 37, plus the full AndroidX/Room/SQLCipher/Firebase sweep.

Two deviations worth knowing about:

  • Kotlin is 2.3.21, not 2.4.20. No KSP release exists for the 2.4.x line — the newest is 2.3.12, whose API depends on kotlin-stdlib 2.3.20. Hilt and Room both run through KSP here, so 2.4.20 is not buildable today.
  • AGP 9 was a structural migration, not a version bump. It has built-in Kotlin support and rejects the kotlin.android plugin outright. That cascaded into Hilt 2.60.1 (2.57.2 failed with Android BaseExtension not found) and compileSdk 36→37, required by androidx.hilt 1.4.0. targetSdk stays at 36.

androidx.biometric deliberately stays at 1.1.0: despite its age it is the newest stable release, 1.2.0/1.4.0 are alpha-only, and it already supports CryptoObject.

Encrypted backup replaces the plaintext CSV export

Export wrote the entire vault as readable CSV into the public Downloads folder. The .passkey extension was cosmetic; the bytes were credentials in cleartext.

New .pkbak format: AES-256-GCM over GZipped JSON, PBKDF2-HMAC-SHA512 at 600k, written through SAF so the user picks the destination. The full header is GCM additional-authenticated-data, so the KDF parameters cannot be downgraded — there is a test that flips a header byte and asserts rejection. Import runs in one transaction, detects format by magic bytes rather than filename, and maps unknown categories to OTHERS instead of throwing. The legacy CSV stays readable via opencsv, which was already a declared but entirely unused dependency.

The second locale root cause

ViewModels resolved strings against the Application context. Below API 33 — most of this app's supported range — setApplicationLocales applies per-Activity only, so every snackbar came back in the previous language after a switch. Effects now carry UiText unresolved and the UI resolves it.

Security and privacy

  • Auto-lock: there was no re-lock at all. Backgrounding and returning granted access indefinitely. Now ProcessLifecycleOwner-based with a configurable idle timeout, using elapsedRealtime so changing the device clock cannot extend the grace period.
  • Clipboard: EXTRA_IS_SENSITIVE so Android 13+ redacts the paste preview, plus a timed clear that only fires if the clip is still ours.
  • Secrets off disk: DetailViewModel kept in-progress passwords in SavedStateHandle, which the system persists to disk.
  • allowBackup="false", storage permissions removed, FLAG_SECURE pinned on all sheets and dialogs.
  • Forced update now actually forces. It was configured IMMEDIATE but the result callback was empty, so cancelling fell straight through into the vault. Added priority/staleness gating, a real FLEXIBLE path, and a Remote Config min_supported_version_code kill switch — which is what will let you stop 5.7.0 mid-rollout if needed.
  • Password analysis worked only in English. It matched hardcoded English substrings, so across 16 translated locales it reported 0 passwords and a perfect score — a feature that silently lied. Now uses an isSecret column (Room 2→3) plus translated keyword arrays.

Documentation

Privacy Policy.md, README.md and the in-app About/Privacy/Terms all claimed "no internet permission" and "no third-party sharing". That is false in the shipped APK — dumping the merged manifest shows INTERNET, AD_ID and the AdServices permissions arriving from firebase-analytics and the Play update library. All rewritten to be accurate, and translated into all 16 locales.

⚠️ The three long legal/about bodies are machine translations. The technical claims are accurate, but legal register in 16 languages has not been reviewed by a native speaker.

⚠️ Worth revisiting: an advertising ID in a password manager is the hardest line to defend in a Play data-safety form. Dropping firebase-analytics would remove it.

Verification

Clean debug + release + lint green, 44 unit tests, 6 instrumented tests. Hindi and Arabic verified on an API 36 emulator in both light and dark mode. Release APK dropped 28M → 13M from the newer R8.

🤖 Generated with Claude Code

aditya-I0063 and others added 9 commits September 16, 2026 08:05
Onboarding titles/descriptions, Skip/Next and the splash tagline rendered in
English in all 16 locales. Root cause: 14 strings were marked
translatable="false" in values/strings.xml, which both excluded them from every
values-*/ folder and suppressed the MissingTranslation lint check that would
have caught it. The Kotlin was already using stringResource() correctly.

Translating the headings alone would have crashed the app on first launch:
TextHighlighter located the emphasised word with fullText.indexOf("Secure") and
passed the result straight to AnnotatedString.addStyle. For any translated
heading indexOf returns -1, and addStyle(start = -1, ...) throws. The highlight
is now marked up as [[word]] inside each string resource, so translators choose
the emphasised word in their own language, and a pure-Kotlin parser strips the
markers. Unbalanced or absent markers degrade to unstyled text instead of
throwing. Headings auto-size 36-64sp so longer languages do not clip.

Also in this release:

- Arabic was offered by the in-app picker but missing from locales_config.xml,
  so selecting it silently did nothing. locales_config, values-*/ and the picker
  now agree on all 17 locales.
- Saving a preview without renaming it matched itself in the duplicate-heading
  check, reported "heading exists" and discarded the edit.
- DetailViewModel trimmed on create but not on edit, so trailing whitespace
  crept into stored passwords.
- The bottom navigation announced the raw enum name ("BANKS") in every locale,
  and the alpha-0 FAB spacer was reachable by TalkBack.
- The details screen's empty state read "No Previews Found".
- PasswordGenerator used kotlin.random.Random rather than SecureRandom, and did
  not guarantee a character from each selected class.
- Removed 10 string resources that were unused in code (170 entries across all
  locales), so they are not translated needlessly.
- proguard-rules.pro kept net.sqlcipher.**, but the shipped artifact is
  net.zetetic:sqlcipher-android. Those rules matched nothing. The library
  supplies its own consumer rules; verified against mapping.txt that all 59
  net.zetetic classes are kept unrenamed without any app-side rule, so the dead
  rules were removed rather than replaced.

MissingTranslation, ExtraTranslation, ImpliedQuantity and the string-format
checks are now build-breaking errors, which is what stops this recurring.

Verified on an API 36 emulator: onboarding launches without crashing in hi, ar,
de, ta, ja and en, with markers stripped and text localized. 16 unit tests pass,
lint passes, release build is green and the Room schema is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Phase 0 of 5.6.0, landed on its own so the upgrade can be verified in isolation
before any behavioural change is built on top of it.

AGP 9 turned out to be a structural migration rather than a version bump, and
two constraints forced deviations from the plan:

- AGP 9 has built-in Kotlin support and rejects the 'org.jetbrains.kotlin.android'
  plugin outright, so that plugin is removed. Kotlin is still pinned through the
  compose-compiler plugin version, and the resolved compiler is 2.3.21 (verified
  via the buildscript classpath, which overrides AGP's bundled 2.2.10).
- The plan targeted Kotlin 2.4.20, but no KSP release exists for the 2.4.x line:
  the newest KSP is 2.3.12, whose symbol-processing-api depends on kotlin-stdlib
  2.3.20. Since Hilt and Room both run through KSP, the buildable target is the
  2.3.x line. Kotlin is therefore 2.3.21, not 2.4.20.

Also required, and not anticipated by the plan:

- Hilt 2.57.2 failed against AGP 9 with "Android BaseExtension not found";
  upgraded to 2.60.1.
- androidx.hilt 1.4.0 requires compiling against API 37, so compileSdk moves
  36 -> 37. targetSdk stays at 36: compiling against newer APIs does not opt the
  app into new runtime behaviour, and that is a separate decision.
- google-services 4.5.0 and firebase-crashlytics 3.0.8 for AGP 9 compatibility.

Library versions: Compose BOM 2026.09.00, core-ktx 1.19.0, activity-compose
1.13.0, appcompat 1.8.0, lifecycle 2.11.0, navigation-compose 2.10.1, coroutines
1.11.0, datastore 1.2.1, Room 2.8.5, sqlcipher-android 4.19.0, androidx.sqlite
2.7.1, reorderable 3.1.0, firebase-bom 34.19.0. Java and jvmTarget move 22 -> 21
(LTS).

androidx.biometric deliberately stays at 1.1.0: despite its age it is the newest
stable release, and 1.2.0/1.4.0 are alpha-only. It already supports CryptoObject,
which is all the 5.7.0 key work needs, and an alpha does not belong in the auth
path of a password manager.

material-icons-extended is frozen upstream at 1.7.8 and is no longer BOM-managed,
so it now carries an explicit version rather than resolving to nothing.

Other build hygiene:

- local.properties was loaded unconditionally, throwing FileNotFoundException on
  any machine or CI checkout without it. Now guarded.
- BuildConfig.PASS_PHRASE renamed to LEGACY_PASS_PHRASE to make its purpose
  explicit ahead of the 5.7.0 key migration. Same value, so existing vaults still
  open; AppModule carries a FIXME describing the replacement.
- Dropped androidx.core:core-splashscreen, which was declared but never used
  (installSplashScreen is never called and no splash theme references it).
- Added lifecycle-runtime-compose, needed for collectAsStateWithLifecycle.
- Added mockk, turbine, kotlinx-coroutines-test and a testImplementation line for
  truth, which was previously androidTest-only.
- opencsv is retained despite being unused: Phase 3 adopts it for the legacy CSV
  reader.

Verified: clean assembleDebug, assembleRelease and lintDebug all green, 16 unit
tests pass, all 65 net.zetetic classes still kept unrenamed by the newer R8, and
onboarding still renders correctly with no crash in hi and ar on an API 36
emulator. Release APK drops 28M to 13M from the newer R8.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…zation

Completes Phase 1 and brings §2.5 forward, since both rewrite the same code.

ViewModels resolved strings eagerly via UiText.StringResource(...).asString(appContext).
Below API 33 - which is most of this app's supported range, minSdk being 28 -
AppCompatDelegate.setApplicationLocales applies the locale override per Activity
only, so the injected Application context keeps the *system* locale and every
string resolved there came back in the previous language after a user switched
language in Settings. That is a second root cause of "the app renders in
English", independent of the translatable="false" bug fixed in 5.5.2.

UiEvents.ShowSnackBar now carries UiText unresolved and the screens resolve it
against LocalContext, which does honour the override on every API level. UiText
gains a PluralResource variant, and its subtypes are now data classes holding
List<Any> rather than a class with a vararg array field, so effects compare by
value - identity comparison would have silently broken the effect assertions
planned for the ViewModel tests.

The Add/Edit bottom sheet title was a *resolved English string* stored in
SavedStateHandle. It is now a boolean the UI turns into text.

Clipboard moves out of the ViewModels into a CopyToClipboard effect handled by
the UI, which is also where it belongs given Android 10+ only lets a focused app
write the clipboard. The new SecureClipboard sets EXTRA_IS_SENSITIVE so Android
13+ redacts the paste preview, and clears the clip after 30s - but only if the
clip is still the one we wrote, checked by ClipDescription timestamp, so we never
wipe something the user copied from another app meanwhile. Auto-clear is
best-effort by construction and is documented as such: a delayed clear that fires
while backgrounded is a silent no-op on API 29+. Copying an entry *heading* is
marked non-sensitive; only the secret itself is. On Android 13+ the app's own
"copied" snackbar is suppressed because the system shows its own confirmation.

PreviewViewModel and DetailViewModel no longer take an Application at all.

Remaining localization work from the plan:
- All 19 hardcoded contentDescriptions addressed. Actionable controls get
  localized strings; decorative branding images get null, because a screen reader
  announcing "App Icon" next to a visible title is noise.
- Hardcoded "Password Settings", "Length: N", "Invalid file selected." and the
  "$score%" percentage extracted - percent placement is locale-dependent.
- First plurals resource in the project, replacing a singular-only "used N times".
  Quantity classes follow CLDR per language rather than mirroring English: Arabic
  gets six forms, Russian four, Spanish/French/Italian/Portuguese three, and
  Japanese/Korean/Chinese only "other".
- 15 new strings plus the plural translated into all 16 locales.
- Added values-night: the only theme hardcoded windowLightStatusBar=true against
  a Light parent, so night mode showed dark status-bar icons on a dark bar and
  flashed a white window on cold start. windowLightStatusBar is now unset so
  enableEdgeToEdge's SystemBarStyle.auto owns bar contrast, and an explicit
  windowBackground is defined for both modes. res/values/colors.xml was an empty
  <resources/> and now holds those.

Lint caught a genuine defect during this work: context.getString inside a
composable is not configuration-aware. Hoisted into composable scope.

Verified: clean debug build, 16 unit tests, and lint all green; onboarding renders
correctly in Hindi with no crash in both light and dark mode on an API 36
emulator.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three pieces of security hygiene plus the repository support Phase 3 needs.

FLAG_SECURE was set on the Activity window only. Compose Dialog and
ModalBottomSheet are separate windows: under the currently resolved Compose
versions their securePolicy defaults to SecureFlagPolicy.Inherit, so they do
inherit it today and this was a latent risk rather than an active leak - but it
breaks silently if a default changes or a sheet is ever hosted off a non-secure
window, and the password-analysis sheet renders weak and reused passwords. All
five ModalBottomSheets and all three dialogs now pin SecureFlagPolicy.SecureOn
explicitly rather than relying on inheritance.

DetailViewModel kept the in-progress entry title and answer in SavedStateHandle,
including freshly generated passwords. SavedStateHandle is serialized into the
saved-instance-state bundle, which the system persists to disk under
/data/system_ce/<user>/ - so cleartext secrets were being written outside the
encrypted database. Both move to plain in-memory MutableStateFlows. The trade is
that a half-typed entry is lost to process death, which is the right side of that
trade for a password manager. The Add/Edit flag stays in SavedStateHandle, being
a boolean rather than vault content.

Added PasskeyRepository.runInTransaction and deleteAll, backed by Room's
withTransaction, and used them immediately to fix reordering: both Preview and
Detail reorder wrote N sequence updates in a bare loop, so an interruption left
the list partially renumbered. Phase 3's import needs the same primitive to stop
a malformed row leaving a half-populated vault.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Export wrote the entire vault as readable CSV into the public Downloads folder.
The .passkey extension and application/passkey MIME type were cosmetic: the bytes
were credentials in cleartext, visible to any app with media access and swept up
by cloud backup tools. This is the single worst data-handling defect in the app.

New .pkbak format: "PKBACKUP" magic, format version, KDF id and parameters, salt,
nonce and length, followed by AES-256-GCM ciphertext over GZIPped JSON. The whole
51-byte header is passed to GCM as additional authenticated data, so the KDF
identifier and its parameters are covered by the auth tag - otherwise an attacker
could rewrite kdfId to name a weaker KDF and the file would still verify. There is
a test for exactly that.

Keys come from PBKDF2-HMAC-SHA512 at 600k iterations. Argon2id is stronger against
GPU attack and is what the kdfId field exists to allow; it needs a native
dependency and is planned alongside the 5.7.0 key work, at which point new files
get kdfId 2 and this reader keeps handling kdfId 1.

Files are written through SAF CreateDocument so the user picks the destination,
using "wt" so an existing file is truncated rather than partially overwritten. The
backup password is collected only after a destination is chosen, and entered twice
with an explicit warning that it cannot be recovered.

Import is hardened throughout:
- format detection is by magic bytes, not filename. The old check matched a
  display name ending in "passkey", which would have rejected every new backup and
  trusted a user-supplied name to decide how to parse the contents.
- the whole restore runs in one transaction, so a bad row part-way through a file
  can no longer leave a half-populated vault.
- unknown categories map to OTHERS instead of Categories.valueOf throwing into a
  broad catch that silently abandoned the rest of the file.
- field lengths, row counts and file size are bounded against a hostile file.
- a wrong password and a corrupt file are deliberately indistinguishable, since
  GCM cannot tell them apart and pretending otherwise leaks whether a guess was
  close.

The legacy plaintext CSV stays readable so existing backups still restore, now via
opencsv - which was already a declared but entirely unused dependency - so quoted
values containing commas parse correctly rather than being split blindly. Rows with
the wrong column count are dropped and counted rather than aborting the import. The
lossy "____" comma sentinel is un-escaped on read; nothing can recover a value that
genuinely contained it, but that is what the file means. Only the reader survives;
the CSV writer is gone.

SettingsViewModel drops from 340 to 212 lines with the file I/O and MediaStore code
removed.

Manifest, now that SAF has removed the need for storage access:
- READ_/WRITE_EXTERNAL_STORAGE deleted. They were capped at maxSdkVersion 28 while
  minSdk is 28, so they were live on exactly the oldest supported devices.
- allowBackup="false". Platform backup would carry the SQLCipher database off the
  device, and with today's build-time key that copy is decryptable by anyone with
  the APK. backup_rules.xml and data_extraction_rules.xml were untouched IDE
  templates and are now correct regardless, so flipping the flag back cannot
  silently start uploading the vault.
- INTERNET is declared explicitly. The app makes no network calls itself, but
  Firebase and Play in-app-update require it, and having it merged in invisibly
  from a library manifest is how the README came to claim otherwise. The merged
  manifest of a built APK also shows AD_ID and the AdServices permissions arriving
  from firebase-analytics, which the privacy rewrite has to disclose.
- MainActivity is singleTask so a second instance cannot come up holding a stale
  unlocked session once app-lock lands.

24 new unit tests cover the crypto round-trip, tamper detection on both ciphertext
and header, and the legacy CSV reader including the shapes the old exporter got
wrong. Round-tripping a value containing commas, newlines, the ____ sentinel and
emoji is now covered, none of which survived the old format.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
In-app updates were configured as AppUpdateType.IMMEDIATE - Play's blocking
flavour - but were bypassable, because registerForActivityResult was given an
empty callback. On RESULT_CANCELED or RESULT_IN_APP_UPDATE_FAILED nothing
happened and the app continued straight into the vault, so a single back press
dropped the gate for the whole session. It also blocked on every available update
however trivial, since updatePriority and clientVersionStalenessDays were never
consulted, and its FLEXIBLE branch was unreachable because updateType was a
hardcoded val with no listener or completeUpdate() to make it work.

Extracted into AppUpdateController, which:
- treats a non-OK result from a required update as "still required" and shows a
  terminal screen with no path into the vault;
- reserves IMMEDIATE for updatePriority >= 4 and uses FLEXIBLE once an update is
  a week stale, so ordinary releases no longer interrupt anyone. Note that
  inAppUpdatePriority can only be set through the Play Developer Publishing API at
  release-creation time and is immutable afterwards, so it belongs on the 5.7.0
  release checklist rather than being discovered later;
- implements FLEXIBLE properly with an InstallStateUpdatedListener registered in
  onStart and released in onStop, surfacing a non-blocking banner when the
  download completes. Deliberately not the blocking screen: a low-priority update
  that is merely ready should not lock someone out of their passwords, and calling
  completeUpdate() unprompted would restart the app from under them;
- keeps the existing DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS resume, which was the
  one part of the original that was already right.

Added a Remote Config min_supported_version_code gate. In-app updates are entirely
client-side and do nothing for a sideloaded install or a device without Play, so
this is the only mechanism that genuinely enforces a floor - and it is the kill
switch if 5.7.0's key migration needs stopping mid-rollout. Failure to reach
Remote Config leaves the app usable rather than bricking it.

This has to ship in 5.6.0 precisely because 5.7.0 re-keys every existing user's
database and is the release most worth forcing.

Note it still does not remove the need to keep LEGACY_PASS_PHRASE indefinitely:
a forced update shortens the tail of old installs but cannot eliminate it, since
someone who never opens the app is never prompted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was no re-lock at all. Once past the biometric screen, backgrounding the
app and returning granted full access indefinitely, with no idle timeout, and
SecurityViewModel's isAuthenticated flag was written but never read - so
navigation was the only thing standing between a picked-up phone and every stored
password.

VaultSession now holds the unlocked state that the rest of the app observes, and
AppLockObserver watches ProcessLifecycleOwner rather than an Activity, so a
configuration change or an internal screen transition is not mistaken for leaving
the app. Timings use SystemClock.elapsedRealtime, never currentTimeMillis: wall
clock lets someone extend their own grace period by changing the device clock, and
it jumps on NTP sync.

Foreground idle is driven by Activity.onUserInteraction, which is dispatched
before the window handles the event and therefore sees touches and key presses
without competing with Compose gesture consumption - considerably more robust than
wrapping the tree in a pointerInput.

On lock the clipboard is cleared first, since once locked there is no UI left to
offer that, and navigation pops to the security gate with popUpTo(0) so every
screen holding vault content is destroyed along with its nav-scoped ViewModel.

The re-lock navigation is deliberately gated on the lock reason rather than merely
"not unlocked": ColdStart is the initial state, and treating it as a lock would
yank new users out of onboarding straight into a biometric prompt.

Timeout is user-configurable in Settings - Immediately / 15s / 30s (default) / 1m /
5m / Never - persisted in DataStore. "Never" is a genuine footgun in a password
manager, so choosing it surfaces an explicit warning rather than hiding behind a
neutral label. All six options and the warning are translated into all 16 locales.

In this release the session tracks lock state only; the database key is still the
build-time constant. In 5.7.0 VaultSession also owns the decrypted key, and
locking will zero it rather than flipping a boolean.

Verified on an API 36 emulator: onboarding is unaffected on a fresh install, and a
background/foreground cycle does not crash.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PasswordAnalyzer decided whether a row held a secret by checking the question for
the hardcoded English substrings "password", "pin", "code" and "secret". Across
the app's 16 non-English locales nothing ever matched, so totalPasswords came back
0 and strengthScore came back 100: every translated user was told their vault was
perfect and had no weak or reused passwords. A feature that silently lies is worse
than one that is absent.

Two changes fix it. Details gains an isSecret column (Room 2 -> 3, purely additive
with a default, so no table rebuild) which is set explicitly when a value comes out
of the password generator. And keyword matching is no longer hardcoded: the caller
supplies R.array.secret_field_keywords, now translated for all 16 locales, unioned
across the default and current locale so entries labelled before a language switch
keep being classified. Matching is case-folded and accent-stripped, so
"Contraseña" matches "contrasena".

A SQL backfill in the migration was considered and rejected: the only heuristic
expressible there is English keyword matching, which is precisely the bug. Existing
rows default to 0 and continue to be classified by the now-localized keywords.

Two scoring defects fixed while here:
- reuse was penalised per distinct duplicated value rather than per affected entry,
  so three copies of one password scored the same as two.
- reusedPasswords was keyed by the plaintext password, and that map is held in
  ViewModel state and rendered by the analysis sheet. It is now keyed by a
  truncated SHA-256 digest; there is no reason for cleartext passwords to sit in a
  composition-scoped object.

Also added an all-identical-characters check to the weak-password heuristic.

9 new unit tests cover the locale behaviour, the isSecret flag, accent-insensitive
matching, the weak heuristic and the scoring caps. MigrationTester gains 2 -> 3 and
a full 1 -> 3 chain test, since someone who skipped several releases upgrades
straight to the newest build; all 6 instrumented tests pass on an API 36 emulator.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…l locales

The in-app About, Privacy and Terms text, README.md and Privacy Policy.md all
stated that the app requires no internet permission and shares nothing with third
parties. That was false in the shipped APK. Dumping the merged manifest of a build
shows INTERNET, ACCESS_NETWORK_STATE, WAKE_LOCK, AD_ID and the Play AdServices
permissions all arriving from firebase-analytics and the Play update library. The
repo's Privacy Policy.md simultaneously contradicted the in-app copy by describing
IP address and device name collection.

All three in-app texts are rewritten to be accurate and, deliberately, far shorter.
The previous About text was several hundred words of marketing that also claimed
LiveData and MVVM (the app uses Flow), dark mode (half-implemented until this
release) and Autofill (still not implemented). Accurate, concise copy is both
better for users and tractable to translate.

The rewritten text states plainly: vault entries are never collected, transmitted
or stored by us and cannot be recovered by us; Firebase Crashlytics, Analytics and
Performance Monitoring plus Play in-app updates send crash diagnostics, usage
events, performance traces and device/installation identifiers including an
advertising ID to Google, and never have access to vault contents; backups are the
user's responsibility once written; and the vault is excluded from Android cloud
backup and device transfer.

Privacy Policy.md is rewritten around the actual architecture with a per-component
table of what each one sends and a permission-by-permission justification.
README.md drops "Fully Offline : No internet permission required", gains a security
model section, and states the current database-key limitation openly rather than
letting the feature list imply protection the build does not yet provide.

With the text corrected it is now safe to translate, which completes the decision
to localize everything: about_message, privacy_message and terms_n_condition_message
are translated into all 16 locales. app_name is the only remaining
translatable="false" string, being the brand name.

These three are long-form legal and descriptive text produced by machine
translation. They should get a native-speaker review pass before release; the
technical claims in them are accurate, but phrasing and legal register have not
been checked by a human for each language.

Version bumped to 5.6.0 (versionCode 45).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant