89 lines
6.5 KiB
Markdown
89 lines
6.5 KiB
Markdown
# Architecture: Uhr (Android, ohne Google)
|
|
|
|
Basis: `recon.md`, `features.csv`. Same conventions as `wetter/`: one native app, no server, no accounts.
|
|
**No network permission at all.** World clock cities come from the system time zone database, sounds from the device and the app.
|
|
|
|
## Stack
|
|
|
|
| Schicht | Wahl | warum |
|
|
| --- | --- | --- |
|
|
| Sprache/UI | Kotlin + Jetpack Compose, Material 3 Expressive | same as Wetter, shared look and know-how |
|
|
| Persistenz | Room (alarms, instances, timers, cities) + DataStore (settings, stopwatch, bedtime) | relational where it matters, key-value for singletons |
|
|
| Alarme | `AlarmManager.setAlarmClock()` | exempt from Doze, shows the alarm icon in the status bar, the only API that is reliable for wake-up alarms |
|
|
| Klingeln | foreground service (type `systemExempted`/`mediaPlayback`) + full-screen-intent notification → ringing activity over the lock screen | the pattern Android expects for alarm clocks |
|
|
| Timer/Stoppuhr | state in DB/DataStore, `end_at` / `elapsedRealtime` based; foreground service with live notification only while running | survives process death; no ticking in the background |
|
|
| Widgets | Jetpack Glance + RemoteViews `TextClock`/`AnalogClock` | clocks tick without app updates |
|
|
| Bildschirmschoner | `DreamService` | system screensaver API |
|
|
| Ortsliste Weltuhr | `java.time.ZoneId` + localized names from ICU (`android.icu.util.TimeZone` display names, exemplar cities) | offline, no city API needed |
|
|
| Töne | `RingtoneManager` (system alarm sounds) + own files via `ACTION_OPEN_DOCUMENT` (persisted URI permission) + bundled CC0 sounds | no streaming services |
|
|
| Build/Verteilung | Gradle KTS, minSdk 29, targetSdk 36; release via `scripts/release.sh uhr` (Obtainium) | same pipeline as Wetter |
|
|
|
|
## Permissions
|
|
|
|
| permission | why |
|
|
| --- | --- |
|
|
| `USE_EXACT_ALARM` (API 33+), `SCHEDULE_EXACT_ALARM` (≤32) | alarm clock apps may use exact alarms without asking |
|
|
| `USE_FULL_SCREEN_INTENT` | ringing screen over the lock screen (Android 14+: check `canUseFullScreenIntent`, guide the user to the setting) |
|
|
| `POST_NOTIFICATIONS` | upcoming, snoozed, missed, timer, stopwatch |
|
|
| `FOREGROUND_SERVICE` (+ type) | ringing, running timers |
|
|
| `RECEIVE_BOOT_COMPLETED` | reschedule after reboot |
|
|
| `VIBRATE`, `WAKE_LOCK` | ringing |
|
|
|
|
No `INTERNET`, no location.
|
|
|
|
## Schema
|
|
|
|
See `schema.sql`: 4 tables (`alarms`, `alarm_instances`, `timers`, `cities`), singletons in DataStore. Access rules: single user, app-private storage, no RLS needed.
|
|
Hard constraints: one bedtime wake alarm (partial unique index); a running timer always has `end_at` (check constraint); one instance row per alarm (primary key).
|
|
|
|
## Interne API (Repositories / Use Cases)
|
|
|
|
| Aufruf | macht | Flow |
|
|
| --- | --- | --- |
|
|
| `AlarmRepo.observe()` | alarms + next instance, sorted by time | F01 |
|
|
| `AlarmRepo.upsert(alarm)` | save, then `Scheduler.reschedule(alarm)` | F01, F02 |
|
|
| `AlarmRepo.delete(id)` / `undo` | delete, cancel PendingIntent | F02 |
|
|
| `Scheduler.reschedule(alarm)` | compute next fire (days, skip_until, DST-safe via `ZonedDateTime`), write instance, `setAlarmClock` | all |
|
|
| `Scheduler.rescheduleAll()` | on boot, `TIME_SET`, `TIMEZONE_CHANGED`, app update | — |
|
|
| `Ringing.start(alarmId)` | foreground service: sound (gradual volume), vibration, full-screen activity, silence-after timeout | F03 |
|
|
| `Ringing.snooze()` / `dismiss()` | instance → snoozed (fire_at + snooze) / next occurrence | F03 |
|
|
| `AlarmRepo.dismissNext(id)` | skip the upcoming ring only | F04 |
|
|
| `TimerRepo.add/pause/resume/addMinute/reset/delete` | state transitions with `end_at`, schedule `setExactAndAllowWhileIdle` for the end | F05 |
|
|
| `Stopwatch.start/stop/lap/reset` | DataStore singleton | F06 |
|
|
| `CityRepo.search(q)` / `add` / `reorder` / `delete` | offline zone search | F07 |
|
|
| `Bedtime.save(schedule)` | writes bedtime + the wake alarm (`is_bedtime_wake`) + reminder alarm | F08 |
|
|
| `Sounds.list()` / `preview(uri)` | ringtones + bundled + picked files | F09 |
|
|
|
|
### Background
|
|
|
|
| Auslöser | macht |
|
|
| --- | --- |
|
|
| `setAlarmClock` fire → `AlarmReceiver` | start ringing service |
|
|
| 2 h before an alarm (inexact alarm) | "upcoming alarm" notification with "Dismiss now" |
|
|
| silence-after timeout | stop ringing, instance → missed, missed notification |
|
|
| `BOOT_COMPLETED`, `TIME_SET`, `TIMEZONE_CHANGED`, `MY_PACKAGE_REPLACED` | `rescheduleAll()` |
|
|
| timer end (exact alarm) | timer ringing |
|
|
| bedtime reminder (exact) | reminder notification |
|
|
|
|
## Was beißt
|
|
|
|
- **Time zones and DST:** alarms are wall-clock; the next fire is computed with `ZonedDateTime` in the current zone, so 7:00 stays 7:00 after a zone change; a time skipped by DST rings at the first valid minute after.
|
|
- **Doze / OEM battery killers:** only `setAlarmClock` is reliable; show a one-time hint for battery optimization on aggressive OEMs.
|
|
- **Android 14+ full-screen intent:** may be off for sideloaded apps → detect and explain, fall back to a heads-up notification that still rings.
|
|
- **Race conditions:** snooze and dismiss from the notification and the activity at the same time → all transitions go through one `Mutex` in the ringing service.
|
|
- **Process death:** timers and stopwatch keep only timestamps, never counters.
|
|
- **Two alarms at the same time:** ring both labels in one screen; the second does not cancel the first.
|
|
- **Sounds:** a picked file can disappear → fall back to the default sound, never ring silently by accident.
|
|
- **No network:** nothing to rate-limit, nothing to leak; app is F-Droid friendly.
|
|
|
|
## Build-Reihenfolge
|
|
|
|
1. **Vertical slice** (S01, S03, S11): add an alarm, it rings at the time with sound over the lock screen, stop. Tables `alarms`, `alarm_instances`. Proves AlarmManager + full-screen intent.
|
|
2. **Must alarms** (S02, S04, S15): expanded card, days, label, vibrate, sound picker, snooze, delete/undo, snoozed notification, settings for snooze/silence-after, reboot/time-change handling.
|
|
3. **Timer + stopwatch** (S08, S09, S12): keypad, multiple timers, ring progress, background notifications, laps. Table `timers`.
|
|
4. **World clock** (S06, S07): local time, cities offline, reorder/delete, analog/digital. Table `cities`.
|
|
5. **Design pass**: expressive cards, bottom bar, big time typography, slide-to-act.
|
|
6. **Widgets** (S16): digital, analog, next alarm, style options like Wetter.
|
|
7. **Should**: pause alarm, dismiss early, upcoming/missed notifications, gradual volume, volume buttons, bedtime (S10), own sound files, settings (S13).
|
|
8. **Could**: sunrise alarm, sleep sounds, screensaver (S14), world clock widget, automatic home clock.
|