Reviewer lens: are the Android assumptions in PRD-original.md and DESIGN-DRAFT.md actually true?
Date: 2026-10-03. Method: primary sources only. That means AOSP main source pulled from android.googlesource.com, developer.android.com, Google help pages, and Google blogs. Where the only evidence was a secondary article or a search-engine summary, the item is marked so.
Legend:
| # | Assumption | Status | Evidence / notes | |
|---|---|---|---|---|
| 1.1 | Google Messages posts MessagingStyle notifications, category=msg, pkg com.google.android.apps.messaging | UNVERIFIED | No primary source documents Google Messages' notification payload. The package name is right. Everything about style, category, channel names, and tag/id scheme is a spike item. | |
| 1.2 | The extra keys exist as named | VERIFIED (framework) | Keys: android.title, android.text, android.messages (bundles with text, time, sender, sender_person), android.people.list, android.conversationTitle, android.isGroupConversation, android.messagingUser, android.messages.historic. Source: Notification.java EXTRA_* constants and MessagingStyle.Message KEY_SENDER="sender", KEY_SENDER_PERSON="sender_person". https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/core/java/android/app/Notification.java. Whether Google Messages fills each one is UNVERIFIED. | |
| 1.3 | android.people.list Person uri is tel: or a contacts lookup URI, which lets us infer "in contacts" without READ_CONTACTS | PARTLY CONTRADICTED | See the note after this table. | |
| 1.4 | The raw number is available for unknown senders and for saved contacts | UNVERIFIED, likely asymmetric | For an unknown sender, android.title is probably the formatted number. For a saved contact, title and sender are probably the display name, so the raw number may not be present at all unless Person.uri is tel:. Consequence: keying sender state on HMAC(number) breaks for contacts. See design change D3. | |
| 1.5 | android.messages includes the user's own recent replies (sender == null or == messagingUser) | VERIFIED (framework) / UNVERIFIED (Google Messages) | MessagingStyle supports it: a null sender means "the user". Whether Google Messages includes outbound messages in the tail is a spike item. | |
| 1.6 | contentIntent, shortcutId, Reply (RemoteInput) and Mark-as-read actions | UNVERIFIED | Google Messages is very likely conversation-shortcut based (Android 11+ conversations). Spike must log it. | |
| 1.7 | Verified-business (RBM) badge, RCS metadata, attachments not visible | VERIFIED by absence | The framework has no extras for any of these. Inline image MMS may surface as a uri/type field on a MessagingStyle message (framework supports setData). UNVERIFIED for Google Messages. | |
| 1.8 | Lock-screen "hide sensitive content" redacts what the listener receives (PRD §6.2 items 6–7) | CONTRADICTED (good news) | See the note after this table. | |
| 1.9 | NEW: Lockdown mode stops delivery | VERIFIED | NotificationManagerService skips listener posts while isInLockDownMode(userId) (lines ~11497, 13052, 13336, same file as row 1.8). Messages received during Lockdown are never seen. Treat them as completeness gaps. | |
| 1.10 | NEW: We can scope the listener to Google Messages only | CONTRADICTED (in part) | NotificationListenerFilter (Android 12+) is a type filter plus a disallow list. There is no OS-level "only package X" allow list (isVisibleToListener, NotificationManagerService). The system grants access to all notifications, and we filter by package in code. The consent screen and disclosure must say that honestly. Use META_DATA_DEFAULT_FILTER_TYPES = `conversations\ | alerting` to reduce the volume. |
Note on 1.3 (Person uri). The Person.setUri javadoc says: "should be specified by the String representation of a ContactsContract.Contacts#CONTENT_LOOKUP_URI … The system will also attempt to resolve mailto: and tel: schema URIs. The path part of these URIs must exist in the contacts database … or the reference will be discarded as invalid." Source: https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/core/java/android/app/Person.java
What this means:
tel: uri is NOT evidence of a contact. It is merely a number the app supplied.content://com.android.contacts/contacts/lookup/... uri implies a contact.Note on 1.8 (lock screen). The listener post path (notifyPostedLocked / isVisibleToListener in NotificationManagerService.java) applies no keyguard or lock-screen-visibility condition. Lock-screen redaction is SystemUI-only. It uses publicVersion and the allow-secure-notifications-on-lockscreen setting, which does not reach listeners. Source: https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/services/core/java/com/android/server/notification/NotificationManagerService.java. Exception: an app-level setting inside Google Messages that changes what it posts would apply. That part is UNVERIFIED.
VERIFIED, but the design's understanding of it is wrong in three ways.
Sources:
NotificationManagerService.redactStatusBarNotification(), isAppTrustedNotificationListenerService(), and NotificationRecord.java (mSensitiveContent is set only from Adjustment.KEY_SENSITIVE_CONTENT). https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/services/core/java/com/android/server/notification/NotificationRecord.javaWho is exempt (any one of these is enough):
RECEIVE_SENSITIVE_NOTIFICATIONS, which is signature|role in core AndroidManifest.xml.OP_RECEIVE_SENSITIVE_NOTIFICATIONS == allowed.A sideloaded sidecar is untrusted unless Byron creates a CDM association with a real device or sets the appop over adb. Both are possible hacks. Both are rejected: redaction protects OTPs, which the PRD wants.
What the listener actually receives (redactStatusBarNotification):
android.title becomes the app label ("Messages"). The sender name or number is gone.android.text becomes the localized string "Sensitive notification content hidden" (R.string.redacted_notification_message). Match on structure, not on the English text.cloneInto(…, heavy=false), which calls lightenPayload(). That removes every Parcelable, Parcelable[] and ArrayList extra, so these are all stripped:android.people.listandroid.messagesandroid.messages.historicandroid.messagingUserandroid.subText and android.textLines are removed.contentIntent.shortcutId.android.conversationTitle survives if Google Messages set it, typically for groups.getActiveNotifications() returns the redacted form too (line ~5681).Corrections to the design:
shortcutId or notification key seen on an earlier, unredacted post in the same conversation. Detect redaction structurally: title == app label, people list absent, single empty-sender message.KEY_SENSITIVE_CONTENT, the sidecar receives raw OTPs. The OTP/financial span-drop before storage (design §G) is therefore mandatory and load-bearing, not a nice-to-have. The spike must test with a real OTP on the Samsung.| # | Assumption | Status | Evidence |
|---|---|---|---|
| 3.1 | Android 13+: a sideloaded app needs App info → ⋮ → "Allow restricted settings" before Notification Access can be enabled | VERIFIED | https://support.google.com/android/answer/12623953 |
| 3.2 | Android 15 Enhanced Confirmation Mode (ECM) replaces this with an installer-based decision | VERIFIED | See the note after this table. |
| 3.3 | adb install is not guarded | UNVERIFIED | See the note after this table. |
| 3.4 | NEW: Restricted settings are additionally blocked during a call with a non-contact | VERIFIED (code) | ECM UNTRUSTED_CALL_RESTRICTED_SETTINGS. Irrelevant to Byron. Worth noting only because it shows the posture: Google treats "notification access" as a scam-assist vector. |
| 3.5 | NEW: Play Protect blocks internet-sideloaded apps that declare a notification listener | VERIFIED (select markets) | See the note after this table. |
| 3.6 | NEW: Android developer verification | VERIFIED, adb status UNVERIFIED | See the note after this table. |
Note on 3.2 (ECM). The ECM service isPackageEcmGuarded() (https://android.googlesource.com/platform/packages/modules/Permission/+/refs/heads/main/service/java/com/android/ecm/EnhancedConfirmationService.java) makes these settings protected:
OPSTR_ACCESS_NOTIFICATIONSBIND_ACCESSIBILITY_SERVICESYSTEM_ALERT_WINDOWGET_USAGE_STATSROLE_SMSA package is always guarded if its packageSource is PACKAGE_SOURCE_LOCAL_FILE or PACKAGE_SOURCE_DOWNLOADED_FILE, i.e. a browser or file-manager install. Allow-listed installers and preinstalled installers are trusted.
Note on 3.3 (adb install). An adb install has packageSource UNSPECIFIED. The outcome then depends on getInstallingPackageName(), which is null or com.android.shell, and on the trustPackagesInstalledViaNonAllowlistedInstallers flag. The code doesn't settle it. Spike action: install the probe with adb install from the Mac, then check whether the Notification Access toggle is greyed out.
Note on 3.5 (Play Protect). Play Protect "enhanced fraud protection" automatically blocks installation of apps from "web browsers, messaging apps, or file managers" that declare RECEIVE_SMS, READ_SMS, NOTIFICATION_LISTENER or ACCESSIBILITY. It is active "in select markets", and the US is not stated. https://developers.google.com/android/play-protect/warning-dev-guidance. Never distribute the APK by browser download or by messaging it to the phone. Use adb.
Note on 3.6 (developer verification). Certified devices begin requiring developer-verified apps on 2026-09-30 in BR, ID, SG and TH, and globally in 2027. There are limited-distribution (hobbyist, ≤20 devices) accounts and a power-user "advanced flow". https://developer.android.com/developer-verification. Whether adb installs are exempt is not stated on that page. Plan for a limited-distribution account before 2027 if Byron wants this to keep working on a certified phone.
contentIntent from our activity (design §D "Open conversation")VERIFIED feasible, with a required opt-in.
https://developer.android.com/guide/components/activities/background-starts
ActivityOptions.makeBasic().setPendingIntentBackgroundActivityStartMode(MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE) to PendingIntent.send(...). Without it, the launch "might be blocked".ALLOW_IF_VISIBLE is sufficient, so Google Messages need not opt in. (An earlier tool summary claimed both are needed. The verbatim text says "at least one".)IntentSender.sendIntent() requires sender opt-in. This is the same API we already use, so no change.PendingIntent held in our process dies with our process.getActiveNotifications() in onListenerConnected(). If it's gone, fall back to smsto:.cancelNotification javadoc, if we fire a FLAG_AUTO_CANCEL contentIntent we should also call cancelNotification(key) to mimic the shade. NotificationListenerService.java: https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/core/java/android/service/notification/NotificationListenerService.javaACTION_SENDTO smsto: (design §D)| # | Assumption | Status | Evidence |
|---|---|---|---|
| 5.1 | ACTION_SENDTO + smsto:<n> opens a compose screen without sending | VERIFIED (contract) | https://developer.android.com/guide/components/intents-common#Messaging |
| 5.2 | The body is passed in sms_body | CONTRADICTED (docs) | The official docs specify Intent.EXTRA_TEXT as the body extra. sms_body is a legacy de facto extra. Set both. Whether Google Messages honors either is UNVERIFIED; spike test. |
| 5.3 | It opens Google Messages | Not guaranteed | The intent resolves to whatever handles SENDTO smsto:, which is normally the default SMS app. On a Samsung where Samsung Messages is default, it opens Samsung Messages. Use setPackage("com.google.android.apps.messaging") only if it resolves, otherwise use the chooser. |
| 5.4 | It doesn't need a number we may not have | CONTRADICTED for saved contacts | See 1.4. For RCS-only or short-code senders, smsto: with a short code may produce an unsendable draft. Disable "Ask who this is" when no dialable number is known. |
| 5.5 | We never fire RemoteInput | Design choice; good | Note: the redacted actions still carry RemoteInput (see §2). The code must never touch actions[] except to log their count. |
| # | Assumption | Status | Evidence |
|---|---|---|---|
| 6.1 | A listener can cancel or snooze another app's notification | VERIFIED | cancelNotification(key), cancelNotifications(keys), snoozeNotification(key, durationMs). See the note after this table. |
| 6.2 | A listener cannot change another app's importance or alerting | VERIFIED | See the note after this table. |
| 6.3 | Alerting has already happened by onNotificationPosted | VERIFIED (by architecture) | The listener is notified in parallel with or after SystemUI. There is no pre-alert hook for listeners. Only the Notification Assistant gets onNotificationEnqueued before posting. |
| 6.4 | Rebind after process death | VERIFIED, partial | See the note after this table. |
| 6.5 | Samsung battery killers | VERIFIED (secondary but authoritative in practice) | https://dontkillmyapp.com/samsung. Covers Sleeping / Deep sleeping apps, auto-optimization, and settings re-applied after firmware updates. Onboarding must walk through: "Never sleeping apps", Battery = Unrestricted, and turning off "Put unused apps to sleep". |
| 6.6 | Doze | Low risk | Notification delivery to a bound listener is not deferred by Doze, because the post wakes the system. UNVERIFIED on Samsung; that is a spike item. |
Note on 6.1 (cancel and snooze).
Note on 6.2 (importance). updateNotificationChannel(pkg, user, channel) exists on the listener, but it requires "an associated device [CDM] or be the notification assistant" (NotificationListenerService.java javadoc). BIND_NOTIFICATION_ASSISTANT_SERVICE is signature.
adb shell cmd notification allow_assistant is theoretically possible. It is rejected: it would displace Android System Intelligence, and that assistant is the very component that marks OTPs sensitive (§2).Note on 6.4 (rebind).
requestRebind(ComponentName) is documented "after a previous call to requestUnbind". It is commonly used as a recovery poke, but it only works if access is still granted.onListenerDisconnected() must set a persisted "disconnected since" timestamp so the UI can show "N messages may have been missed".requestRebind.| # | Assumption | Status | Evidence |
|---|---|---|---|
| 7.1 | Messages Google Messages files as spam post no notification | UNVERIFIED (likely) | See the note after this table. |
| 7.2 | Google Messages already ships on-device conversational Scam Detection | VERIFIED | See the note after this table. |
| 7.3 | Google Messages also does link warnings for unknown senders and has an international non-contact filter | VERIFIED (regional pilots, 2024) | Same Oct 2024 blog post as 7.1. |
| 7.4 | Reporting spam sends the number and the last 10 messages to Google | VERIFIED | https://support.google.com/messages/answer/9061432. The UI must say this before the user taps Report in Google Messages. |
Note on 7.1 (spam-filed messages). The primary source only says "When Google Messages suspects a potential scam text, it will automatically move the message into your spam folder or warn you." (https://security.googleblog.com/2024/10/5-new-protections-on-google-messages.html). The "no notification for spam-folder messages" wording came from a search summary, not a primary page.
The implications are the same either way:
Note on 7.2 (Scam Detection). It is on by default as part of Spam Protection. It only applies to conversations with non-contacts. It runs in English in the US, UK and Canada, and covers SMS, MMS and RCS. https://blog.google/security/new-ai-powered-scam-detection-features/
The design claims "No single-message filter sees this" about trajectory. That is CONTRADICTED in spirit: Google already does conversation-level detection. The differentiator must be explainability + the personal context ledger + "unknown ≠ malicious" verdicts, not trajectory detection per se.
Spike action: check whether Google Messages' scam warning shows up as a notification change, e.g. an updated text or extra.
| # | Assumption | Status | Evidence |
|---|---|---|---|
| 8.1 | A personal-profile listener sees work-profile notifications | VERIFIED: yes, by default | See the note after this table. |
| 8.2 | Private Space (Android 15) | UNVERIFIED | Not investigated in source. Spike if Byron uses it. |
Note on 8.1 (work profile). The DevicePolicyManager.setPermittedCrossProfileNotificationListeners javadoc says "By default all packages are permitted … When zero or more packages have been added, notification listeners installed on the primary user that are not in the list … won't receive events for managed profile notifications." https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/core/java/android/app/admin/DevicePolicyManager.java
So the sidecar will receive work-profile notifications unless the employer's DPC restricts it. That is a data-minimization and possibly employer-policy problem.
sbn.user != Process.myUserHandle() by default.VERIFIED with limits.
REDACTED_BY_OS state.shortcutId or the notification key from a prior unredacted post.HMAC(normalized number) when a number is present. Otherwise use HMAC over a stable substitute, in this order:Person.uriPerson.keyshortcutIdRecord which one was used. identity_confidence must treat "title is a name" as weak evidence of being a contact, not proof. That is UNVERIFIED until the spike: Google Messages may show caller-ID or business names for non-contacts.
tel: uri means "number known", nothing more. Offer optional READ_CONTACTS (PhoneLookup) as an explicit opt-in upgrade, and keep the no-contacts mode working.PendingIntent.send() with MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE from a resumed activity.getActiveNotifications() after reconnect.CanceledException and fall back to smsto:.EXTRA_TEXT and sms_body.conversations|alerting.TelecomManager.createManageBlockedNumbersIntent() for the system blocked-numbers screen. Spike whether Samsung honors it.analysis_completeness at the session level.adb install only. Never by browser or messenger (Play Protect auto-block plus ECM).INTERNET. Whether ML Kit GenAI / Gemini Nano works without INTERNET in our manifest is UNVERIFIED. It goes through AICore, but model download or feature checks may need Play Services. Spike before promising it.The same list applies to anything without READ_SMS, which the PRD forbids anyway.
BlockedNumberContract: "Only the system, the default SMS application, and the default phone app … and carrier apps can read, and write to the blockednumber provider." https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/core/java/android/provider/BlockedNumberContract.javaCaveat that strengthens the sidecar choice: even a default SMS app gets no RCS. There is no public RCS API for third-party SMS apps, so becoming default would cost Byron RCS chats entirely. Notification listening is therefore not a stopgap. It is the only way to observe RCS content at all.
android.messages includes our own replies, shortcutId, actions count, channel id, tag/id, category.contentIntent from a foreground activity, on the latest Android version available, with ALLOW_IF_VISIBLE.ACTION_SENDTO with EXTRA_TEXT vs sms_body. Check that the body is prefilled, the user still taps send, and which app opens.