From 0f709aaff88f7fbce39a5f09b75b9be6b01a8031 Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 17 Oct 2026 16:21:50 +0200 Subject: [PATCH] replica(telefon): architecture, tokens (20 pairs AA), components Co-Authored-By: Claude Opus 5.5 --- replica/telefon/architecture.md | 49 +++++++++++++++++++ replica/telefon/design/Tokens.kt | 71 ++++++++++++++++++++++++++++ replica/telefon/design/components.md | 8 ++++ replica/telefon/design/gen_theme.py | 26 ++++++++++ replica/telefon/design/tokens.json | 26 ++++++++++ 5 files changed, 180 insertions(+) create mode 100644 replica/telefon/architecture.md create mode 100644 replica/telefon/design/Tokens.kt create mode 100644 replica/telefon/design/components.md create mode 100644 replica/telefon/design/gen_theme.py create mode 100644 replica/telefon/design/tokens.json diff --git a/replica/telefon/architecture.md b/replica/telefon/architecture.md new file mode 100644 index 0000000..e31424e --- /dev/null +++ b/replica/telefon/architecture.md @@ -0,0 +1,49 @@ +# Architecture: Telefon (Android, ohne Google) + +Same conventions as the other apps. **No network permission.** The app owns almost no data: it reads the system call log and contacts. + +## Stack + +| Schicht | Wahl | warum | +| --- | --- | --- | +| UI | Kotlin + Compose, M3 Expressive, dynamic colour | same as the others | +| Telefonie | `RoleManager.ROLE_DIALER` + own `InCallService` (BIND_INCALL_SERVICE, IN_CALL_SERVICE_UI) | the only official way to show the call UI and control calls | +| Anrufen | `TelecomManager.placeCall` | uses the default SIM/phone account, works for emergency numbers through the system | +| Daten | `CallLog.Calls` and `ContactsContract` via ContentResolver, observed with ContentObserver → Flow | no copy of personal data in our database | +| Eigene Daten | DataStore (quick replies, theme) | – | +| Audio | `InCallService.setMuted / setAudioRoute`, `CallEndpoint` on API 34+ | speaker, earpiece, Bluetooth | +| Näherungssensor | `PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK` while the earpiece is active | screen off at the ear | + +## Permissions + +READ_CALL_LOG, WRITE_CALL_LOG (delete entries), READ_CONTACTS, CALL_PHONE, READ_PHONE_STATE, MANAGE_OWN_CALLS no, USE_FULL_SCREEN_INTENT, POST_NOTIFICATIONS, WAKE_LOCK, SEND_SMS no (quick replies hand off to the SMS app with `smsto:`). +The dialer role grants most of these automatically once the user accepts it. + +## Data + +No schema. Own state in DataStore only. Access: single user; the system enforces the permissions. + +## Interne API + +| Aufruf | macht | Flow | +| --- | --- | --- | +| `CallLogRepo.observe(filter)` | query CallLog, join contact name/photo by number (PhoneLookup), group by day | F01 | +| `ContactsRepo.favorites()` / `search(q)` / `t9(digits)` | starred contacts; name/number search; T9 digit match on names and numbers | F02, F03, F06 | +| `Dialer.call(number)` | `placeCall(tel:)` | F01–F03 | +| `CallManager` (in `InCallService`) | StateFlow of calls; answer, reject (with SMS), hold, unhold, mute, route, DTMF, disconnect | F04, F05, F07 | +| `Role.request()` | role request intent | F08 | + +## Was beißt + +- **Wrong default dialer state**: without the role, calls still work through the system dialer; the app shows a banner instead of failing. +- **Lock screen**: incoming UI via full-screen-intent notification; `showWhenLocked` + `turnScreenOn` on the activity. +- **Ringtone**: the system rings for the default dialer? No: the InCallService app must play the ringtone itself only when `Call.Details` say so; on modern Android the platform `Ringer` plays it for the default dialer. We do not play our own. +- **Process death mid-call**: the InCallService is bound by the system as long as calls exist; UI state is rebuilt from `calls`. +- **Emergency numbers**: always via `placeCall`, never blocked by our UI. +- **Privacy**: no network, no analytics; call log stays in the system. + +## Build-Reihenfolge + +1. Slice: role request, InCallService, incoming + in-call screens, place a call from the keypad. +2. Must: call log with grouping and call back, favourites, search, notifications, mute/speaker/DTMF, proximity. +3. Should: filters, T9, hold, call waiting, quick reply SMS, details screen, voicemail, Bluetooth. diff --git a/replica/telefon/design/Tokens.kt b/replica/telefon/design/Tokens.kt new file mode 100644 index 0000000..fd5eb71 --- /dev/null +++ b/replica/telefon/design/Tokens.kt @@ -0,0 +1,71 @@ +// Generated from tokens.json by gen_theme.py. Do not edit by hand. +package app.telefon.ui.theme + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +object WColor { + val bg = Color(0xFFF6F7FB) + val card = Color(0xFFFFFFFF) + val text = Color(0xFF191C20) + val textMuted = Color(0xFF44474E) + val missed = Color(0xFFBA1A1A) + val chipOn = Color(0xFFD3E3FD) + val onChipOn = Color(0xFF041E49) + val accept = Color(0xFF1E6B38) + val onAccept = Color(0xFFFFFFFF) + val hangup = Color(0xFFC5221F) + val onHangup = Color(0xFFFFFFFF) + val toggle = Color(0xFFFFFFFF) + val onToggle = Color(0xFF191C20) + val toggleOn = Color(0xFFD3E3FD) + val onToggleOn = Color(0xFF041E49) + val slideTrack = Color(0xFFE2E5EC) + val onSlideTrack = Color(0xFF191C20) + val darkBg = Color(0xFF111318) + val darkCard = Color(0xFF1D2024) + val darkText = Color(0xFFE2E2E9) + val darkTextMuted = Color(0xFFC4C6D0) + val darkMissed = Color(0xFFFFB4AB) + val darkChipOn = Color(0xFF2F4A72) + val darkOnChipOn = Color(0xFFD6E3FF) + val darkAccept = Color(0xFF6FDC8C) + val darkOnAccept = Color(0xFF00391A) + val darkHangup = Color(0xFFF2645A) + val darkOnHangup = Color(0xFF410002) + val darkToggle = Color(0xFF2B2F36) + val darkOnToggle = Color(0xFFE2E2E9) + val darkToggleOn = Color(0xFFA8C7FA) + val darkOnToggleOn = Color(0xFF062E6F) + val darkSlideTrack = Color(0xFF2B2F36) + val darkOnSlideTrack = Color(0xFFE2E2E9) +} + +object WType { + val label = TextStyle(fontSize = 12.sp, lineHeight = 16.sp, fontWeight = FontWeight.W500) + val bodySm = TextStyle(fontSize = 14.sp, lineHeight = 20.sp, fontWeight = FontWeight.W400) + val body = TextStyle(fontSize = 16.sp, lineHeight = 24.sp, fontWeight = FontWeight.W400) + val title = TextStyle(fontSize = 22.sp, lineHeight = 28.sp, fontWeight = FontWeight.W400) + val caller = TextStyle(fontSize = 40.sp, lineHeight = 48.sp, fontWeight = FontWeight.W400) + val dial = TextStyle(fontSize = 36.sp, lineHeight = 44.sp, fontWeight = FontWeight.W400) + val key = TextStyle(fontSize = 30.sp, lineHeight = 36.sp, fontWeight = FontWeight.W400) +} + +object WSpace { + val s0 = 0.dp + val s4 = 4.dp + val s8 = 8.dp + val s12 = 12.dp + val s16 = 16.dp + val s24 = 24.dp + val s32 = 32.dp +} + +object WRadius { + val cardOuter = 24.dp + val cardInner = 4.dp + val pill = 999.dp +} diff --git a/replica/telefon/design/components.md b/replica/telefon/design/components.md new file mode 100644 index 0000000..2bae50b --- /dev/null +++ b/replica/telefon/design/components.md @@ -0,0 +1,8 @@ +# Komponenten: Telefon + +Avatars: photo from contacts, else initial on a colour derived from the name (stable), unknown number = person icon. On call screens the avatar is a large scalloped cookie (12 waves). +CallLogGroup: rows inside one card per day, outer radius 24, inner 4, 2dp gaps (same kit as the settings tiles). Row: avatar 40, name (or formatted number), line 2 = direction icon + label/type + time, missed in error colour, trailing call icon button. +SlideToAnswer: track pill, labels "Ablehnen" left / "Annehmen" right, thumb with phone icon; past 50 % acts; custom accessibility actions. +InCallControls: 4 round toggles 64dp with label below (Wähltasten, Stumm, Lautsprecher, Mehr), red hang-up pill 160×64 below; "Mehr" sheet: Halten, Anruf hinzufügen. +Keypad: 3×4 keys (digit 30sp + letters 11sp), green round call FAB 72dp, backspace. +``` diff --git a/replica/telefon/design/gen_theme.py b/replica/telefon/design/gen_theme.py new file mode 100644 index 0000000..5d79c30 --- /dev/null +++ b/replica/telefon/design/gen_theme.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +"""tokens.json -> Tokens.kt (Compose). Re-run after every token change.""" +import json, re, sys, pathlib +src = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else "tokens.json") +out = pathlib.Path(sys.argv[2] if len(sys.argv) > 2 else src.with_name("Tokens.kt")) +t = json.loads(src.read_text()) +camel = lambda s: re.sub(r"-(\w)", lambda m: m.group(1).upper(), s) +L = ["// Generated from tokens.json by gen_theme.py. Do not edit by hand.", + "package app.telefon.ui.theme", "", + "import androidx.compose.ui.graphics.Color", + "import androidx.compose.ui.text.TextStyle", + "import androidx.compose.ui.text.font.FontWeight", + "import androidx.compose.ui.unit.dp", + "import androidx.compose.ui.unit.sp", "", + "object WColor {"] +L += [f" val {camel(k)} = Color(0xFF{v.lstrip('#').upper()})" for k, v in t["color"].items()] +L += ["}", "", "object WType {"] +for k, v in t["type"].items(): + L.append(f" val {camel(k)} = TextStyle(fontSize = {v['size']}.sp, lineHeight = {v['line']}.sp, fontWeight = FontWeight.W{v['weight']})") +L += ["}", "", "object WSpace {"] +L += [f" val s{v} = {v}.dp" for v in t["space"]] +L += ["}", "", "object WRadius {"] +L += [f" val {camel(k)} = {v}.dp" for k, v in t["radius"].items()] +L += ["}", ""] +out.write_text("\n".join(L)) +print("wrote", out) diff --git a/replica/telefon/design/tokens.json b/replica/telefon/design/tokens.json new file mode 100644 index 0000000..84ea5e7 --- /dev/null +++ b/replica/telefon/design/tokens.json @@ -0,0 +1,26 @@ +{ + "_about": "Telefon tokens from replica/telefon/screens/ref-01..03. Fallback palette; dynamic colour on Android 12+. Check with contrast.py.", + "color": { + "bg": "#f6f7fb", "card": "#ffffff", "text": "#191c20", "text-muted": "#44474e", "missed": "#ba1a1a", + "chip-on": "#d3e3fd", "on-chip-on": "#041e49", "accept": "#1e6b38", "on-accept": "#ffffff", + "hangup": "#c5221f", "on-hangup": "#ffffff", "toggle": "#ffffff", "on-toggle": "#191c20", "toggle-on": "#d3e3fd", "on-toggle-on": "#041e49", + "slide-track": "#e2e5ec", "on-slide-track": "#191c20", + "dark-bg": "#111318", "dark-card": "#1d2024", "dark-text": "#e2e2e9", "dark-text-muted": "#c4c6d0", "dark-missed": "#ffb4ab", + "dark-chip-on": "#2f4a72", "dark-on-chip-on": "#d6e3ff", "dark-accept": "#6fdc8c", "dark-on-accept": "#00391a", + "dark-hangup": "#f2645a", "dark-on-hangup": "#410002", "dark-toggle": "#2b2f36", "dark-on-toggle": "#e2e2e9", "dark-toggle-on": "#a8c7fa", "dark-on-toggle-on": "#062e6f", + "dark-slide-track": "#2b2f36", "dark-on-slide-track": "#e2e2e9" + }, + "pairs": [ + ["text","bg"],["text","card"],["text-muted","card"],["missed","card"],["on-chip-on","chip-on"],["on-accept","accept"],["on-hangup","hangup"], + ["on-toggle","toggle"],["on-toggle-on","toggle-on"],["on-slide-track","slide-track"], + ["dark-text","dark-bg"],["dark-text","dark-card"],["dark-text-muted","dark-card"],["dark-missed","dark-card"],["dark-on-chip-on","dark-chip-on"], + ["dark-on-accept","dark-accept"],["dark-on-hangup","dark-hangup"],["dark-on-toggle","dark-toggle"],["dark-on-toggle-on","dark-toggle-on"],["dark-on-slide-track","dark-slide-track"] + ], + "type": { + "label": {"size": 12, "line": 16, "weight": 500}, "body-sm": {"size": 14, "line": 20, "weight": 400}, "body": {"size": 16, "line": 24, "weight": 400}, + "title": {"size": 22, "line": 28, "weight": 400}, "caller": {"size": 40, "line": 48, "weight": 400}, "dial": {"size": 36, "line": 44, "weight": 400}, "key": {"size": 30, "line": 36, "weight": 400} + }, + "space": [0, 4, 8, 12, 16, 24, 32], + "radius": {"card-outer": 24, "card-inner": 4, "pill": 999}, + "layout": {"avatar-row": 56, "avatar-call": 200} +}