diff --git a/replica/messages/architecture.md b/replica/messages/architecture.md new file mode 100644 index 0000000..4449ec3 --- /dev/null +++ b/replica/messages/architecture.md @@ -0,0 +1,18 @@ +# Architecture: Messages (Android, ohne Google) + +Same conventions as the others; **no network permission** (SMS goes through the radio, not the internet). + +| Schicht | Wahl | warum | +| --- | --- | --- | +| UI | Compose, M3 Expressive, dynamic colour | same as the others | +| Rolle | `RoleManager.ROLE_SMS` | only the default SMS app may write the SMS provider and receives SMS_DELIVER | +| Empfangen | `SMS_DELIVER` receiver (BROADCAST_SMS), joins multipart PDUs, inserts into `Telephony.Sms.Inbox` | default app duty | +| MMS-Pflicht | `WAP_PUSH_DELIVER` receiver (accepted, stored later with MMS support) | required for the role | +| Senden | `SmsManager.divideMessage` + `sendMultipartTextMessage` with sent/delivered PendingIntents; row in outbox → sent/failed | status updates in the provider | +| Antwort ohne UI | `RESPOND_VIA_MESSAGE` service (SEND_RESPOND_VIA_MESSAGE) | phone app "decline with message"; required for the role | +| Daten | system provider via ContentResolver + ContentObserver | no copies | +| Kontakte | PhoneLookup, same helper as Telefon | | + +Data: no own schema (DataStore for settings). +Bites: multipart and encoding, dual SIM (`SubscriptionManager`), status callbacks after process death (receiver updates by message id), notification grouping per thread, short codes and alphanumeric senders (no reply possible). +Build order: role + receive + list → conversation + send + status → notifications with reply → new chat, search, archive, delete → MMS. diff --git a/replica/messages/design/Tokens.kt b/replica/messages/design/Tokens.kt new file mode 100644 index 0000000..f957654 --- /dev/null +++ b/replica/messages/design/Tokens.kt @@ -0,0 +1,62 @@ +// Generated from tokens.json by gen_theme.py. Do not edit by hand. +package app.messages.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(0xFFF8F9FF) + val row = Color(0xFFFFFFFF) + val text = Color(0xFF191C20) + val textMuted = Color(0xFF44474E) + val bubbleIn = Color(0xFFE8EAF1) + val onBubbleIn = Color(0xFF191C20) + val bubbleOut = Color(0xFF2B5EA7) + val onBubbleOut = Color(0xFFFFFFFF) + val unread = Color(0xFF2B5EA7) + val error = Color(0xFFBA1A1A) + val fab = Color(0xFFD6E3FF) + val onFab = Color(0xFF001B3E) + val composer = Color(0xFFECEEF4) + val onComposer = Color(0xFF191C20) + val darkBg = Color(0xFF111318) + val darkRow = Color(0xFF1D2024) + val darkText = Color(0xFFE2E2E9) + val darkTextMuted = Color(0xFFC4C6D0) + val darkBubbleIn = Color(0xFF2B2F36) + val darkOnBubbleIn = Color(0xFFE2E2E9) + val darkBubbleOut = Color(0xFFA8C7FA) + val darkOnBubbleOut = Color(0xFF062E6F) + val darkUnread = Color(0xFFA8C7FA) + val darkError = Color(0xFFFFB4AB) + val darkFab = Color(0xFF284777) + val darkOnFab = Color(0xFFD6E3FF) + val darkComposer = Color(0xFF2B2F36) + val darkOnComposer = 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 = 22.sp, fontWeight = FontWeight.W400) + val title = TextStyle(fontSize = 22.sp, lineHeight = 28.sp, fontWeight = FontWeight.W400) +} + +object WSpace { + val s0 = 0.dp + val s2 = 2.dp + val s4 = 4.dp + val s8 = 8.dp + val s12 = 12.dp + val s16 = 16.dp + val s24 = 24.dp +} + +object WRadius { + val bubble = 20.dp + val bubbleTight = 6.dp + val pill = 999.dp +} diff --git a/replica/messages/design/gen_theme.py b/replica/messages/design/gen_theme.py new file mode 100644 index 0000000..9ac7398 --- /dev/null +++ b/replica/messages/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.messages.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/messages/design/tokens.json b/replica/messages/design/tokens.json new file mode 100644 index 0000000..daf3319 --- /dev/null +++ b/replica/messages/design/tokens.json @@ -0,0 +1,16 @@ +{ + "_about": "Messages fallback palette; dynamic colour on Android 12+.", + "color": { + "bg": "#f8f9ff", "row": "#ffffff", "text": "#191c20", "text-muted": "#44474e", + "bubble-in": "#e8eaf1", "on-bubble-in": "#191c20", "bubble-out": "#2b5ea7", "on-bubble-out": "#ffffff", + "unread": "#2b5ea7", "error": "#ba1a1a", "fab": "#d6e3ff", "on-fab": "#001b3e", "composer": "#eceef4", "on-composer": "#191c20", + "dark-bg": "#111318", "dark-row": "#1d2024", "dark-text": "#e2e2e9", "dark-text-muted": "#c4c6d0", + "dark-bubble-in": "#2b2f36", "dark-on-bubble-in": "#e2e2e9", "dark-bubble-out": "#a8c7fa", "dark-on-bubble-out": "#062e6f", + "dark-unread": "#a8c7fa", "dark-error": "#ffb4ab", "dark-fab": "#284777", "dark-on-fab": "#d6e3ff", "dark-composer": "#2b2f36", "dark-on-composer": "#e2e2e9" + }, + "pairs": [["text","bg"],["text","row"],["text-muted","row"],["on-bubble-in","bubble-in"],["on-bubble-out","bubble-out"],["unread","row"],["error","bg"],["on-fab","fab"],["on-composer","composer"], + ["dark-text","dark-bg"],["dark-text","dark-row"],["dark-text-muted","dark-row"],["dark-on-bubble-in","dark-bubble-in"],["dark-on-bubble-out","dark-bubble-out"],["dark-unread","dark-row"],["dark-error","dark-bg"],["dark-on-fab","dark-fab"],["dark-on-composer","dark-composer"]], + "type": {"label": {"size": 12, "line": 16, "weight": 500}, "body-sm": {"size": 14, "line": 20, "weight": 400}, "body": {"size": 16, "line": 22, "weight": 400}, "title": {"size": 22, "line": 28, "weight": 400}}, + "space": [0, 2, 4, 8, 12, 16, 24], + "radius": {"bubble": 20, "bubble-tight": 6, "pill": 999} +} diff --git a/replica/messages/features.csv b/replica/messages/features.csv new file mode 100644 index 0000000..b99eaf5 --- /dev/null +++ b/replica/messages/features.csv @@ -0,0 +1,25 @@ +feature,area,priority,original,clone,notes +Become the default SMS app (role),core,must,yes,no, +Receive SMS and store it in the system provider,core,must,yes,no, +Send SMS incl. long multipart messages,core,must,yes,no, +Sent and delivered status; failed with retry,core,must,yes,no, +Conversation list with unread state,list,must,yes,no, +Contact names and avatars,list,must,yes,no, +Conversation bubbles with day separators,chat,must,yes,no, +Composer with send button,chat,must,yes,no, +Notification with direct reply and mark as read,notifications,must,yes,no, +Start a new chat from contacts or a number,compose,must,yes,no, +Mark as read when opened,core,must,yes,no, +Delete conversations,list,should,yes,no, +Archive conversations,list,should,yes,no, +Search messages,search,should,yes,no, +Copy message text and links clickable,chat,should,yes,no, +Call from the conversation,chat,should,yes,no, +Quick reply from the phone app (respond via message),core,must,yes,no,needed for the role +Dual SIM choice,core,could,yes,no, +Dynamic colour and dark mode,design,must,yes,no, +Delivery reports setting,settings,could,yes,no, +MMS pictures and group MMS,mms,should,yes,no,later +RCS chat features,rcs,could,yes,skip,Google/carrier backend +Spam protection and Gemini features,other,could,yes,skip,Google services +Messages for web,other,could,yes,skip,Google service diff --git a/replica/messages/recon.md b/replica/messages/recon.md new file mode 100644 index 0000000..6df2dc6 --- /dev/null +++ b/replica/messages/recon.md @@ -0,0 +1,64 @@ +# Recon map: Google Messages (Android) + +Scope: conversation list, conversation with bubbles, compose/new chat, SMS send/receive/delivery, notifications with reply, search, archive/delete, contacts integration +For: personal use, an SMS app without Google services (project NoGooleApps) +Date: 2026-10-17 + +## Sources + +| # | source | URL | notes | +| --- | --- | --- | --- | +| 1 | screenshots | replica/messages/screens/ref-01..05.png | supplied by user: launcher icon, RCS group chat with reactions, list (store art), list on a phone (older), chat with image. Reference only | +| 2 | platform docs | https://developer.android.com/reference/android/provider/Telephony | SMS/MMS provider, default SMS app duties | +| 3 | platform docs | https://developer.android.com/reference/android/app/role/RoleManager#ROLE_SMS | default SMS app role requirements | + +## Core loop + +See new texts at a glance and answer in two taps, from the list or straight from the notification. + +## Screens + +| ID | screen | how | purpose | components | states | +| --- | --- | --- | --- | --- | --- | +| S01 | Conversations | launch | all threads | top bar (title, search, menu), rows (avatar, name or number, snippet with "Du: ", time, unread bold + dot), FAB "Chat starten" | empty, no role, filled, unread | +| S02 | Conversation | tap a row | read and reply | top bar (back, avatar, name, call, menu), bubbles (incoming left surface, outgoing right primary), day separators, time + delivery status, composer pill (attach, field, send) | sending, sent, delivered, failed (tap to retry), long text, links | +| S03 | New chat | FAB | pick a recipient | "An:" field, contact suggestions, type a number | empty, matches | +| S04 | Search | search icon | find messages | field, results with highlighted match | | +| S05 | Archived | menu | archived threads | list | empty | +| S06 | Settings | menu | options | default app, notifications (system), delivery reports, theme | | +| S07 | Notification | system | read and reply without opening | MessagingStyle with sender, direct reply, mark as read | | + +## Flows + +``` +F01 Reply to a new text: notification → reply inline → sent (taps: 2) +F02 Read and answer in the app: S01 → S02 → type → send +F03 Start a chat: S01 FAB → S03 pick contact → S02 → send +F04 Delete / archive a thread: long press in S01 → action +F05 Become the default SMS app: first start → system dialog +edge: long messages (multipart), no signal (failed, retry), dual SIM, unknown sender, short codes, delivery report +``` + +## Components + +Avatar (photo, initial, unknown) · ConversationRow · Bubble (in, out; first/middle/last grouping radius) · DaySeparator · Composer pill · Status line (Wird gesendet, Gesendet, Zugestellt, Fehler) · FAB extended "Chat starten" · Selection top bar. + +## Data model + +Messages and threads live in the system provider (`Telephony.Sms`, `Telephony.Threads`); as default SMS app we write incoming messages there ourselves. + +``` +Thread (system) thread_id, recipients, snippet, date, read, archived (Telephony.Threads.ARCHIVED) confidence: high +Sms (system) _id, thread_id, address, body, date, type (inbox|sent|outbox|failed|queued), read, status confidence: high +Prefs own: delivery_reports, theme confidence: high +``` + +## Out of scope + +- RCS (typing indicators, reactions, high-res media, read receipts over data): Google's RCS backend (Jibe) and carrier deals; not open to other apps. +- Google account backup, spam protection, Magic Compose/Gemini, "Messages for web": Google services. +- MMS (pictures, group MMS): technically possible for a default SMS app but needs carrier MMS settings and PDU handling; planned after SMS works. + +## Size + +Screens 7, flows 5. Hard parts: (1) default-SMS duties (store incoming, mark read, respond-via-message service, sent/delivered intents, multipart), (2) fast thread list with contact names, (3) MMS later. Size: M. diff --git a/replica/messages/screens/ref-01.png b/replica/messages/screens/ref-01.png new file mode 100644 index 0000000..5263000 Binary files /dev/null and b/replica/messages/screens/ref-01.png differ diff --git a/replica/messages/screens/ref-02.png b/replica/messages/screens/ref-02.png new file mode 100644 index 0000000..63e3507 Binary files /dev/null and b/replica/messages/screens/ref-02.png differ diff --git a/replica/messages/screens/ref-03.png b/replica/messages/screens/ref-03.png new file mode 100644 index 0000000..299591b Binary files /dev/null and b/replica/messages/screens/ref-03.png differ diff --git a/replica/messages/screens/ref-04.png b/replica/messages/screens/ref-04.png new file mode 100644 index 0000000..f416c32 Binary files /dev/null and b/replica/messages/screens/ref-04.png differ diff --git a/replica/messages/screens/ref-05.png b/replica/messages/screens/ref-05.png new file mode 100644 index 0000000..d237607 Binary files /dev/null and b/replica/messages/screens/ref-05.png differ