Files
2026-10-17 14:58:56 +02:00

6.5 KiB

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.