Export My Data #35
Labels
No Label
P0
P1
P2
release-blocker
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: null/Privacy-Period-Tracker#35
Loading…
Reference in New Issue
No description provided.
Delete Branch "%!s(<nil>)"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
"Your cycle belongs to you" is a promise the app cannot currently keep: there is no way to get the data out.
What is true now
No export exists.
CycleRepositorycan read everything that would go into one —core/data/src/main/kotlin/dev/privacyllc/period/core/data/CycleRepository.kt— and nothing assembles it into a file.What it costs
Data portability is the difference between a local-first app and a data silo, and it is the practical answer to "what if I switch phones". The Batch 06 milestone lands only when "export produces only the user's own data".
What to do
A complete, human-readable export of the user's own records — period records, spotting, and the settings that are theirs — through the system share or save sheet.
Traps
Downloads/and then sharing a path is exactly the pattern that rule exists to prevent. Use the Storage Access Framework or aFileProvidercontent URI, so the file lands where the user chose and nowhere else.docs/security/SECURITY.md's third-party table the moment the user sends it somewhere. The document'sReview triggerfires on "any change to what is stored, exported, backed up or logged" — it is updated in the same commit, not afterwards.cacheDir, and it is deleted after.Why filed and not fixed
Needs the Settings screen to hang off, and the file format is a decision that outlives this batch: whatever ships first is what people's archives will be in.
Verify: Export produces a file the user chose the destination for, containing their period, spotting and settings records and nothing else, with no copy left in app-private or external storage afterwards;
docs/security/SECURITY.mddescribes it; and no cycle date appears in logcat during the flow.Built in
9a0a644.The format, pinned rather than described
One pretty-printed JSON file, plaintext,
application/json, namedrecords-2026-08-19.json. Checked againstNotificationCopy.SENSITIVE_WORDS, because the name shows up in file pickers and sync notifications.It is asserted byte-for-byte against
core/export/src/test/resources/golden-v1.json, which doubles as the documented example soSECURITY.mdlinks at it instead of keeping a second copy that would drift. This is the strongest protection available for something the issue says "outlives this batch".Dates are ISO calendar dates with no timezone and no conversion.
Converters.ktstores aLocalDateas its epoch day precisely so it "cannot carry a timezone by accident", andit renders identically in every time zonerenders the fixture under UTC,Pacific/Kiritimati(+14) andEtc/GMT+12requiring identical bytes — that bug would never fire in the zone it was written in.Plaintext, and that is a decision
An earlier note in this repo said the export would be encrypted. It should not be, and
SECURITY.mdnow records why: this file is the copy that makes a lost Keystore key survivable rather than final — the exact condition written down for ever revisiting database encryption. A passphrase would reproduce the failure that decision was taken to avoid: a forgotten secret and an archive nobody, including this app, can open. §45's "prefer encrypted backup/export formats" is scoped to backup, which this is not.The screen says so before the picker opens: "Your PIN does not protect the file."
"Only the user's own data" is a compile error, not a review comment
:core:exportis pure JVM and depends on:domain:cyclealone.Prediction,PredictionAccuracy,FertilityEstimateandCycleRecordlive in:domain:predictionand are not on its classpath;kotlin("jvm")keepsandroid.os.Buildoff it too. Adding a forecast or a device fact does not compile.The key set is asserted with
assertEquals, nevercontains— a new field is a failing test rather than a silent addition — plus named negative assertions for ids, predictions, confidence, accuracy, device, locale, timezone and the Play entitlement.Excluded with reasons: row ids (monotonic, so gaps disclose how many records the user deleted), timestamps (behavioural — when the user opened the app, not their health record; §14's edit provenance is already carried by
source = EDITED), predictions (derived),adsRemoved(a purchase one file-edit from being granted once import exists),onboardingCompleted(app state).The traps, each addressed
cacheDir, noFileProvider, no path handed to another app.no copy is left anywhere in app-private storagewalkscacheDirafter a successful export and requires it empty. It fails the instant anybody reintroduces write-then-share.checkNoHealthLoggingcoversappandcore/export;print(/println(are matched as substrings, soPrintWriteris absent by construction.Verification
261 JVM tests, 0 failures, 0 skipped. Five guards green:
Key tests:
ExportFormatTest(7, including the golden and the timezone check),DataExporterTest(7, including the empty-cacheDirclause and a failing stream never reported as done).Two new guards, both proved to fail
checkNoSharedStorageWrites— §45's shared-storage ban was enforced by nobody having typed it.checkPermissionsstructurally cannot see it: it matches<uses-permission>, and a<provider>declaring androidx'sFileProvidermerges green, with FileProvider already on the classpath via core-ktx. Proved by swapping the SAF stream forgetExternalFilesDir: exactly one violation, named with its line.checkNoHealthLoggingcompleteness — a module missing frommodulesSeeingHealthDatawas silently exempt with a green build, a hazardapp/proguard-rules.prohas described since before:core:securityand:core:exportexisted. Every module must now appear in that list or in an explicitmodulesWithNoHealthDatawith its reason; being in neither is a violation. Proved by removingcore/exportfrom the list:One deliberate exclusion, recorded rather than overlooked
"Not yet" answers are not in v1. Both readings are defensible, so the tiebreak was reversibility: under the format contract, adding a key later is additive and safe, while removing one after archives exist is breaking. It also keeps the file matching this issue's verify line as literally written.