Files
clockula/docs/BACKUP.md
T
makiolaj cac5b2dfca refactor(settings): remove the unused default timer duration
Settings > Timers offered a "Default duration", but nothing that creates
a timer ever read it: the keypad starts empty. The row, its preference,
ClockDefaults.timerDuration and the backup field are gone, and the
screen's hint and the hub's subtitle now only speak of the sound.

Backups no longer write default_timer_duration_millis. Older files that
carry it still import, since the codec ignores unknown keys.

Closes #5
2026-10-05 19:28:31 +02:00

5.4 KiB

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

{
  "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, …) 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

"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",
  "home_zone_id": null,
  "timer_presets": [60000, 120000, 180000, 300000, 600000, 900000, 1800000, 3600000]
}

Field names mirror ClockPrefs'/SettingsPrefs' own on-disk keys. Older files may also carry default_timer_duration_millis, from a default timer length setting that has since been removed; it is ignored on import like any other unknown field. 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.