Project 6: Privacy-First Notification Router

Build a notification trigger adapter that is useful to automation rules without becoming a private-message archive.

Quick Reference

  • Main language: Kotlin
  • Alternative language: Java
  • Difficulty: Level 3 - Advanced
  • Estimated time: 20-28 hours
  • Primary APIs: NotificationListenerService, StatusBarNotification, NotificationChannel, PendingIntent
  • Supporting tools: Jetpack Compose, Room, WorkManager, Android Studio tests
  • Main book: Foundations of Information Security by Jason Andress
  • Safety boundary: selected packages only, minimization before persistence, no credential or OTP routing

1. Learning Objectives

By completing this project, you will be able to:

  1. Explain why notification access is a user-enabled service rather than an ordinary runtime permission.
  2. Model not granted, connected, temporarily disconnected, and revoked as different states.
  3. Normalize posted, updated, and removed notifications into stable domain events.
  4. Apply package, channel, category, and importance filters before reading optional text.
  5. Redact sensitive fields before any event crosses the persistence boundary.
  6. Separate transient Android objects from durable notification metadata.
  7. Treat a PendingIntent as temporary delegated authority rather than serializable data.
  8. Represent stale, canceled, locked, and denied actions as typed outcomes.
  9. Reconcile service state after process death without losing the user’s rule definitions.
  10. Test high-volume notification bursts without blocking the listener callback.
  11. Produce privacy-safe traces that explain why a notification matched or was rejected.
  12. Document the Google Play disclosure and data-handling obligations of notification access.

Mastery signal: You can demonstrate a useful notification-driven rule while proving that unselected apps, prohibited fields, and expired actions never enter durable storage.


2. Theoretical Foundation

2.1 Core Concept: Notification Access Is a Capability

NotificationListenerService is bound by the Android system only after the user enables notification access in Settings. The app cannot silently grant itself this authority. A declaration in the manifest establishes eligibility; it does not prove that the service is currently usable.

The service has at least four meaningful states:

  • Not granted: the user has never enabled access.
  • Connected: onListenerConnected() has established a usable session.
  • Disconnected: the grant may remain, but the system binding is unavailable.
  • Revoked: the user removed access and dependent automation must stop.

Rules should survive all four states. Their execution eligibility changes; their definitions do not disappear.

2.2 Core Concept: Data Minimization Before Persistence

Notifications may expose sender names, message bodies, authentication codes, health details, calendar subjects, or corporate data. Encryption at rest does not justify collecting all of it. The strongest design prevents unnecessary data from crossing into storage at all.

Use a staged pipeline:

  1. Check whether the source package is explicitly allowlisted.
  2. Inspect low-sensitivity routing metadata.
  3. Apply channel, category, and importance predicates.
  4. Read optional text only when a declared rule requires it.
  5. Transform it into a bounded predicate result or redacted token.
  6. Drop the original text before emitting the domain event.

An event such as title contains "build failed" should normally persist only predicate=matched, not the original title.

2.3 Core Concept: Identity Across Updates

A notification can be posted, updated in place, grouped, or removed. Android exposes keys and identifiers, but their useful lifetime is not equivalent to a globally permanent database ID.

Your adapter needs a stable session identity composed from bounded metadata:

source package + notification key + observed generation

The generation increments when a previously removed key reappears. This avoids confusing an old action handle with a new notification that reused similar identifiers.

2.4 Core Concept: Delegated Authority

A notification action commonly carries a PendingIntent. It gives the holder permission to ask the system to perform an operation chosen by the originating app. It is not proof that the operation remains available or appropriate.

Important invariants:

  • Never serialize the framework object.
  • Never assume it survives notification removal.
  • Resolve it only during an explicitly authorized run.
  • Preserve the originating notification identity in the command.
  • Return a typed stale or canceled result.
  • Do not use notification actions to automate purchases, authentication, or destructive confirmation.

2.5 Lifecycle and Backpressure

Listener callbacks are ingestion points, not places for database transactions or slow rule evaluation. A burst from chat synchronization can deliver many callbacks quickly. Normalize, minimize, enqueue, and return.

system callback
     |
     v
allowlist gate -> metadata filter -> redactor -> bounded event queue
                                                    |
                                                    v
                                           pure rule evaluator

Define an overflow policy before coding. Coalescing repeated updates by notification key is usually safer than silently dropping removals.

2.6 Why This Matters

Notification triggers are attractive because many apps already publish meaningful state. They are also an unusually broad observation point. A trustworthy automation product must make the narrow path visible: which packages are observed, which fields are inspected, what is retained, and how revocation disables execution.

2.7 Brief History

  • Notification listener access arrived in API level 18.
  • Notification channels became central to user control in API level 26.
  • Modern Android tightened background execution and user visibility.
  • Current platform guidance emphasizes lifecycle callbacks and user-controlled special access.
  • Play distribution adds disclosure, data safety, and least-privilege review beyond API correctness.

2.8 Common Misconceptions

Misconception: A manifest declaration means access is granted.

Correction: It only makes the service discoverable; the user controls access.

Misconception: A Settings grant means the listener is connected.

Correction: binding can be delayed or lost; connection is runtime state.

Misconception: Hashing notification text always makes it anonymous.

Correction: predictable small domains can be recovered; avoid collecting the text.

Misconception: A PendingIntent is a durable command token.

Correction: it is ephemeral delegated authority that can be canceled or invalidated.

Misconception: Deleting rules is the safest response to revocation.

Correction: disable execution while preserving user configuration and an explanation.

2.9 Key Terms

  • Notification access: user-enabled authority to observe posted notifications.
  • Redaction: irreversible removal or replacement of sensitive content.
  • Minimization: collecting only data required for a declared feature.
  • Delegated authority: permission conveyed by another app through a system token.
  • Coalescing: replacing redundant queued updates with the latest state.
  • Revocation: removal of a previously granted capability.
  • Provenance: evidence describing where an event originated.
  • Typed failure: a specific machine-readable outcome rather than a generic exception.

3. Project Specification

3.1 What You Will Build

Build a test notifier and a router with these screens:

  • Access status: grant, binding, last connection, and last disconnect reason.
  • Source allowlist: explicit app selection without broad inventory assumptions.
  • Rule editor: package, channel, category, importance, and bounded text predicates.
  • Event viewer: sanitized fields and predicate decisions only.
  • Action tester: invokes an allowed action and reports typed outcomes.
  • Privacy ledger: documents fields inspected, retained, and discarded.

3.2 Functional Requirements

  • Default-deny every package until selected by the user.
  • Emit events for post, update, and removal.
  • Include package, channel, category, importance, stable session key, and timestamp.
  • Record whether each configured predicate matched without retaining raw text.
  • Disable dependent rules when the listener is unavailable.
  • Restore rule eligibility after reconnection without recreating the rules.
  • Expose a user action that forgets all retained router metadata.
  • Reject password, OTP, financial, and authentication categories from text inspection.
  • Invoke only explicitly selected, currently live actions.
  • Report stale, canceled, locked, policy_denied, and completed separately.

3.3 Non-Functional Requirements

  • Callback processing should remain bounded and non-blocking.
  • Durable rows must never contain raw notification title or body.
  • Logs must be safe to attach to a bug report.
  • Rules must be testable with synthetic normalized events.
  • Process restart must not convert an unknown capability into connected.
  • The UI must explain the consequence before opening system Settings.
  • The app must work without requesting unrelated permissions.

3.4 Example Interaction

$ post-test-notification --channel builds --importance high
Test Notifier: posted key=test:build:42

Router event
  package: dev.example.testnotifier
  channel: builds
  category: status
  importance: high
  event: posted
  title_predicate: matched
  retained_text: none
  action: available_until_notification_changes

After removal:

Action result: stale_notification
Resolution: wait for a new matching notification

3.5 Real-World Outcome

A reviewer selects only the test notifier, posts several messages, and sees sanitized events. Notifications from every unselected app remain absent. Revoking notification access changes the capability state, visibly disables dependent rules, and preserves their definitions. An action from a removed notification fails safely with a precise explanation.


4. Solution Architecture

4.1 High-Level Architecture

 Android notification manager
             |
             v
 NotificationListenerService
             |
      connection state probe
             |
             v
  +--------------------------+
  | allowlist + policy gate  |
  +--------------------------+
             |
      metadata predicates
             |
      optional bounded read
             |
        redaction boundary
             v
  NormalizedNotificationEvent
             |
      +------+-------+
      |              |
 rule evaluator   redacted journal
      |
 execution plan
      |
 live action registry -> PendingIntent sender

4.2 Component Responsibilities

Component Responsibility Must Not Do
Listener adapter receive callbacks and connection state evaluate complex rules
Allowlist gate reject unselected packages early enumerate every installed app
Metadata filter evaluate low-sensitivity predicates retain text extras
Redactor produce bounded safe fields log the original payload
Event queue absorb bursts and preserve removals grow without a limit
Rule evaluator create deterministic plans hold Android framework objects
Live action registry map session keys to current tokens persist PendingIntent objects
Action executor invoke allowed current actions guess after ambiguous failure
Privacy ledger explain collection decisions expose private notification data

4.3 Domain Data Structures

CapabilityState = NotGranted | Connecting | Connected | Disconnected | Revoked

NotificationEvent {
  sourcePackage
  sessionKey
  generation
  eventKind
  channelId?
  category?
  importanceBucket
  predicateResults[]
  observedAt
}

ActionOutcome = Completed | Stale | Canceled | Locked | PolicyDenied | Failed

These are schemas, not executable Kotlin.

4.4 Ingestion Algorithm

on notification callback:
  if package is not explicitly allowed: return
  derive bounded metadata
  if metadata predicates reject event: emit optional rejection metric; return
  if rule requires optional text and policy allows it:
      evaluate predicate in memory
      discard original text
  create sanitized event
  update live action registry for this notification generation
  enqueue event using bounded coalescing policy
  return immediately

4.5 Critical Invariants

  1. Unselected packages do not cross the first gate.
  2. Raw title and body do not cross the redaction boundary.
  3. Framework notification objects never enter Room.
  4. Removal events are never coalesced away.
  5. Revocation makes execution ineligible immediately.
  6. A command references both notification key and generation.
  7. Unknown state is never reported as healthy.

5. Implementation Guide

5.1 Environment Setup

  • Install current stable Android Studio.
  • Install Android SDK Platform 36 for the reference track.
  • Create an Android 13+ emulator and use one recent physical device if available.
  • Create a separate test-notifier application module.
  • Keep all sample notification text fictional and non-sensitive.
  • Add a debug-only event generator for normalized domain events.

5.2 Suggested Project Structure

notification-router/
  app/
    capability/
    listener/
    policy/
    normalization/
    rules/
    actions/
    persistence/
    ui/
  test-notifier/
  tests/
    unit/
    instrumentation/
    privacy/

5.3 Core Question

How can a notification become a useful trigger without turning the automation app into a permanent archive of private conversations?

Answer it in an architecture decision record before implementing the adapter.

5.4 Concepts to Understand First

  1. Bound-service lifecycle and onListenerConnected().
  2. Notification keys, updates, groups, channels, and removals.
  3. Data minimization and purpose limitation.
  4. Backpressure and event coalescing.
  5. PendingIntent delegation and cancellation.
  6. Process-death recovery and reconciliation.

5.5 Questions to Guide Your Design

  • Which predicate can be evaluated without reading notification text?
  • Which fields are prohibited even for an allowlisted package?
  • What does the UI show when the grant exists but binding is absent?
  • How does a removed notification invalidate an action?
  • How are update bursts coalesced without hiding a removal?
  • What exact evidence proves that raw text never reached storage?
  • How does the user inspect and delete retained metadata?

5.6 Thinking Exercise

Draw timelines for:

  • post -> update -> update -> remove;
  • listener connected -> process killed -> reconnected;
  • action selected -> notification removed -> action invoked;
  • grant revoked while three dependent rules are enabled.

For each point, write the durable state, transient state, visible UI, and allowed next operation.

5.7 Interview Questions

  1. Why is a Settings grant insufficient evidence that a listener is usable?
  2. Where should text redaction occur and why?
  3. How would you handle an event storm from a messaging application?
  4. Why should a PendingIntent not be serialized?
  5. How do you distinguish notification updates from new generations?
  6. What should survive notification-access revocation?

5.8 Hints in Layers

Hint 1 - Start with capability truth

Build the state probe and reconnect behavior before any rule editor.

Hint 2 - Move the privacy boundary left

Reject packages before extracting optional extras.

Hint 3 - Normalize without Android objects

Design a small immutable event schema that unit tests can construct directly.

Hint 4 - Treat actions as leases

Keep live tokens in memory and bind them to notification generation.

Hint 5 - Test absence

Assert that forbidden fields are not present in rows, traces, crash logs, or exports.

5.9 Books That Will Help

Topic Book Suggested focus
Privacy engineering Foundations of Information Security least privilege and data handling
Reliability Release It! failure containment and timeouts
Architecture Design It! boundaries and decision records
Testing Growing Object-Oriented Software, Guided by Tests ports and deterministic fakes

5.10 Implementation Phases

Phase 1 - Capability probe

  • Declare the service correctly.
  • Add a user-initiated Settings journey.
  • Show grant and connection separately.
  • Record connection transitions without private data.

Phase 2 - Test notifier

  • Create channels with different importance.
  • Produce post, update, group, action, and removal cases.
  • Add a deliberately canceled action case.

Phase 3 - Allowlist and policy

  • Default to no selected packages.
  • Add explicit package enrollment.
  • Define prohibited data classes.
  • Freeze processing immediately outside policy.

Phase 4 - Normalization and redaction

  • Map callbacks to domain events.
  • Evaluate bounded predicates in memory.
  • Discard source text.
  • Persist only safe metadata and decisions.

Phase 5 - Backpressure

  • Add a bounded queue.
  • Coalesce redundant updates by generation.
  • Preserve posts and removals.
  • Expose queue health metrics.

Phase 6 - Delegated actions

  • Build a transient action registry.
  • Require explicit rule authorization.
  • Check current capability and generation.
  • Map cancellation to a typed outcome.

Phase 7 - Recovery and UX

  • Kill and recreate the process.
  • Reconcile current service state.
  • Disable and restore dependent rules truthfully.
  • Add privacy-ledger and deletion screens.

5.11 Key Design Decisions

  • Prefer package allowlists over deny lists.
  • Prefer boolean predicate results over retained text.
  • Prefer a bounded session identity over global permanence.
  • Prefer explicit typed failures over retries for stale actions.
  • Prefer deterministic domain tests over framework-heavy tests.

6. Testing Strategy

6.1 Unit Tests

  • Unselected package is rejected before text access.
  • Selected package emits only allowed metadata.
  • Prohibited category blocks optional text predicates.
  • Update coalescing keeps the latest safe state.
  • Removal invalidates the live action generation.
  • Capability reducer distinguishes all lifecycle states.
  • Redaction output cannot contain source title or body.
  • Action outcome maps cancellation and policy denial correctly.

6.2 Integration Tests

  • Post, update, and remove using the test notifier.
  • Revoke access while a rule is enabled.
  • Restore access and verify rule eligibility reconciliation.
  • Cancel a PendingIntent before invocation.
  • Send a burst of hundreds of updates.
  • Restart the app between post and action selection.

6.3 Privacy Tests

  • Query every router table for seeded secret markers.
  • Export traces and search for seeded private strings.
  • Trigger a controlled crash and inspect the report.
  • Verify clipboard and screenshot surfaces expose no raw payload.
  • Confirm unselected app notifications create zero rows.

6.4 Failure-Injection Matrix

Failure Expected result
listener disconnect state becomes unavailable; rules remain
access revoked dependent execution stops immediately
queue full coalesce updates; preserve removal; report metric
action canceled return canceled; do not guess or loop
process death transient actions disappear; durable rules remain
malformed extras produce safe partial metadata or reject
prohibited content no read, no persistence, policy trace only

6.5 Acceptance Transcript

Save a redacted transcript showing:

  1. default-deny behavior;
  2. explicit enrollment;
  3. one matched sanitized event;
  4. one rejected unselected event;
  5. revocation and disabled rules;
  6. reconnection and reconciliation;
  7. stale action failure;
  8. database and trace secret scans returning no matches.

7. Common Pitfalls & Debugging

Problem 1: Grant appears enabled but no events arrive

  • Likely cause: service binding has not reached onListenerConnected().
  • Fix: display connection separately and request rebind only through supported lifecycle behavior.
  • Quick test: observe a connection transition before posting the test notification.

Problem 2: Duplicate runs after notification updates

  • Likely cause: every update is treated as a new generation.
  • Fix: key updates consistently and define edge-versus-level trigger semantics.
  • Quick test: update one notification three times and inspect one causation chain.

Problem 3: Sensitive text appears in logs

  • Likely cause: logging occurs before redaction.
  • Fix: move all diagnostics behind the sanitized domain boundary.
  • Quick test: seed a unique marker and search the exported trace.

Problem 4: Action works once and later crashes

  • Likely cause: stale or canceled delegated authority.
  • Fix: keep it transient, verify generation, and catch cancellation as data.
  • Quick test: remove the notification before selecting its action.

Problem 5: Revocation deletes user rules

  • Likely cause: capability health is embedded in rule identity.
  • Fix: separate durable configuration from current eligibility.
  • Quick test: revoke and restore access without recreating a rule.

Problem 6: Burst freezes the application

  • Likely cause: database or rule work runs in the callback.
  • Fix: normalize minimally, use a bounded queue, and measure latency.
  • Quick test: generate a synchronized burst and profile callback duration.

8. Extensions & Challenges

Extension 1: Notification Digest

Aggregate counts by allowlisted package and category without storing individual text.

Extension 2: Channel-Aware Rules

Explain how channel deletion or user importance changes affect a rule.

Extension 3: Local Differential Privacy Experiment

Compare exact counts with privacy-preserving aggregate telemetry in a synthetic dataset.

Extension 4: Rule Simulator

Replay sanitized synthetic events without needing live notification access.

Extension 5: OEM Compatibility Notebook

Record binding and reconnection behavior on three vendors without claiming universal behavior.

Forbidden Extensions

  • OTP extraction or forwarding.
  • Credential or financial notification automation.
  • Silent collection from every installed app.
  • Uploading raw notification content.
  • Auto-clicking destructive or purchase actions.

9. Real-World Connections

This architecture resembles notification-based workflows in productivity tools, incident-response clients, wearable companions, and accessibility aids. Production systems add localization, enterprise data controls, abuse review, retention configuration, support diagnostics, and extensive OEM testing.

The deeper transferable lesson is that broad observation APIs need a deliberately narrow internal contract. The same pattern applies to email ingestion, webhook receivers, accessibility events, and screen-capture pipelines.

Production Gap Analysis

Learning build Production requirement
one test notifier diverse apps, versions, channels, and OEMs
local allowlist migration, backup, and account-bound preferences
local journal retention policy, export controls, support tooling
simple action call abuse prevention, analytics, localization
manual review documented Play disclosure and privacy review

10. Resources

Official Android Documentation

Policy and Privacy

Suggested Reading Order

  1. Read the listener reference and lifecycle notes.
  2. Review notification channels and action behavior.
  3. Study Play user-data disclosure requirements.
  4. Revisit the parent guide’s capability, privacy, and observability chapters.

11. Self-Assessment Checklist

Knowledge

  • I can distinguish grant state from connection state.
  • I can explain why minimization must precede persistence.
  • I understand post, update, removal, grouping, and generation identity.
  • I can explain why a PendingIntent is delegated authority.
  • I can describe a bounded overflow policy.

Design

  • My router is default-deny by package.
  • My domain events contain no Android framework objects.
  • Raw notification text never enters durable storage.
  • Revocation disables execution without deleting rules.
  • Every action failure is typed and visible.

Verification

  • I tested post, update, removal, and reconnection.
  • I tested stale and canceled actions.
  • I tested a high-volume burst.
  • I scanned storage, exports, and logs for secret markers.
  • I documented the user disclosure and deletion path.

Reflection

  • I can defend every retained field by purpose.
  • I can name one feature I intentionally excluded for safety.
  • I can explain how the design changes under process death.
  • I can identify the remaining OEM compatibility risk.

12. Submission / Completion Criteria

Submit all of the following:

  1. Architecture diagram with the redaction boundary marked.
  2. Capability state machine and transition table.
  3. Notification event schema with field-by-field purpose.
  4. Package and prohibited-content policy document.
  5. Queue capacity and coalescing decision record.
  6. Action lease and typed-failure design.
  7. Unit and integration test report.
  8. Privacy scan showing no seeded secret in durable artifacts.
  9. Redacted acceptance transcript covering revocation and recovery.
  10. Short Play policy and user-disclosure checklist.

Definition of Done

  • Unselected packages never produce domain events or rows.
  • Grant, connected, disconnected, and revoked are distinct visible states.
  • Post, update, removal, stale action, and revocation tests pass.
  • Raw title and body are absent from storage, traces, and exports.
  • Dependent rules visibly degrade without being deleted.
  • Action tokens are transient and generation-bound.
  • Callback work stays within the documented latency budget.
  • No prohibited automation scenario is implemented.
  • Another developer can reproduce the acceptance transcript.
  • The architecture decisions explain tradeoffs, not just final choices.

Expanded from ANDROID_AUTOMATION_APPS_MASTERY.md. For the complete learning path, see the parent guide.