Files
clockula/docs/BACKUP.md
T
makiolaj df8be5e158 Merge branch 'feat/26-quick-settings-tiles' into release/v1.0.0
# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/de/jeanlucmakiola/clockula/backup/BackupEngine.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/data/backup/BackupCodec.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/data/backup/BackupDto.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/data/prefs/SettingsPrefs.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/domain/backup/BackupContent.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/ui/common/AlarmTimeFormatter.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/ui/widget/NextAlarmWidgetContent.kt
#	app/src/main/res/values/strings.xml
#	app/src/test/java/de/jeanlucmakiola/clockula/ManifestRulesTest.kt
#	app/src/test/java/de/jeanlucmakiola/clockula/data/backup/BackupCodecTest.kt
#	app/src/test/java/de/jeanlucmakiola/clockula/data/prefs/SettingsPrefsTest.kt
#	docs/ARCHITECTURE.md
#	docs/BACKUP.md
#	docs/ROADMAP.md
2026-10-26 21:00:57 +01:00

11 KiB
Raw Blame History

Clockula backup format

clockula-backup-v1.json is 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": 2,
  "exportedAt": 1700000000000,
  "alarms": [ … ],
  "timers": [ … ],
  "worldClocks": [ … ],
  "settings": { … }
}
  • format: must be exactly "clockula-backup".
  • version: 2 on export. Import accepts 1 and 2; any other value rejects the whole file, since Clockula does not guess at a schema it has not implemented. Version 2 adds an alarm's scheduled_date, pause_start and pause_end; a version 1 file is a version 2 file without them, and imports with every alarm unscheduled and unpaused. The bump is what keeps an older Clockula from importing a v2 file and silently turning a dated alarm into tomorrow's, or a paused one back on. The file name stays clockula-backup-v1.json so an existing backup folder keeps working.
  • 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 or dates, 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 alarm's own snooze_minutes, snooze_limit and volume_ramp_seconds are clamped at parse time to the same ranges as the defaults they override, and a timer's duration_millis to the setup range (1 second to 24 hours). Room has no clamp of its own, so this is where it happens.
  • A missing default_timer_vibrate (a file from before timers had their own vibration setting) restores default_vibrate in its place: that is what the exporting install's timers vibrated by. A missing default_timer_volume_ramp_seconds restores 0, the ring timers had then.
  • A missing timer_presets keeps the built-in presets. An empty list is a user who removed them all, and is imported as such.
  • A missing quick_tile_duration_millis (a file from before the Quick Settings tiles), or one outside the quick-timer range of 1 second to 59:59, keeps the quick-timer tile's default of five minutes — the same reading the quick-timer widget gives its own stored duration.
  • 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 and export are limited to 8 MiB. Import also rejects more than 10,000 alarms, 10,000 timers, 1,000 world clocks, oversized labels or ringtone URIs, and more than 1,000 timer presets. These limits keep a picked document from exhausting memory or creating an impractical number of rows.

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. Settings are written in one DataStore transaction; the three Room tables are replaced in one database transaction. The live ring and timer services are paused while the tables change. If the database replace fails, the old rows remain, the previous settings are restored, and the ring and timer registrations are resumed. Scheduling is reconciled after either outcome. An imported timer always starts idle.

Export writes a complete temporary document before removing an older backup. If the provider refuses the final rename, the complete temporary document is kept in the chosen folder and the app reports the failure with its filename.

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.
scheduled_date String? v2. ISO-8601 local date (2026-10-28) a one-shot alarm rings on. Ignored on a repeating alarm.
pause_start String? v2. First day of a repeating alarm's pause, ISO-8601. Ignored on a one-shot alarm.
pause_end String? v2. Last day of the pause, inclusive. Present exactly when pause_start is.

A date that does not parse, lies outside 2000-01-01..9999-12-31, a pause with only one end, or a pause ending before it starts rejects the whole file, like an out-of-range hour. A date on a repeating alarm or a pause on a one-shot is ignored unread, valid or not, which is what the editor would do with it.

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_timer_vibrate": false,
  "default_timer_volume_ramp_seconds": 0,
  "default_dismiss_challenge": "NONE",
  "home_zone_id": null,
  "default_alarm_silence_after_minutes": 10,
  "default_timer_silence_after_minutes": 10,
  "volume_buttons_while_ringing": "CONTROL_VOLUME",
  "world_clock_show_seconds": false,
  "automatic_local_clock": true,
  "timer_presets": [60000, 120000, 180000, 300000, 600000, 900000, 1800000, 3600000],
  "bedtime": {
    "enabled": false,
    "bedtime_minutes": 1350,
    "wake_minutes": 420,
    "days": 127,
    "reminder_minutes": 0,
    "grayscale": true,
    "dim_wallpaper": true,
    "dark_theme": true,
    "hide_always_on_display": false
  },
  "screen_saver_style": "DIGITAL",
  "screen_saver_night_mode": false,
  "quick_tile_duration_millis": 300000
}

bedtime is the opt-in bedtime schedule (issue #1). Its times are minutes since local midnight, days is the same 7-bit mask as an alarm's repeat_days (the nights bedtime is for, named by their evening), and reminder_minutes 0 means no reminder. A file without bedtime (any file written before it existed) leaves the device's current bedtime settings as they are, the one setting a restore does not replace when its file is silent about it. Like the other settings it is clamped rather than rejected: times into a day, the reminder into 0–120 minutes, and a days of 0 (which the screen never allows) into every night. The id of the Do Not Disturb rule a device created is not part of it; the importing device creates its own (docs/ARCHITECTURE.md §18).

Field names mirror ClockPrefs'/SettingsPrefs'/ScreenSaverPrefs' own on-disk keys. A file from before the screen saver has neither screen_saver_* field and restores its defaults (digital, night mode off); an unknown style degrades to DIGITAL rather than rejecting the file. 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. volume_buttons_while_ringing is one of CONTROL_VOLUME/SNOOZE/DISMISS/NOTHING; a file without it (any backup made before the setting existed) or with a name this version does not know restores CONTROL_VOLUME, the behaviour that file's device had.

default_alarm_silence_after_minutes and default_timer_silence_after_minutes are how long a ringing alarm or a finished timer keeps going before it stops on its own: whole minutes, with 0 meaning "Never" (which still stops after an hour). A file without them, from before the setting existed, imports as 10, the value it was made with. A value of 60 or more reads as 0, "Never" (which rings for that long anyway); a negative one reads as the default, 10.

Files from before world_clock_show_seconds existed import with it off, its default. It travels with the settings but is not a clock default: nothing an alarm or timer inherits depends on it. A file without automatic_local_clock (one written before it existed) restores it as true, its default.

Every numeric/duration value is clamped on apply using the same ranges and encoding as the existing preference 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.