Reviewer lens: what Byron actually sees and feels on his phone. Inputs: docs/PRD-original.md and docs/DESIGN-DRAFT.md (2026-10-03).
Android behaviors that I have not verified on a device are marked [verify] and collected in §S. Do not take them as fact.
Kill criterion: see §7.
| # | Failure | Mode | Severity | Cause |
|---|---|---|---|---|
| F1 | Double notifications. Each message produces two shade entries (Messages plus sidecar), and both persist after one is read. | Both | High (daily annoyance) | No lifecycle link between the original and the companion notification. |
| F2 | Silent messages when the sidecar is dead. | Re-ring | Critical | The Messages channel is set to silent, and nothing re-alerts. Samsung kills the process, an update unbinds the listener, or Samsung's "Auto optimize daily" restart drops it. |
| F3 | The buzz arrives before the verdict. | Annotate | Medium | Messages alerts at post time. The sidecar can only annotate afterwards, so Annotate never reduces interruption. It only adds information. Be explicit that Annotate does not meet the PRD goal "low-confidence unknowns less interruptive". |
| F4 | Latency on re-ring. A known contact rings late, or rings twice. | Re-ring | Medium | The LLM or a slow DB read sits in the alert path. If the original channel was also audible (a priority conversation), the phone rings twice. |
| F5 | Alert throttling. Bursts of re-rings get quieter or are dropped. | Re-ring | Low to medium | Android rate-limits notification posts per app. Android 15 "notification cooldown" lowers volume for rapid successive alerts from one app [verify on Samsung One UI; it may be Pixel-only]. Because every message re-rings through one app (the sidecar), cooldown hits the sidecar harder than it hit Messages. |
| F6 | Thread updates re-classified. | Both | Medium | Google Messages updates the same notification key as the thread grows (the MessagingStyle tail). Without dedupe on per-message timestamps, the same message gets re-annotated and re-rung. |
| F7 | Snoozed scam notification lost. | Re-ring | Medium | The snooze is unbounded, or the Messages re-post behaviour while snoozed is unknown [verify]. |
| F8 | Sidecar notification leaks text on the lock screen. | Both | Medium | The companion duplicates message spans. Lock-screen redaction of the original doesn't carry over to ours. |
NotificationListenerService.Ranking.getChannel() and getImportance() (API 26+) tell us the original's channel and importance.Ranking.getLastAudiblyAlertedMillis() (API 29+) tells us whether the original actually made a sound. Use this to dedupe F4: if the original alerted within the last ~2 s, don't re-ring.Ranking.isConversation() and getConversationShortcutInfo() (API 30+) tell us whether it's a conversation and which shortcut it belongs to.snoozeNotification(key, durationMs) and cancelNotification(key) act on the original.NotificationListenerService.updateNotificationChannel() and getNotificationChannels() can modify another app's channels, but only for listeners with a CompanionDeviceManager association. Getting one for a sideloaded phone-only app is a hack, and Android can close the loophole at any point.Mode A: Annotate (default, zero risk to reachability)
setSortKey placing it next to Messages where the launcher honours that.sbn.key plus the message timestamp.onNotificationRemoved(original): cancel the companion, for any reason. The user read it, replied, swiped it, or it was read on another device.(conversation key, message timestamp, sender) from android.messages. Classify only the new tail entries. Update the existing companion in place with setOnlyAlertOnce(true) rather than posting a new one.Mode B: Re-ring (opt-in, labelled "Quiet unknowns (re-ring)")
Setup is guided, with screenshots, and the sidecar checks every step itself:
| Sidecar channel | Default importance | Used for |
|---|---|---|
ring_known | HIGH, sound | Recognized and Likely legitimate. Skipped if getLastAudiblyAlertedMillis shows the original already rang (Priority conversation). |
quiet_unknown | LOW, silent | Unrecognized. Optionally batched as an hourly "3 unknown messages since 2 pm" summary. |
caution | DEFAULT, no sound, visible | Suspicious. |
scam_pattern | MIN, silent | Likely scam. The original is snoozed for a finite 8 h, never cancelled. |
cannot_assess | HIGH, sound | Cannot assess. When in doubt, ring. A redacted OTP is often something he's waiting for. |
health | HIGH, sound, bypass DND if granted | Watchdog alarm (below). |
onNotificationPosted. The LLM, if enabled, can update the card afterwards but never gates the ring.setTimeoutAfter(~10 s). It rings, shows the verdict chip, then disappears and leaves the original Messages notification (with its inline reply) as the one shade entry. The verdict stays one tap away in the sidecar app.Failsafes for F2 (sidecar dead means silent messages):
| Failsafe | Mechanism | Covers |
|---|---|---|
| Priority conversations | Android rings top contacts natively, independent of the sidecar. | The most important people are reachable even if everything else fails. |
| Listener rebind | onListenerDisconnected → requestRebind(component); BOOT_COMPLETED / MY_PACKAGE_REPLACED receivers → rebind. | Updates, reboots, the Samsung daily restart. |
| Watchdog | WorkManager periodic job (15 min minimum) plus an exact alarm. It checks that the listener is connected and that the last heartbeat is recent. If disconnected for more than 2 min: a loud health notification, "Message check is off. Messages are SILENT. Tap to fix / switch to normal alerts." | Process killed while the app can still schedule work. |
| Missed-message sweep | On reconnect, getActiveNotifications() and re-classify anything from Messages posted while we were down. Any found → ring once with "N messages arrived while message check was off". | Catch-up after a gap. |
| Deadman by design | Each snooze is finite (8 h), so the system restores the notification even if we never come back. | Scam snoozes. |
| One-tap revert | The setup screen and the health alert both deep-link to the Messages channel settings to un-silence it. The sidecar can't do it for him. | Recovery. |
| Honest copy | The Re-ring toggle says: "If this app stops, only your Priority conversations will make a sound." | Informed consent. |
The residual risk the watchdog can't cover is a force-stopped app: Samsung "deep sleeping", or a user force-stop, which kills alarms too. That's why the Samsung "Never sleeping apps" step in §4 is mandatory for Re-ring and checked at runtime. If the app is not on that list, Re-ring refuses to turn on.
Lock screen (fixes F8): every sidecar notification uses VISIBILITY_PRIVATE with a text-free publicVersion, for example "Message check: Unknown sender". Quoted spans never reach the lock screen.
legitimacy_confidence is derived from the others, and showing it next to scam_risk invites the "these should sum to 100" misreading. It appears only in the Scores expander.| Verdict (internal) | Chip text | Colour | One-line why (template) | Never say |
|---|---|---|---|---|
| Recognized | KNOWN | blue-grey | "In your contacts as {name}." / "You trusted this number on {date}." | |
| Likely legitimate | LIKELY OK | green | "Says it's {org}; matches your note '{ledger entry}'." / "Short code {code} is {brand}'s known number." | "Safe", "verified" |
| Unrecognized / insufficient context | UNKNOWN | neutral grey | "Not in contacts. {No name given / No link / No request}. Not enough to judge either way." | "Spam", "suspicious", "unverified sender" (reads as an accusation) |
| Suspicious | CAUTION | amber | "Asks you to {pay / move to WhatsApp / share a code}: a common scam step. Could still be real." | "Spam", "fraud" |
| Likely scam / phishing | SCAM PATTERN | red | "Link goes to {domain}, not {brand}. Matches the fake-{toll/delivery} pattern." | "Spam", "definitely", "fraudster" |
| Cannot assess | CAN'T SEE | grey, dashed outline | "Preview was {hidden / redacted by Android / cut off}. Open in Messages to read it." | Any risk language |
The golden case, "Are you working today?", renders as:
UNKNOWN · "Not in contacts. No name, no link, no request. Real people and wrong-number scams both open like this, so I'll watch the next message."
| From | Lines shown (max 3) |
|---|---|
| UNKNOWN | "+ They say who they are and it matches something you're expecting → LIKELY OK" · "+ You mark them trusted → KNOWN" · "- Next message has a link, asks for money or a code, or moves to another app → CAUTION" |
| LIKELY OK | "- A link to a domain that isn't {brand} → SCAM PATTERN (your expected-sender note can't override this)" |
| CAUTION | "+ Nothing in their texts can clear this. Check through {brand}'s official app or number." · "- A payment or code request → SCAM PATTERN" |
| SCAM PATTERN | "Nothing they send can change this. If you think it's real, contact {brand} directly, not via this message." |
| CAN'T SEE | "+ Turn on full previews (Settings > ...) → I can read it next time." |
| KNOWN + risk codes | "Possible compromised account: confirm with {name} by calling, not by replying." |
For Byron, who is technical, keep the numbers one tap away:
Scores (log-odds view: on)
identity 12 ##........ UNKNOWN_SENDER (-0.1)
context 0 .......... -
scam_risk 18 ##........ GENERIC_OPENER (+0.4), prior 0.08
legitimacy 10 #......... derived: identity + context - risk
completeness 100 ########## full text, sender id present
rule fired: #6 "otherwise -> Unrecognized"
engine: deterministic v0.3 · LLM: off · network: none
Rules every draft follows:
ACTION_SENDTO smsto: with sms_body. Google Messages opens an editable compose screen and Byron taps send himself. Per the draft, RemoteInput is never fired.| Situation | Ask-who offered? | Why |
|---|---|---|
| UNKNOWN, 1:1, long-code number | Primary action | This is the case it exists for. |
| LIKELY OK but the identity isn't confirmed | Secondary | Cheap confirmation. |
| CAUTION without a tripwire (for example a generic opener plus a relationship claim) | Secondary, with the note "Replying tells the sender this number is active." | Real risk, low cost. Let him choose. |
| CAUTION with a tripwire, or SCAM PATTERN | Hidden. Show "Block & report in Messages" instead. | Replying confirms a live number and invites the follow-up script. |
| Short code or alphanumeric sender ID | Hidden | These can't receive a conversational reply. A reply may be read as a keyword. |
| Group thread | Hidden | The question would go to everyone. |
| CAN'T SEE | Hidden | He hasn't seen the content. It might be an OTP he's waiting for. |
| KNOWN | Hidden | Not needed. |
| Email-gateway or iMessage-relay style senders | Hidden | Replies go somewhere unexpected. |
Always offer "Wait for them" next to it. It's a sidecar-local reminder: "Remind me in 2 h if they don't say more". Legitimate senders usually follow up with context (a name or a reason) within minutes. The reminder needs no reply, so it confirms nothing.
Opening the thread in Google Messages sends an RCS read receipt (if read receipts are on). That tells a scammer the number is live and someone read the message.
The sidecar card shows the full preview the listener already received, without opening Messages, and the sidecar never fires the "Mark as read" action. Say this on the card for UNKNOWN, CAUTION and SCAM PATTERN: "Read here: the sender won't get a read receipt." [verify: read receipts and typing indicators fire only on open in Messages, not on notification delivery]
Target: under 4 minutes, with every step self-verified by the app. No "did you do it?" checkboxes.
| Step | What Byron does | What the app verifies | Notes |
|---|---|---|---|
| 0. Install | Prefer adb install from the Mac over a downloaded APK. | n/a | ADB installs may be exempt from restricted settings and from region-gated Play Protect sideload blocking [verify both]. Google's developer-verification rollout for sideloaded apps (2026 onward, region-phased) may eventually require a registered developer identity even for personal APKs [verify current status for the US; ADB/dev installs were described as exempt]. Play Protect may still scan and warn. |
| 1. Restricted settings (Android 13+, only for browser- or file-manager-installed APKs) | Try to enable Notification Access → "Restricted setting" dialog → App info → ⋮ → Allow restricted settings → authenticate → come back. | Whether NotificationManagerCompat.getEnabledListenerPackages() contains us. | Show the exact screens. The ⋮ menu option only appears after the first blocked attempt, which is the confusing part. |
| 2. Notification Access | Deep link Settings.ACTION_NOTIFICATION_LISTENER_DETAIL_SETTINGS with our component (API 30+). Toggle on. | The listener connects (onListenerConnected). | Copy: "Reads notifications from Google Messages only. Never sends, deletes or opens anything. This app has no internet permission." Link to the manifest proof in-app. |
| 3. Samsung battery | Settings > Battery > Background usage limits > Never sleeping apps > add. Also App info > Battery > Unrestricted. Then App info > "Pause app activity if unused" → off. | PowerManager.isIgnoringBatteryOptimizations(). The Samsung list membership can't be read via API, so ask once and then infer from heartbeat gaps. | Mandatory for Re-ring, recommended for Annotate. Mention Samsung "Auto optimize daily" restarts and that we rebind after them. |
| 4. Live test | "Text this phone from any other phone: Are you working today?" | The listener sees a Messages notification. The card shows a field-coverage report: sender id ✓, full text ✓, Person uri tel: ✓, actions ✓, redacted ✗. | This doubles as the §6.2 feasibility probe UI. The verdict must be UNKNOWN. That's the golden case, live. |
| 5. Mode choice | Annotate (default) or Re-ring (runs the §1.3 guided setup, including Priority conversations). | For Re-ring: S1 behaviour observed live. A Priority contact texts while the sidecar is force-stopped and the phone still rings. | Only offered after step 3 passes. |
| 6. Optional | Add 1 to 3 expected senders now (§4.1). Optionally grant READ_CALENDAR. | n/a | Skippable. Remind after 3 days. |
The ledger is the product's main differentiator (draft §B), and it dies if adding an entry feels like a form. Entry points, fastest first:
Delivery Appointment Service visit New job/colleague Travel Person.Parsing examples. The default expiry is 7 days for deliveries, the event date plus 1 day for appointments, and 30 days for people.
"fedex this week" -> Delivery · FedEx · expires Sun
"dentist thu bright smile" -> Appointment · Bright Smile Dental · Thu (+1d)
"carlos building ashford" -> Person · Carlos · The Ashford · 30d
Ledger entries render as chips on the main screen with a countdown. Swipe to delete. An expired entry fades for a day before it's removed.
I know them (→ trust sender, optional name, never touches Contacts) and Not wanted (→ local label plus a deep link to block in Messages).This is real action. It opens the card, asks "Which signal was wrong?" with the reason codes as tappable chips, and records a per-code correction.Something's off action.The main screen has three zones: "Needs a look", "Expecting", and "Recent". The sender page shows the trajectory as a vertical timeline, so the multi-message pivot (draft §A, the stateful differentiator) is visible:
+555 0142 (unnamed) [ UNKNOWN -> CAUTION ]
---------------------------------------------------------------------
Tue 09:12 UNKNOWN "Are you working today?"
GENERIC_OPENER
Tue 09:40 UNKNOWN "Sorry wrong number! I'm Amy, nice to meet you"
WRONG_NUMBER_PIVOT (+ trajectory: opener -> pivot)
Tue 10:05 CAUTION "...chat on [WhatsApp]?"
PLATFORM_SHIFT (+ trajectory: 3-step pattern)
---------------------------------------------------------------------
Pattern: opener -> wrong-number pivot -> platform shift
This sequence is typical of investment/romance scams. Any one
message alone was not enough.
---------------------------------------------------------------------
[ Block & report in Messages ] [ I know them ] [ Forget sender ]
What Google Messages already does better:
What the sidecar does that Google Messages doesn't:
Kill criterion (propose to the consolidator): run the Phase 0 probe in shadow for 14 days. If it sees fewer than ~5 unknown-sender, non-short-code messages per week, or Google's own filtering already handles nearly all the scams he gets, the value is too thin. In that case ship only the probe plus the expected-senders card, or stop. Byron should set the actual threshold.
+---------------------------------------------------------------+
| Messages · now |
| (555) 010-0142 |
| Are you working today? |
| [Reply] [Mark as read] |
+---------------------------------------------------------------+
| Message check · now (silent) |
| [ UNKNOWN ] (555) 010-0142 |
| Not in contacts. No name, no link, no request. |
| Not enough to judge either way. |
| [Ask who this is] [I know them] [Wait 2h] |
+---------------------------------------------------------------+
lock screen public version: "Message check: Unknown sender"
+---------------------------------------------------------------+
| Message check · now (rings) |
| [ LIKELY OK ] 72789 · "FedEx: your package arrives today..." |
| Short code matches FedEx; you're expecting FedEx (till Sun). |
+---------------------------------------------------------------+
+---------------------------------------------------------------+
| Message check |
| [ SCAM PATTERN ] +1 (888) 555-0199 |
| Link goes to ezdrive-pay.top, not a government domain. |
| [See why] [Block & report in Messages] [This is real] |
+---------------------------------------------------------------+
+---------------------------------------------------------------+
| ____ |
| [ CAUTION ] +1 (555) 010-0177 · 10:05 |
| |
| Asks you to continue on WhatsApp: a common scam step. |
| Could still be real. |
| |
| Who Context Risk signals Visibility |
| Unknown None 2 found Full text |
| |
| "Hi it's Amy again :) can we chat on [WhatsApp]? My number |
| is [+44 7700 900123]" |
| ^ highlighted spans = evidence |
| |
| Read here: the sender won't get a read receipt. |
| |
| v Why (3) |
| ! PLATFORM_SHIFT "chat on WhatsApp" raises risk |
| ! WRONG_NUMBER_PIVOT earlier: "Sorry wrong number!" risk |
| . UNKNOWN_SENDER not in contacts (weak, capped) |
| |
| v What would change this |
| - A payment or code request -> SCAM PATTERN |
| + Nothing in their texts can clear this; verify another way |
| |
| > Scores (numbers, rule fired, engine version) |
| |
| [ Open in Messages ] [ Block & report in Messages ] |
| [ This is real ] [ Sender history ] |
+---------------------------------------------------------------+
+---------------------------------------------------------------+
| Message check ( * ) on · last seen Messages 2m ago |
+---------------------------------------------------------------+
| EXPECTING [+ Expecting]|
| ( Delivery · FedEx · 3d ) ( Appt · Bright Smile · Thu ) |
| ( Person · Carlos/Ashford · 27d ) |
+---------------------------------------------------------------+
| NEEDS A LOOK (3) |
| [ UNKNOWN ] (555) 010-0142 "Are you working today?" 9:12 |
| [ CAUTION ] (555) 010-0177 "chat on WhatsApp?" 10:05 |
| [ CAN'T SEE ] 72000 preview hidden by Android 10:30|
+---------------------------------------------------------------+
| RECENT filter: all v |
| [ SCAM PATTERN ] (888) 555-0199 toll link (snoozed 8h) 8:01 |
| [ LIKELY OK ] 72789 FedEx matches Expecting 7:40|
| (KNOWN messages are not listed unless "show known" is on) |
+---------------------------------------------------------------+
| [ Review week (6) ] [ Settings / Privacy ] |
+---------------------------------------------------------------+
| Draft § | Change |
|---|---|
| E | Annotate: no companion for Recognized; lifecycle-link companions to the original (onNotificationRemoved cancels them); update in place with setOnlyAlertOnce. |
| E | Rename "Quiet unknowns" to "Re-ring". Require Priority conversations for top contacts (gated on S1), plus the watchdog, the missed-message sweep and a verified Never-sleeping-apps entry. Per-verdict sidecar channels. Cannot assess rings. |
| E | Re-ring the companion via heads-up with setTimeoutAfter. Dedupe against Ranking.getLastAudiblyAlertedMillis() (API 29+). Deterministic-only alert path under 300 ms. |
| E | Snooze is finite (8 h) and never cancelNotification. |
| D | Add "Wait for them" (reminder) next to "Ask who this is". Add the §3.2 hide rules. Add the "Read here, no read receipt" affordance. Drafts must not answer embedded questions or say "wrong number". |
| D | Feedback: per-verdict targeted actions (§5) instead of a generic "this was wrong". Always show the effect of the correction. |
| B | Ledger entry via card ("I was expecting this"), share sheet, QS tile and keyboard voice. Default expiries per type. |
| A | Verdict copy table (§2.2). Unknown = grey chip. Counterfactuals generated from the decision table. legitimacy_confidence hidden outside the Scores expander. |
| G / new | All sidecar notifications VISIBILITY_PRIVATE with a text-free public version. |
| H | Probe APK step 4 doubles as the onboarding field-coverage report. Add the §S items to the probe's test script. |
| # | Claim to verify | Why it's load-bearing | How to test on the Samsung (and Pixel if available) |
|---|---|---|---|
| S1 | A conversation marked Priority in Android still alerts audibly when the parent Google Messages channel is Silent. | Re-ring's only failsafe for top contacts when the sidecar is dead. If this fails, Re-ring is not shippable. | Set the parent channel to Silent and one thread to Priority. Force-stop the sidecar. Text from that contact and confirm sound or vibration. Repeat after a reboot. |
| S2 | Ranking.getLastAudiblyAlertedMillis() is populated correctly for Messages notifications on One UI. | Prevents double ringing. | Probe logs the value for silent-channel and priority-thread messages. |
| S3 | Behaviour of a snoozed Messages notification when the same key is re-posted (the next message in the thread), and across a reboot. | The scam-snooze design, and avoiding lost messages. | Snooze via the probe, send a second message, observe. Reboot mid-snooze. |
| S4 | Google Messages on his build/region has (or lacks) a native unknown-sender filter. | It might make Re-ring partly redundant. | Inspect Messages settings: Spam protection and any filter options. |
| S5 | Google Scam Detection is available on his Samsung model and region. | §7 overlap and honesty. | Check Messages > Settings > Protection & Safety. |
| S6 | adb install is exempt from the restricted-settings gate. | Onboarding friction. | Install via adb and via a browser download. Compare the Notification Access flow. |
| S7 | Developer-verification and Play Protect sideload blocking status for notification-listener apps in the US as of the build date. | Whether the APK installs at all in future. | Check Google's current Android developer verification docs at build time. Attempt a non-ADB install. |
| S8 | An RCS read receipt fires only on opening the thread in Messages, not on notification delivery or listener access. | The "read without receipt" claim on the card. | Use a second RCS phone with receipts on. Read via the sidecar card only, then check the sender's view. |
| S9 | Android 15+ notification cooldown exists on One UI and affects a third-party app's rapid alerts. | Re-ring bursts get quieter. | Send 5 messages in 20 s with Re-ring on and listen. |
| S10 | Listener rebind succeeds after the Samsung "Auto optimize daily" restart and after an app update. | Silent-sidecar risk. | Trigger both, then check onListenerConnected timing in the probe log. |
| S11 | setTimeoutAfter heads-up behaviour on One UI: it rings, shows briefly, then disappears cleanly. | The single-shade-entry design for Re-ring. | Probe posts a test notification. |
| S12 | The per-conversation channel and Ranking.getConversationShortcutInfo() are exposed for Messages threads. | Mapping a notification to a conversation without private APIs. | Probe logs the channel id and shortcut id per thread. |