Notification small icon is a framework dialog asset #43

Closed
opened 2026-08-18 22:47:10 -05:00 by null · 1 comment
Owner

Found while checking whether an artwork change could affect notifications. It cannot — but this was sitting there.

What is true now

core/notifications/src/main/kotlin/dev/privacyllc/period/core/notifications/PeriodNotifier.kt:102 and :109 both call .setSmallIcon(android.R.drawable.ic_dialog_info) — once for the public lock-screen version and once for the private one.

Android masks a notification's small icon to a white silhouette from its alpha channel. ic_dialog_info is a full-colour framework asset, so what reaches the status bar and the shade is its silhouette, not the icon anyone chose.

Everything else about notifications is clean and deliberate: no setLargeIcon anywhere, no R.drawable reference in the module at all, action icons are literally 0, and no manifest meta-data names a notification icon. An artwork change genuinely cannot break them.

What it costs

The status bar icon is the app's identity in the one place the product is most careful about — the lock screen. Every reminder this app has ever sent has shown a generic framework glyph.

What to do

Use a single-colour vector. app/src/main/res/drawable/ic_launcher_monochrome.xml is already exactly that: a ring with one offset dot, drawn as the themed-icon layer for precisely this reason — it reads at small sizes in one colour. It will need to move or be duplicated into core/notifications, since that module cannot see :app resources.

Traps

  • The icon must be alpha-only. Anything with colour becomes a blob; that is the defect being fixed, and it is easy to reintroduce by pointing at the full-colour emblem.
  • Both call sites, not one. The public and private builders are separate and the lock-screen one is the one that matters most.
  • Notification privacy is not affected by the icon, but re-check the instrumented NotificationPrivacyTest still passes — it asserts what a lock screen would render.
  • Channels are immutable after creation, but the icon is per-notification rather than per-channel, so no channel migration is needed.

Verify: a reminder fired on PeriodQA and on PeriodMinSdk26 shows the app's own mark in the status bar and in the shade, in both themes, and NotificationPrivacyTest still passes.

Found while checking whether an artwork change could affect notifications. It cannot — but this was sitting there. ## What is true now `core/notifications/src/main/kotlin/dev/privacyllc/period/core/notifications/PeriodNotifier.kt:102` and `:109` both call `.setSmallIcon(android.R.drawable.ic_dialog_info)` — once for the public lock-screen version and once for the private one. Android masks a notification's small icon to a **white silhouette from its alpha channel**. `ic_dialog_info` is a full-colour framework asset, so what reaches the status bar and the shade is its silhouette, not the icon anyone chose. Everything else about notifications is clean and deliberate: no `setLargeIcon` anywhere, no `R.drawable` reference in the module at all, action icons are literally `0`, and no manifest `meta-data` names a notification icon. An artwork change genuinely cannot break them. ## What it costs The status bar icon is the app's identity in the one place the product is most careful about — the lock screen. Every reminder this app has ever sent has shown a generic framework glyph. ## What to do Use a single-colour vector. `app/src/main/res/drawable/ic_launcher_monochrome.xml` is already exactly that: a ring with one offset dot, drawn as the themed-icon layer for precisely this reason — it reads at small sizes in one colour. It will need to move or be duplicated into `core/notifications`, since that module cannot see `:app` resources. ## Traps - **The icon must be alpha-only.** Anything with colour becomes a blob; that is the defect being fixed, and it is easy to reintroduce by pointing at the full-colour emblem. - Both call sites, not one. The public and private builders are separate and the lock-screen one is the one that matters most. - Notification privacy is not affected by the icon, but re-check the instrumented `NotificationPrivacyTest` still passes — it asserts what a lock screen would render. - Channels are immutable after creation, but the icon is per-notification rather than per-channel, so no channel migration is needed. Verify: a reminder fired on `PeriodQA` and on `PeriodMinSdk26` shows the app's own mark in the status bar and in the shade, in both themes, and `NotificationPrivacyTest` still passes.
null added this to the Batch 08 — Polish milestone 2026-08-18 22:47:10 -05:00
null added the
P2
label 2026-08-18 22:47:10 -05:00
null closed this issue 2026-08-19 21:51:58 -05:00
Author
Owner

Fixed in caf1244.

core/notifications/src/main/res/drawable/ic_notification.xmlredrawn, not copied. ic_launcher_monochrome.xml is a 108dp launcher canvas whose shape sits in the upper safe zone because a launcher crops and masks it; copying that geometry would have put a small mark floating above centre in the status bar. This is a 24dp canvas the ring nearly fills, opaque white throughout, since the system discards colour and supplies its own.

Both call sites, PeriodNotifier.kt:102 and :109.

Two tests, because "which resource id" is not the whole claim

bothTheLockScreenAndTheShadeShowThisAppsOwnMark reads the posted Notification rather than the source, and checks publicVersion separately. A change made by half is the likely mistake and it fails silently on exactly the surface this product is most careful about.

theStatusBarMarkRendersAsAReadableSilhouette renders the vector and measures alpha coverage. Two failures look identical in source and completely different in the status bar — a vector that draws nothing, and one that draws a filled shape — and neither is caught by asserting a resource id.

Proved to fail: reverting only the public builder to ic_dialog_info:

AssertionError: the LOCK SCREEN icon is not this app's, in DISCREET — the public
version is a separate builder and is the one that matters most
expected:<2131165190> but was:<17301659>
1 test completed, 1 failed        ->  prove-guard exit 0

NotificationPrivacyTest could never run on minSdk, and nobody knew

Found while trying to satisfy this issue's own verify line. GrantPermissionRule asked for POST_NOTIFICATIONS unconditionally — a permission that arrived in API 33. On PeriodMinSdk26 every test in the class failed with Failed to grant permissions before reaching a single assertion, for a reason having nothing to do with notifications.

That is how it stayed hidden: it is the only emulator where it fails, and a green run on a modern image looks like a green run. The class guards what a locked screen shows. "Passes on the newest device" was never the claim worth having.

The rule is conditional now; below 33 no permission is needed to post at all, so a no-op rule is correct rather than a workaround. All six tests now pass on PeriodMinSdk26 — the first time this file has ever run there.

Starting 6 tests on PeriodMinSdk26(AVD) - 8.0.0
tests=6 failures=0 errors=0

Traps from the issue, each addressed

  • Alpha only — every path is #FFFFFFFF, and the render test fails if the shape becomes a blob.
  • Both call sites — asserted separately, and the proof mutates only the public one.
  • NotificationPrivacyTest still passes — it now passes somewhere it never could before.
  • No channel migration — the icon is per-notification, unchanged.

The new drawable also brought core/notifications/src/main/res under checkThemedDrawables (#42, landed just before this), where ic_notification is exempted with its reason: the system masks it to a silhouette and supplies the colour, so a dark variant would never be drawn.

Not verified

The API 36 instrumented run. That emulator dies the moment Gradle starts on this machine today — three attempts, including after stopping the Gradle daemons to free memory — while it ran the entire app-lock UI verification earlier in the same session. Resource contention, not a defect, but I would rather say so than imply a run that did not happen. Both new assertions are resource-id and render checks whose substance does not vary by API level, and the release checklist carries the on-device row.

Fixed in `caf1244`. `core/notifications/src/main/res/drawable/ic_notification.xml` — **redrawn**, not copied. `ic_launcher_monochrome.xml` is a 108dp launcher canvas whose shape sits in the upper safe zone because a launcher crops and masks it; copying that geometry would have put a small mark floating above centre in the status bar. This is a 24dp canvas the ring nearly fills, opaque white throughout, since the system discards colour and supplies its own. Both call sites, `PeriodNotifier.kt:102` and `:109`. ## Two tests, because "which resource id" is not the whole claim **`bothTheLockScreenAndTheShadeShowThisAppsOwnMark`** reads the posted `Notification` rather than the source, and checks `publicVersion` separately. A change made by half is the likely mistake and it fails silently on exactly the surface this product is most careful about. **`theStatusBarMarkRendersAsAReadableSilhouette`** renders the vector and measures alpha coverage. Two failures look identical in source and completely different in the status bar — a vector that draws nothing, and one that draws a filled shape — and neither is caught by asserting a resource id. **Proved to fail:** reverting only the public builder to `ic_dialog_info`: ``` AssertionError: the LOCK SCREEN icon is not this app's, in DISCREET — the public version is a separate builder and is the one that matters most expected:<2131165190> but was:<17301659> 1 test completed, 1 failed -> prove-guard exit 0 ``` ## `NotificationPrivacyTest` could never run on minSdk, and nobody knew Found while trying to satisfy this issue's own verify line. `GrantPermissionRule` asked for `POST_NOTIFICATIONS` unconditionally — a permission that **arrived in API 33**. On `PeriodMinSdk26` every test in the class failed with `Failed to grant permissions` before reaching a single assertion, for a reason having nothing to do with notifications. That is how it stayed hidden: it is the only emulator where it fails, and a green run on a modern image looks like a green run. The class guards what a **locked screen** shows. "Passes on the newest device" was never the claim worth having. The rule is conditional now; below 33 no permission is needed to post at all, so a no-op rule is correct rather than a workaround. **All six tests now pass on `PeriodMinSdk26` — the first time this file has ever run there.** ``` Starting 6 tests on PeriodMinSdk26(AVD) - 8.0.0 tests=6 failures=0 errors=0 ``` ## Traps from the issue, each addressed - **Alpha only** — every path is `#FFFFFFFF`, and the render test fails if the shape becomes a blob. - **Both call sites** — asserted separately, and the proof mutates only the public one. - **`NotificationPrivacyTest` still passes** — it now passes somewhere it never could before. - **No channel migration** — the icon is per-notification, unchanged. The new drawable also brought `core/notifications/src/main/res` under `checkThemedDrawables` (#42, landed just before this), where `ic_notification` is exempted with its reason: the system masks it to a silhouette and supplies the colour, so a dark variant would never be drawn. ## Not verified The **API 36** instrumented run. That emulator dies the moment Gradle starts on this machine today — three attempts, including after stopping the Gradle daemons to free memory — while it ran the entire app-lock UI verification earlier in the same session. Resource contention, not a defect, but I would rather say so than imply a run that did not happen. Both new assertions are resource-id and render checks whose substance does not vary by API level, and the release checklist carries the on-device row.
Sign in to join this conversation.
No Label
P0
P1
P2
release-blocker
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: null/Privacy-Period-Tracker#43
No description provided.