clockula-backup-v1.json, schema documented in docs/BACKUP.md per PLAN.md §7: alarms, timers and world clocks as configuration only — ring state and a timer's running anchors never leave the device, since neither means anything on another one. Unknown fields are ignored at any level; a malformed known field rejects the whole file rather than importing it half-way. Import is whole-state replacement, not a merge, and the backup screen says so before the user confirms. Before replacing: a ringing alarm is dismissed and every running timer is deleted, so the import can never strand a live ring session; after it, AlarmEngine and TimerEngine are told to re-resolve so the imported state is live immediately. BackupDao adds delete-all/insert-many queries under one transaction — no schema or version change, queries only.
138 lines
5.3 KiB
Markdown
138 lines
5.3 KiB
Markdown
# Clockula backup format
|
|
|
|
`clockula-backup-v1.json` — a documented, versioned JSON export of Clockula's
|
|
own data, written to (and read from) a user-chosen folder via SAF
|
|
(`docs/PLAN.md` §7). This file is the schema's source of truth; the codec
|
|
(`data/backup/BackupDto.kt`, `BackupCodec.kt`) implements exactly what is
|
|
written here.
|
|
|
|
## Top level
|
|
|
|
```json
|
|
{
|
|
"format": "clockula-backup",
|
|
"version": 1,
|
|
"exportedAt": 1700000000000,
|
|
"alarms": [ … ],
|
|
"timers": [ … ],
|
|
"worldClocks": [ … ],
|
|
"settings": { … }
|
|
}
|
|
```
|
|
|
|
- `format` — must be exactly `"clockula-backup"`.
|
|
- `version` — must be exactly `1`. Any other value rejects the whole file:
|
|
Clockula does not guess at a schema it has not implemented.
|
|
- `exportedAt` — epoch milliseconds, informational only (not re-imported).
|
|
|
|
## Parsing rules
|
|
|
|
- **Tolerant of unknown fields, at any level.** An older or newer client's
|
|
extra field is ignored, never an error.
|
|
- **Strict about known fields.** A malformed value in a field this schema
|
|
defines — `format`, `version`, an alarm's `hour`/`minute`, a world clock's
|
|
`zone_id`, a timer's `duration_millis` — rejects the **whole file** with a
|
|
specific reason. Nothing is imported half-way.
|
|
- Settings values (snooze minutes, volume ramp, timer duration, …) are
|
|
**not** range-checked at parse time; they pass through the same clamps
|
|
`ClockPrefs`/`SettingsPrefs` already apply on every write, so a value from
|
|
a future, more permissive version degrades to the nearest valid one instead
|
|
of failing the import.
|
|
- An enum field with a name this version does not recognise (a `dismiss_
|
|
challenge` or `theme_mode` from a future version, say) degrades to that
|
|
field's default, the same forgiving-read posture `AlarmMapper` already uses
|
|
for a corrupt Room row — it does not fail the import.
|
|
|
|
## Import semantics
|
|
|
|
Import **replaces all state** — alarms, timers and world clocks are deleted
|
|
and re-created from the file, and every setting the file carries is written
|
|
over the current value. This is not a merge, and the backup screen says so
|
|
before the user confirms. A ringing alarm is dismissed and any running timer
|
|
is deleted *before* the replace, so the import never leaves an orphaned ring
|
|
session; alarm/timer/stopwatch scheduling is re-resolved immediately after.
|
|
|
|
Every imported alarm and timer gets a **fresh id and fresh timestamps** —
|
|
the backup carries no id and no `created_at`/`updated_at`, since those are
|
|
local-database facts, not portable ones.
|
|
|
|
## Alarms
|
|
|
|
One object per row of `alarms`, config only:
|
|
|
|
| Field | Type | Notes |
|
|
|---|---|---|
|
|
| `hour` | Int | 0..23. Out of range rejects the whole file. |
|
|
| `minute` | Int | 0..59. Out of range rejects the whole file. |
|
|
| `label` | String | |
|
|
| `enabled` | Boolean | |
|
|
| `repeat_days` | Int | The 7-bit mask (`RepeatDays`); bit *n* is ISO day *n+1*. |
|
|
| `skip_next_occurrence` | Boolean | |
|
|
| `ringtone_uri` | String? | Exported as-is. See "Ringtone URIs" below. |
|
|
| `vibrate` | Boolean? | `null` = inherit the app default. |
|
|
| `snooze_minutes` | Int? | `null` = inherit. |
|
|
| `snooze_limit` | Int? | `null` = inherit. |
|
|
| `volume_ramp_seconds` | Int? | `null` = inherit. |
|
|
| `dismiss_challenge` | String? | `NONE`/`MATH`/`HOLD`, or `null` = inherit. |
|
|
| `delete_after_use` | Boolean | A transient, contract-created alarm. |
|
|
|
|
**Not exported:** the `alarm_states` ring-state table (snooze/ringing/handled-
|
|
occurrence bookkeeping) — it is volatile, per-device state, not configuration,
|
|
and `docs/ARCHITECTURE.md` §4/§6 are explicit that it must never appear here.
|
|
|
|
## Timers
|
|
|
|
One object per row of `timers`, config only:
|
|
|
|
| Field | Type | Notes |
|
|
|---|---|---|
|
|
| `label` | String | |
|
|
| `duration_millis` | Long | ≥ 0. Negative rejects the whole file. |
|
|
| `ringtone_uri` | String? | |
|
|
| `sort_order` | Int | |
|
|
| `delete_after_use` | Boolean | |
|
|
|
|
**Not exported:** `state`, `remaining`, the elapsed-realtime anchors, or
|
|
`ends_at_wall_clock` — a timer's running state is meaningless on another
|
|
device or after a reboot (`docs/PLAN.md` §5). Every imported timer lands
|
|
`IDLE`, with `remaining` equal to `duration`.
|
|
|
|
## World clocks
|
|
|
|
| Field | Type | Notes |
|
|
|---|---|---|
|
|
| `zone_id` | String | Must be a zone id the *exporting* device's IANA tzdata knows (`Zones.isValid`). A bad id rejects the whole file. |
|
|
| `label` | String? | |
|
|
| `sort_order` | Int | |
|
|
|
|
## Settings
|
|
|
|
```json
|
|
"settings": {
|
|
"theme_mode": "SYSTEM",
|
|
"dynamic_color": true,
|
|
"default_snooze_minutes": 10,
|
|
"default_snooze_limit": 3,
|
|
"default_vibrate": true,
|
|
"default_volume_ramp_seconds": 15,
|
|
"default_alarm_ringtone_uri": null,
|
|
"default_timer_ringtone_uri": null,
|
|
"default_dismiss_challenge": "NONE",
|
|
"default_timer_duration_millis": 300000,
|
|
"home_zone_id": null,
|
|
"timer_presets": [60000, 120000, 180000, 300000, 600000, 900000, 1800000, 3600000]
|
|
}
|
|
```
|
|
|
|
Field names mirror `ClockPrefs`'/`SettingsPrefs`' own on-disk keys. Every
|
|
numeric/duration value is clamped on *apply*, through the existing pref
|
|
setters — never on parse.
|
|
|
|
## Ringtone URIs
|
|
|
|
Exported exactly as stored (a `content://` URI, typically). On import the
|
|
URI is written back unchanged; if the *importing* device cannot resolve it
|
|
at ring/preview time, the existing ringtone-resolution fallback already used
|
|
elsewhere in the app applies (falls through to the device default) — this is
|
|
not special-cased in the backup path itself.
|