Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions LESSONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ When a SmallFloatingActionButton contains an Icon with `contentDescription = nul
**`ModalBottomSheetProperties` requires all parameters explicitly in Material3 1.2.x**
The constructor has no default values in this version — passing only `shouldDismissOnBackPress` fails to compile. Always supply all three: `securePolicy = SecureFlagPolicy.Inherit, isFocusable = true, shouldDismissOnBackPress = false`. `SecureFlagPolicy` also needs an explicit import from `androidx.compose.ui.window`.

**Every schema or preference addition must sweep the full-data surfaces: export, import, and delete-all — a reminder comment there is not enforcement**
An app that promises "export all your data" and "delete all your data" has hidden contracts in its backup writer, backup importer, and delete-everything path. New tables and health-bearing preferences silently break all three: the data exists, round-trips nowhere, and survives deletion. A NOTE comment in the delete method asking future authors to keep it in sync did not survive contact with the next table (groups shipped without touching any of the three). The working defence is a checklist step on every schema/preference PR: grep for the export builder, the importer, and the delete-all implementation, and either update each or record why the new data is exempt (pure view state, id-based selections that cannot survive a restore). Restores also need the writer and reader to agree on more than field names: flags like allow-multiple change the SAVE semantics, so an importer that calls the ordinary save path with defaults quietly collapses data that the export faithfully contains.

**A classification defined by negation ("anything but X") silently misclassifies new variants**
`TrackingCategory.isNumeric` was `categoryType != "default"`, which was correct while every non-default type happened to store numbers. Adding the label-valued "yes_no" and "time" types would have silently routed "Yes"/"HH:mm" strings into numeric chart math (`toFloatOrNull()` returning null everywhere) with no compile error, because a negated predicate auto-includes every future variant. When a derived property gates behaviour, define membership positively (enumerate the types that ARE numeric); then a new variant defaults to the safe side and the property's KDoc records why. Grep for `!=` against discriminator fields whenever adding a variant to a string-keyed or enum type.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,8 @@ interface GroupDao {

@Delete
suspend fun deleteGroup(group: Group)

/** Removes every group. Callers must unfile member categories first. */
@Query("DELETE FROM `groups`")
suspend fun deleteAllGroups()
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ interface TrackingCategoryDao {
@Query("UPDATE tracking_categories SET groupId = NULL WHERE groupId = :groupId")
suspend fun clearGroupAssignments(groupId: Long)

/** Unfiles every category. Called before all groups are deleted in a reset. */
@Query("UPDATE tracking_categories SET groupId = NULL")
suspend fun clearAllGroupAssignments()

// ── Values ────────────────────────────────────────────────────────────

@Query("SELECT * FROM tracking_values WHERE categoryId = :categoryId ORDER BY displayOrder ASC, id ASC")
Expand Down
22 changes: 17 additions & 5 deletions app/src/main/java/com/mapgie/goflo/data/export/DataExporter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,25 @@ import java.time.LocalDate
* ACTION_SEND intent pointing at it via FileProvider, ready to pass to
* Context.startActivity(Intent.createChooser(...)).
*
* The cache file lives in context.cacheDir/exports/ and is overwritten on
* every export — no accumulation of old files.
* The cache file lives in context.cacheDir/exports/. Because the file name
* embeds the date, every export first clears the directory so full health
* exports never accumulate in the cache; "Delete All Data" clears it too
* via [clearExportCache].
*/
object DataExporter {

/** Removes every previously written export file from the cache. */
fun clearExportCache(context: Context) {
File(context.cacheDir, "exports").deleteRecursively()
}

private fun freshExportDir(context: Context): File {
clearExportCache(context)
return File(context.cacheDir, "exports").also { it.mkdirs() }
}

fun buildShareIntent(context: Context, json: String): Intent {
val dir = File(context.cacheDir, "exports").also { it.mkdirs() }
val dir = freshExportDir(context)
val file = File(dir, "goflo_export_${LocalDate.now()}.json")
file.writeText(json, Charsets.UTF_8)

Expand All @@ -41,7 +53,7 @@ object DataExporter {
* for sharing a plain-text doctor visit summary.
*/
fun buildTextShareIntent(context: Context, text: String): Intent {
val dir = File(context.cacheDir, "exports").also { it.mkdirs() }
val dir = freshExportDir(context)
val file = File(dir, "goflo_cycle_summary_${LocalDate.now()}.txt")
file.writeText(text, Charsets.UTF_8)

Expand All @@ -65,7 +77,7 @@ object DataExporter {
* pointing at it via FileProvider, ready for Context.startActivity().
*/
fun buildCsvShareIntent(context: Context, csv: String): Intent {
val dir = File(context.cacheDir, "exports").also { it.mkdirs() }
val dir = freshExportDir(context)
val file = File(dir, "goflo_export_${LocalDate.now()}.csv")
file.writeText(csv, Charsets.UTF_8)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -518,11 +518,16 @@ class TrackingRepository(
}

/**
* Deletes all user-created categories and restores built-in categories to visible.
* Tracking logs for deleted categories are cascade-deleted by the FK constraint.
* Period data (periods, symptoms) is untouched.
* Deletes all user-created categories and groups, and restores built-in
* categories to visible. Tracking logs for deleted categories are
* cascade-deleted by the FK constraint. Period data (periods, symptoms)
* is untouched. Groups are category configuration, so a configuration
* reset removes them too (their user-entered names can be sensitive);
* built-in categories are unfiled rather than deleted.
*/
suspend fun resetCategoryConfiguration() {
categoryDao.clearAllGroupAssignments()
groupDao?.deleteAllGroups()
categoryDao.deleteAllCustomCategories()
categoryDao.unarchiveAllSystemCategories()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ fun ExportOptionsDialog(
) {
Text(customStart?.format(dateDisplayFmt) ?: "From")
}
Text("")
Text("to")
OutlinedButton(
onClick = { showEndPicker = true },
modifier = Modifier.weight(1f)
Expand Down Expand Up @@ -200,7 +200,7 @@ fun ExportOptionsDialog(
Text(f.name, style = MaterialTheme.typography.bodyMedium)
Text(
when (f) {
ExportFormat.JSON -> "Full backup can be re-imported"
ExportFormat.JSON -> "Full backup, can be re-imported"
ExportFormat.CSV -> "Spreadsheet-friendly flat table"
},
style = MaterialTheme.typography.bodySmall,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -487,14 +487,14 @@ fun SettingsScreen(
Text("How should the imported data be handled?", style = MaterialTheme.typography.bodyMedium)
Spacer(Modifier.height(4.dp))
Text(
"Merge adds new periods; skips any whose start date already exists. " +
"Merge: adds new periods; skips any whose start date already exists. " +
"Safe if you have already logged some entries on this device.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(Modifier.height(2.dp))
Text(
"Replace deletes everything on this device first, then imports. " +
"Replace: deletes everything on this device first, then imports. " +
"Use when moving all data from your old phone.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
Expand Down Expand Up @@ -555,7 +555,8 @@ fun SettingsScreen(
text = {
Text(
"This will permanently remove all period logs, symptoms, tracking logs, " +
"and notes. Your category configuration is kept. This cannot be undone."
"notes, any saved pregnancy date, and cached export files. Your category " +
"and group configuration is kept. This cannot be undone."
)
},
confirmButton = {
Expand All @@ -576,8 +577,9 @@ fun SettingsScreen(
title = { Text("Reset category settings?") },
text = {
Text(
"This will delete all your custom categories and restore any hidden built-in " +
"categories. Your period logs and tracking history are kept. This cannot be undone."
"This will delete all your custom categories and groups, and restore any hidden " +
"built-in categories. Your period logs and tracking history are kept. " +
"This cannot be undone."
)
},
confirmButton = {
Expand Down Expand Up @@ -1648,7 +1650,7 @@ private fun ExportDataSubScreen(
Column {
Text("Full backup", style = MaterialTheme.typography.bodyMedium)
Text(
"Data plus category names, values, colours, settings, and dashboard pins. Use for transferring to a new phone.",
"Data plus category names, values, colours, groups, settings, and dashboard pins. Use for transferring to a new phone.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Expand Down
Loading
Loading