RFC: honor the phone's Do Not Disturb on the watch (ANCS silent flag)#1772
RFC: honor the phone's Do Not Disturb on the watch (ANCS silent flag)#1772vvzvlad wants to merge 1 commit into
Conversation
Read the ANCS EventFlagSilent bit into a per-item header flag and, when the opt-in notifRespectPhoneSilence preference is set, keep silent notifications out of the popup/vibration/backlight path while still storing and listing them (mirrors the existing Quiet Time hide path). The suppression is keyed on header.silent alone, not on the item being an ANCS notification, so a companion app can drive the same behavior on Android by setting the silent flag on the wire flags byte. The preference is exposed over blob-db settings sync (no watch UI) so the companion app owns the toggle; it defaults off, leaving the firmware inert until an app opts in. Storage-compatible: silent uses bit 6 of the header flags byte (always zero in stored items); bit 5 stays reserved for TimelineItemFlagPersistent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Vlad Zaytsev <git@vvzvlad.xyz>
|
Apple doesn't document what is EventFlagSilent if its the focus mode, or the silent switch hardware (note that I could find) and not sure that is correctly exposing and working, have you test this? INFocusStatus only return a boolean saying if a focus mode is enable or not, but does't specific what focus mode (sleep,work,silence etc) not much you can do with that. |
|
Yes, I came up with a workaround, and I’ve been using firmware for a while now that stops displaying notifications when this flag is set. As far as I can tell, the behavior is simple: if a notification triggers a sound or vibration on the phone, the flag isn’t sent; if the notification arrives silently on the phone, the flag is sent. This behavior is identical to that of my Garmin watch. |
|
Oh, interesting. What about:
INFocusStatus is used into the pebble core app to do what? why do we need mobile side updates? |
|
Tested on-device (my firmware suppresses on the flag → "watch stays silent = flag set; watch alerts = flag not set"): Flag SET: classic Do Not Disturb, and any custom Focus where the sending app isn't allowed → watch stays silent. Flag NOT set:
So On The mobile-app change is two things: (1) un-gate the existing "Respect Phone Do Not Disturb" toggle, which today is Android-only; (2) sync that toggle's state to the watch. The reason is that the notification paths differ per platform — on Android the app is in the path and can filter on the phone; on iOS notifications go straight to the watch over ANCS, so the filtering has to happen on the watch, and the watch has to be told whether it should honor Without that app change we'd have to put the "Respect Phone DND" toggle on the watch itself, but since it only does anything on iOS that's an inconsistent product decision: the phone toggle acts only on Android (and is only shown on Android), whereas a watch toggle would show on both iOS and Android but act only on iOS. So I reused the existing Android setting and just sync its state to the watch, and the watch uses that to decide whether to act on |
The problem
When an iPhone is in Do Not Disturb / Focus, the watch still buzzes and pops up every notification. To get silence you mute the phone, then separately mute the watch, then remember to undo both. I went Pebble → Garmin → back to Pebble, and inheriting the phone's silence is the one thing I miss — on Garmin the watch just mirrors the phone.
What the firmware does today
prv_handle_ns_notification()reads the ANCSEventFlagsand acts on exactly two bits —EventFlagMultiMediaandEventFlagPreExisting.EventFlagSilentis declared inancs_types.hand tested nowhere, so the phone's silent state has no effect on the watch. Inherited fromgoogle/pebble, which didn't use the bit either.What this PR does
Reads
EventFlagSilentinto a new per-item header flag (header.silent), and — when the opt-innotifRespectPhoneSilencepreference is set — keeps silent notifications out of the popup/vibration/backlight path while still storing and listing them. Mirrors the existing Quiet Time hide path.Deliberate design choices:
s_syncable_notif_prefs), so the companion app owns the toggle. Defaults off — the firmware is inert until an app opts in. I deliberately did not add a watch-side settings row; see Why no watch menu row below.header.silentalone, not on the item being an ANCS notification. So a companion app could drive the same behavior on Android by setting the silent flag on the wireflagsbyte (TimelineItemFlagSilent, bit 6), with no firmware change — see question 1.silentuses bit 6 of the header flags byte (always zero in stored items); bit 5 stays reserved for the existingTimelineItemFlagPersistent.The flag
Apple's ANCS spec names bit 0
EventFlagSilent(Table 3-3 of the appendix). The name is the spec's own statement of intent — the notification is silent — so honoring it by staying silent on the watch is the natural reading.Apple doesn't document when iOS sets it, so I characterized it empirically on-device. The bit maps to exactly one thing: the notification was intercepted by an active Do Not Disturb / Focus, honoring that Focus's per-app/per-person allow-list (an allow-listed sender does not get the bit, so it still alerts). It is not set by the hardware Ring/Silent switch, ringer volume, per-app "Mute for 1 hour", or Screen Time. Effectively the iOS counterpart of Android's
Ranking.matchesInterruptionFilter().Why no watch menu row
The notification path is asymmetric:
NotificationListenerService), seesRanking.matchesInterruptionFilter(), and already ships a "Respect Phone Do Not Disturb" toggle that drops locally. Enforcement is app-side.A watch-side toggle would work on iOS but do nothing on Android (where the app owns it), or be a second toggle to reconcile. Instead: firmware exposes a synced preference and no UI; the app owns the visible toggle and enforces per platform.
For the record: my own daily-driver watch currently runs exactly that watch-side toggle as a personal stopgap, and it works fine for me on iOS. I'm deliberately not proposing it as this PR — a settings row that silently does nothing on Android is a bad product decision. I'd rather land the mechanism and let the companion app own the switch than ship that crutch.
Questions
1. How should Android be wired? Your call; it lives entirely in the companion app — the firmware commit is identical either way, and the paired app PR ships neither variant on Android (it keeps the status quo):
header.silenton Android, so the gate is a no-op there).TimelineItemFlagSilentand forward instead of dropping → DND-suppressed notifications appear silently in the list, matching iOS. (Changes existing Android behaviour: they currently vanish.) The app PR declares the wire flag so this needs no protocol change, but does not implement the emit path — it's left as a maintainer choice.2. Is ANCS the right transport, given the exact trigger isn't spelled out by Apple? (I've now characterized it empirically — see The flag — but it's still Apple's undocumented behaviour, not a contract.)
INFocusStatusCenter(iOS 15+) lets a companion app read Focus state with user authorisation and relay it — fully documented semantics, at the cost of an entitlement and app work.Known behaviour
If the notification window is already open when a silent notification arrives, it is not inserted into the live list until the window reloads (still lands in storage / the notifications app). Identical to the existing Quiet Time hide early-return directly above the new gate — intentional, but flagged.
Tested
./pbl build(obelix) and the affected./pbl testsuites pass:test_ancs,test_ancs_notifications,test_notification_window(obelix + gabbro),test_timeline_item,test_notification_storage.Unit tests added cover: the ANCS layer propagating the flag, the flag reaching the stored item, and the suppression path itself (with
header.silent = true/header.ancs_notif = false, proving the gate is platform-neutral, plus the two negative cases).On-device (obelix, via my personal watch-side-toggle build — same suppression path): with the preference on, notifications caught by an active Focus/DND land in the list silently, while everything else alerts normally — including senders allow-listed in the Focus. The hardware silent switch, ringer volume at 0, per-app "Mute for 1 hour", and Screen Time all leave the notification alerting (they do not set the flag); details under The flag.
Companion-app side — see the paired PR
coredevices/mobileapp#304 un-gates the existing "Respect Phone Do Not Disturb" toggle so it shows on both platforms and mirrors it to the synced
notifRespectPhoneSilencepref — a hiddenBoolWatchPref, so there's no duplicate row — which enables this feature on iOS; and it declaresTimelineItem.Flag.SILENTas the wire counterpart of this bit. It intentionally does not change Android notification handling — the Android forward-but-quiet path (question 1) is left as a maintainer decision.