M4: Push notifications with Copy and Open actions (ENG-263) #26

Merged demi.dev
targeting main from stoopkidddd/eng-263-m4-push-notifications-with-copy-and-open-actions
Merged opened by and assigned to demi.dev
targeting main from stoopkidddd/eng-263-m4-push-notifications-with-copy-and-open-actions

Linear: https://linear.app/engineereddev/issue/ENG-263

What's in this PR (22 files, all under apps/android/):

  • notify/PushNotifier implements PushSink: channel pushes (IMPORTANCE_HIGH), eligibility (source ≠ self AND target null or self), just-in-time decryption, §14.6 layouts (note BigText + Copy; link title-or-host + Open + Copy; file name/size + Download → DownloadRequested, "Coming soon" for now), brand icon/accent, setWhen = createdAt, stable id + tag = push id, group summary at 3+.
  • CopyReceiver (non-exported, immutable broadcast PendingIntent): plain-text clip, toast only below API 33, cancels the notification. Open goes through a non-exported OpenActivity that cancels the notification, then ACTION_VIEW for http(s) only.
  • Body tap → MainActivity with pushId, passed through PneumoNavHost (Home consumes it in ENG-264). push_deleted cancels the notification (SyncEngine hook).
  • POST_NOTIFICATIONS denied: no crash; Settings shows "Notifications are off" with a button to system settings (new preview + 2 Roborazzi baselines).
  • Tests: PushNotifierTest, NotificationActionsTest (Robolectric, ShadowNotificationManager, SDK 32 vs 34 toast), SyncEngineTest delete cancel, ShellRestorationTest.

Verify (lead, on eca40d7):

  • mise run check:android → exit 0 (Spotless, detekt, Lint, Roborazzi verify)
  • ./gradlew testDebugUnitTest --rerun → exit 0 (165 tests, 0 failures)
  • ./gradlew pixel6Api34DebugAndroidTest --rerun → exit 0 (11 tests, 0 failures)
  • Acceptance (implementer, API 34 emulator, local random-port server): dev-client send note "hello" → notification → Copy → paste shows hello (screenshot checked by the lead, not committed). Open, three-push grouping and permission recovery also exercised.

Deviation: Open uses a non-exported OpenActivity dispatcher because action-button taps don't honour auto-cancel.

🤖 Generated with Claude Code

Commits 2

M4: Preserve grouped notifications and isolate Open task (ENG-263)
M4: Add push notification actions and record banner baselines (ENG-263)