Project 2: Context Signal Monitor
Build a live laboratory that turns noisy Android callbacks into typed, redacted, observable automation events.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Level 2 — Intermediate |
| Time Estimate | 16-22 hours |
| Language | Kotlin; Java for adapters and Scala for a separate pure event-model experiment |
| Prerequisites | Project 1, coroutines basics, Android callbacks, Compose state |
| Key Topics | Broadcasts, callbacks, Flow, normalization, deduplication, backpressure, source health |
1. Learning Objectives
By completing this project, you will:
- Explain the difference between raw observations, state readings, transitions, and durable events.
- Normalize unrelated Android callbacks into one versioned event envelope.
- Choose deliberately among StateFlow, SharedFlow, Channel-like queues, and durable handoff.
- Apply deduplication, debounce, coalescing, sampling, and bounded buffering appropriately.
- Make queue pressure, drops, source lifecycle, and registration health visible.
- Test event adapters with deterministic fakes instead of relying only on physical actions.
- Redact sensitive payload fields before they cross the platform adapter boundary.
- Document which source semantics survive process death and which do not.
2. Theoretical Foundation
2.1 Core Concepts
Observation, state, and event
A platform callback is an observation: Android informed the process about something. A state is the latest known value, such as connected and unmetered. An event is a domain-relevant occurrence, such as network became unavailable. Several observations can describe one state transition. Treating each callback as a separate automation event creates duplicate actions.
Canonical event envelope
Every adapter emits the same outer structure: stable type, schema version, source, occurrence time when known, observation time, identity or deduplication key, sensitivity class, payload, and causation metadata. The payload uses domain values and never retains BroadcastReceiver, Network, Intent, or framework callback objects.
Occurrence time versus observation time
Occurrence time represents when the external change happened. Observation time represents when the app learned about it. They may differ because callbacks are delayed, batched, reconstructed, or injected. Ordering by wall clock alone is unsafe because the user can change the clock. Monotonic time is useful for durations within one boot but is not a durable calendar timestamp.
Backpressure
Backpressure appears when a source produces observations faster than downstream consumers can process them. Android event bursts make unbounded queues dangerous. A source contract must specify whether every occurrence matters, whether only the latest state matters, and what the system does when capacity is exhausted.
Source health
Registered does not always mean healthy, and quiet does not always mean broken. Health evidence can include desired state, registration result, last observation, last canonical event, error, queue depth, drop count, and lifecycle scope. Health must avoid claiming certainty the source cannot prove.
2.2 Why This Matters
An event-condition-action engine can be perfectly deterministic and still behave badly if its events are noisy or misleading. A charger connection may produce battery and power broadcasts. Connectivity callbacks can deliver capability changes around availability. A Compose screen can be recreated while an application-scoped collector continues. Process death removes in-memory registrations entirely.
The source layer converts these platform-specific facts into stable semantics. The engine should not know that one trigger came from a broadcast and another from a network callback. It should receive one canonical envelope with documented confidence and durability.
Visible backpressure is essential. Silently dropping observations makes support impossible; persisting every raw callback creates a privacy and storage problem. A monitor that shows raw-to-canonical transformation teaches the learner to justify every reduction.
2.3 Historical Context / Background
Android once permitted broader manifest-declared implicit broadcast behavior. Modern releases restrict many background broadcasts and encourage context registration, callbacks, and purpose-specific background work. Kotlin coroutines and Flow provide expressive stream tools, but they do not define domain semantics automatically. A SharedFlow buffer policy is an implementation mechanism; the product must still decide what data may be lost.
Connectivity reporting also evolved from simple broadcast assumptions to callback and capability models. Current code should ask what network properties are validated or metered rather than equating Wi-Fi with usable internet.
2.4 Common Misconceptions
- Every callback is an event: callbacks can repeat the same state or describe one transition in stages.
- Debounce and sampling are interchangeable: debounce waits for quiet; sampling emits on a cadence.
- StateFlow is an event log: it represents current state and intentionally conflates updates.
- A SharedFlow prevents loss: its replay, buffer, and overflow configuration determine behavior.
- Manifest receivers are universal background listeners: implicit broadcast limits and process state matter.
- Timestamps create total ordering: wall time can change and sources may have unknown occurrence time.
- Sensitive payload can be redacted later: raw persistence has already created exposure.
3. Project Specification
3.1 What You Will Build
Build a Compose monitor with synchronized Raw Observations and Canonical Events timelines. Implement at least these real sources:
- Power connection and charging/battery state.
- Network availability and capability changes.
- Application foreground/background state.
- Headset or Bluetooth connection where supported and permission-safe.
- A deterministic synthetic debug source.
Each adapter emits raw diagnostics to a debug-only channel and canonical envelopes to the future RuleForge input port.
3.2 Functional Requirements
- Shared envelope: All canonical sources use one versioned event structure.
- Raw timeline: Show callback identity, receipt time, and sanitized payload.
- Canonical timeline: Show type, source, occurrence/observation time, dedupe identity, and sensitivity.
- Transition derivation: Repeated state observations produce at most one meaningful transition.
- Pressure policy: Every source declares capacity and overflow behavior.
- Metrics: Track received, emitted, deduplicated, coalesced, sampled, and dropped counts.
- Source health: Show registration scope, last activity, and current error.
- Synthetic injection: Reproduce event sequences with a debug drawer.
- Lifecycle contract: Mark each source UI-scoped, process-scoped, or durably reconstructed.
- Ingress redaction: Remove or classify sensitive fields before canonical emission.
3.3 Non-Functional Requirements
- Performance: Event storms do not grow memory without bound or freeze Compose.
- Reliability: Registration and unregistration are paired and idempotent.
- Privacy: Debug raw observations are bounded, local, and excluded from release persistence.
- Observability: Every reduction decision increments a metric with a reason.
- Testability: Each adapter accepts a fake platform source and injectable clocks.
- Compatibility: Unsupported sources appear as capability states rather than crashes.
3.4 Example Usage / Output
RAW OBSERVATIONS CANONICAL EVENTS
10:41:02.104 POWER_CONNECTED 10:41:02.131 power.connection.changed
10:41:02.118 BATTERY_CHANGED payload: connected=true
10:41:02.125 BATTERY_CHANGED source: power-adapter
dedupe: power/connected/boot-41
reduction: 3 observations -> 1 event
Metrics
received=3 emitted=1 coalesced=2 dropped=0 queue=0/32
3.5 Real World Outcome
Launch the Monitor screen. The left pane or top tab shows raw callback observations; the right pane or second tab shows canonical events. Each row can expand into timing, source, schema, sensitivity, and reduction evidence.
Connect the emulator to simulated power. Several platform observations may appear, but exactly one power connection transition reaches the canonical timeline. Disconnect it and see one inverse transition. Repeat the same state without transition; the event count does not increase, while the dedupe metric does.
Change network conditions. The raw side shows availability and capability callbacks. The canonical side reports a meaningful transition with validated, metered, and transport categories rather than claiming that a transport guarantees internet access. A burst badge reports coalescing.
Open the debug drawer and inject a ten-observation script. Pause the canonical consumer to create pressure. The bounded queue follows the declared source policy and increments drop or coalesce counters instead of consuming unlimited memory.
Navigate away and return. UI-scoped source behavior matches its contract. Background and foreground state remains truthful. Force-stop the process, reopen, and observe that in-memory history is gone unless explicitly retained, while current state is reconstructed from fresh platform queries.
4. Solution Architecture
4.1 High-Level Design
platform callbacks synthetic scripts
broadcast / network / |
lifecycle / device |
| |
+----------+----------+
v
+---------------+
| Source adapter|
| map + classify|
+-------+-------+
|
raw debug observation stream
|
v
+-------------------------+
| Normalizer |
| compare previous state |
| redact / dedupe / time |
+------------+------------+
|
bounded buffer
|
+---------+----------+
| |
v v
Canonical event bus Source metrics
| |
+---------+----------+
v
Compose monitor
4.2 Key Components
| Component | Responsibility | Key Decisions |
|---|---|---|
| SignalSourcePort | Start, stop, expose health and observations | Explicit lifecycle scope |
| RawObservation | Sanitized debug representation | Never enters the rule engine |
| EventNormalizer | Maps observations and previous state to events | Pure where practical |
| CanonicalEvent | Stable envelope and typed payload | No Android objects |
| DedupeStore | Remembers bounded recent identity/state | Per-source policy |
| PressureController | Applies buffer and overflow policy | Metrics on every reduction |
| SourceHealthRegistry | Combines capability and runtime health | Does not infer quiet equals failure |
| MonitorViewModel | Joins timelines and metrics | Batches UI updates |
4.3 Data Structures
RawObservation
source_id
kind
observed_at_wall
observed_at_monotonic
sanitized_fields
EventEnvelope
event_id
event_type
schema_version
source_id
occurred_at optional
observed_at
dedupe_key optional
sensitivity
payload
causation_id optional
SourceMetrics
received
emitted
deduplicated
coalesced
sampled
dropped
current_queue_depth
4.4 Algorithm Overview
Key Algorithm: Normalize a Stateful Observation
- Receive a platform observation and immediately classify sensitive fields.
- Map it to a domain state candidate.
- Load the previous normalized state for this source key.
- If semantically equivalent, increment deduplicated and stop.
- If multiple observations describe a transition window, apply the documented coalescing policy.
- Construct a canonical envelope with both known times and a stable identity.
- Offer it to the bounded buffer.
- On overflow, apply the source-specific reduction rule and update metrics.
- Publish the current source health and canonical event.
Complexity Analysis:
- Time: O(1) average per observation when state and dedupe lookups are keyed.
- Space: O(S + B + H), where S is source state, B buffer capacity, and H bounded debug history.
5. Implementation Guide
5.1 Development Environment Setup
$ adb devices
List of devices attached
emulator-5554 device
$ adb shell dumpsys battery
Current Battery Service state:
AC powered: false
status: 3
Use an emulator with controllable battery and network settings, one physical device if available, coroutines/Flow, lifecycle libraries, and Compose. Keep debug injection behind a debug-only build boundary.
5.2 Project Structure
context-signal-monitor/
├── event-domain/
│ ├── envelope/
│ ├── payloads/
│ ├── normalization/
│ └── pressure/
├── signal-android/
│ ├── power/
│ ├── connectivity/
│ ├── lifecycle/
│ └── accessory/
├── signal-debug/
│ ├── injector/
│ └── scripted-scenarios/
├── app/ui/monitor/
└── tests/
├── normalizer/
├── flow-control/
└── instrumented/
5.3 The Core Question You Are Answering
When Android tells me something changed, what exactly happened, how durable is that knowledge, and how much noise should enter the rule engine?
Write an answer for each source before choosing a Flow primitive. The primitive implements semantics; it does not decide them.
5.4 Concepts You Must Understand First
Stop and research these before coding:
- State versus event
- Is connected a state or an occurrence?
- How does old-versus-new comparison create a transition?
- Book Reference: Enterprise Integration Patterns — Message and Event Message.
- Android broadcast and callback lifecycle
- Which receiver forms are restricted?
- Who owns registration and unregistration?
- Official Reference: Android broadcasts overview.
- Flow semantics
- What does replay mean?
- What happens under slow collection?
- When is conflation correct?
- Backpressure vocabulary
- How do debounce, sample, coalesce, and drop differ?
- Which losses preserve user meaning?
- Time and identity
- Why maintain wall and monotonic observations?
- Can this source produce a stable dedupe key?
5.5 Questions to Guide Your Design
- Which sources represent latest state and which represent every occurrence?
- What is the maximum legitimate event rate per source?
- Which payload fields must be redacted before debug display?
- What evidence proves registration without claiming future delivery?
- Does this source need replay for a new collector?
- How is a repeated identical state handled?
- What happens if the process dies between external change and observation?
- Which timeline is permitted in a release build?
5.6 Thinking Exercise
Normalize Ten Observations by Hand
Trace this sequence:
1 network available Wi-Fi
2 capabilities Wi-Fi unvalidated
3 capabilities Wi-Fi validated
4 capabilities Wi-Fi validated
5 power connected
6 battery charging
7 battery charging
8 network lost
9 network available cellular
10 synthetic test event
Questions to answer:
- How many canonical events exist?
- Which observations are coalesced or deduplicated?
- Is unvalidated-to-validated a meaningful transition?
- Which identity and timestamps does each event carry?
- What happens if the buffer capacity is two and the consumer is paused?
5.7 The Interview Questions They Will Ask
- What restrictions apply to manifest-declared implicit broadcasts?
- Why should a receiver not start an untracked background thread?
- What is the difference between StateFlow and SharedFlow?
- How do debounce, sample, and conflate differ?
- How do you prevent duplicate charger automations?
- How would you test a source without changing a real phone?
5.8 Hints in Layers
Hint 1: Build the synthetic source first
Prove the event contract before platform complexity.
Hint 2: Keep raw diagnostics separate
Only normalized envelopes reach future rules.
Hint 3: Compare semantic states
Object inequality does not necessarily mean domain change.
Hint 4: Bound every queue
If a buffer has no documented maximum, the design is incomplete.
5.9 Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| Event semantics | Enterprise Integration Patterns | Message, Event Message, Message Identifier |
| Concurrency | Effective Java, 3rd Edition | Items 78-84 |
| Reactive pressure | Reactive Design Patterns | Flow-control chapters |
| Process lifecycle | Operating Systems: Three Easy Pieces | Processes and concurrency |
5.10 Implementation Phases
Phase 1: Envelope and Synthetic Laboratory (4-5 hours)
Goals:
- Define canonical envelope and source health.
- Visualize scripted raw and canonical sequences.
Tasks:
- Define event identity, timing, sensitivity, and payload rules.
- Build a synthetic source with pause and burst controls.
- Render synchronized timelines and metric counters.
Checkpoint: A scripted ten-observation input produces a deterministic transcript.
Phase 2: Real Adapters and Normalization (7-10 hours)
Goals:
- Add power, connectivity, lifecycle, and accessory sources.
- Derive semantic transitions.
Tasks:
- Isolate platform callback registration behind ports.
- Implement source-specific state comparison and dedupe.
- Classify and redact ingress data.
Checkpoint: One charger connection produces one canonical transition despite repeated observations.
Phase 3: Pressure, Lifecycle, and Compatibility (5-7 hours)
Goals:
- Bound resources and prove lifecycle behavior.
- Handle unsupported sources visibly.
Tasks:
- Add capacity and overflow policy per source.
- Add health, drop, coalesce, and queue metrics.
- Test navigation, process recreation, burst load, and two API levels.
Checkpoint: A deliberate storm remains responsive and every reduction is counted.
5.11 Key Implementation Decisions
| Decision | Options | Recommendation | Rationale |
|---|---|---|---|
| Domain input | Raw callbacks; canonical envelopes | Canonical envelopes | Isolates Android semantics |
| Current state | SharedFlow; StateFlow | StateFlow | New collectors need latest state |
| Occurrences | StateFlow; bounded shared event stream | Bounded event stream | Avoids conflating distinct events |
| Pressure | Unbounded; implicit default; explicit per source | Explicit per source | Loss must match meaning |
| Raw history | Persist forever; bounded debug only | Bounded debug only | Minimizes sensitive data |
| Time | Wall only; monotonic only; both where useful | Both | Calendar evidence plus duration ordering |
6. Testing Strategy
6.1 Test Categories
| Category | Purpose | Examples |
|---|---|---|
| Normalizer tests | Prove state-to-transition semantics | Repeated power state emits once |
| Flow-control tests | Prove bounded behavior | Burst with paused collector |
| Adapter tests | Prove mapping and lifecycle | Register/unregister exactly once |
| Compose tests | Prove timeline evidence | Raw and canonical rows differ |
| Instrumented tests | Prove real callbacks | Emulator power and network changes |
| Privacy tests | Prove ingress redaction | Seeded secret absent from persistence |
6.2 Critical Test Cases
- Duplicate power observations: Three raw readings yield one canonical transition.
- Network capability progression: Unvalidated and validated states follow declared semantics.
- Slow consumer: Capacity is never exceeded; reduction metric increments.
- Lifecycle restart: Registration remains paired and no duplicate callback owner appears.
- Process recreation: Current state is freshly queried rather than replayed as a false occurrence.
- Wall-clock change: Observation ordering uses the documented monotonic evidence within a boot.
- Unsupported accessory: Source reports unsupported and never repeatedly retries.
- Sensitive marker: Marker appears in neither canonical persistence nor release diagnostics.
- Synthetic determinism: Same script and clocks produce the same canonical transcript.
6.3 Test Data
source: power
observations:
- connected=true, t=100
- connected=true, t=102
- charging=true, t=104
expected:
emitted=1
deduplicated_or_coalesced=2
event_type=power.connection.changed
source: burst
buffer_capacity=2
consumer=paused
policy=KEEP_LATEST_STATE
expected:
memory_bound=true
reduction_count_greater_than_zero=true
7. Common Pitfalls & Debugging
7.1 Frequent Mistakes
| Pitfall | Symptom | Solution |
|---|---|---|
| Callback equals event | One charger action runs several times | Derive transition from normalized state |
| UI-owned source accidentally | Events stop after navigation | Declare and implement lifecycle scope |
| Unbounded debug history | Memory grows during event storm | Cap history and queue |
| Silent overflow | Missing events have no explanation | Increment typed reduction metric |
| Wall time ordering | Events appear reversed after clock change | Keep observation monotonic evidence |
| Android objects leak | JVM tests require framework | Map at adapter boundary |
7.2 Debugging Strategies
- Compare columns: Find the first raw observation that failed to produce expected canonical output.
- Inspect source health: Confirm desired, registered, last callback, and current error separately.
- Freeze consumers: Exercise pressure behavior intentionally.
- Replay scripts: Use deterministic synthetic sequences before touching device settings.
- Inspect counts: Received must equal emitted plus documented reductions for a closed test window.
- Check owners: Log registration owner identity and balanced stop count in debug builds.
7.3 Performance Traps
Do not render every high-rate callback as an independent Compose mutation. Batch display updates, use lazy bounded lists, and retain summarized metrics. Avoid deep object serialization in logs. Use source-specific equality rather than expensive whole-object comparison when the domain cares about only a few fields.
8. Extensions & Challenges
8.1 Beginner Extensions
- Add timeline filters by source and reduction outcome.
- Add a one-tap reset for bounded debug metrics.
- Export a sanitized synthetic transcript.
8.2 Intermediate Extensions
- Add a source health dashboard with last-seen freshness categories.
- Add a file-backed script format for deterministic test scenarios.
- Compare two pressure policies with the same event storm.
8.3 Advanced Extensions
- Build a source-contract conformance suite for future adapters.
- Add property tests asserting no duplicate transitions for equivalent state runs.
- Measure event latency and UI cost with a Macrobenchmark scenario.
9. Real-World Connections
9.1 Industry Applications
- Automation engines: Normalize OS signals before rule evaluation.
- IoT gateways: Reduce noisy sensor readings into state transitions.
- Network monitors: Track capability changes without equating transport and reachability.
- Analytics pipelines: Make sampling and loss explicit.
- Wearable companions: Reconcile intermittent callbacks and process lifetime.
9.2 Related Open Source Projects
- Android Platform Samples: https://github.com/android/platform-samples — current component and API examples.
- Now in Android: https://github.com/android/nowinandroid — Flow-oriented app architecture.
- Kotlin Coroutines: https://github.com/Kotlin/kotlinx.coroutines — Flow implementation and documentation source.
9.3 Interview Relevance
- Stream semantics show whether you understand data meaning beyond syntax.
- Backpressure demonstrates resource-bounded design.
- Android callback lifecycle reveals platform maturity.
- Deterministic fake sources demonstrate testable boundary design.
10. Resources
10.1 Essential Reading
- Broadcasts overview: https://developer.android.com/develop/background-work/background-tasks/broadcasts — modern receiver guidance.
- ConnectivityManager: https://developer.android.com/reference/android/net/ConnectivityManager — network callbacks and capabilities.
- Read network state: https://developer.android.com/develop/connectivity/network-ops/reading-network-state — current network modeling.
- Battery monitoring: https://developer.android.com/training/monitoring-device-state/battery-monitoring — power and battery state.
- Processes and lifecycle: https://developer.android.com/guide/components/activities/process-lifecycle — transient registrations.
- App architecture: https://developer.android.com/topic/architecture — layered and testable boundaries.
10.2 Video Resources
- Kotlin Flow and Android sessions: https://www.youtube.com/@AndroidDevelopers
- Android architecture pathway: https://developer.android.com/courses/pathways/android-architecture
10.3 Tools & Documentation
- Kotlin Flow: https://kotlinlang.org/docs/flow.html — stream operators and cancellation.
- ADB: https://developer.android.com/tools/adb — device-state experiments.
- App Inspection: https://developer.android.com/studio/debug/app-inspection — runtime diagnostics.
- Macrobenchmark: https://developer.android.com/topic/performance/benchmarking/macrobenchmark-overview — later event-storm measurement.
10.4 Related Projects in This Series
- Previous — Project 1: Capability Navigator: Supplies support and health gates for each source.
- Next — Project 3: RuleForge Core: Consumes canonical envelopes without Android dependencies.
- Project 5: Timekeeper Scheduler: Turns schedule delivery into the same event language.
11. Self-Assessment Checklist
Before considering this project complete, verify:
11.1 Understanding
- I can distinguish observation, state, transition, and event.
- I can explain occurrence time versus observation time.
- I can choose StateFlow or SharedFlow based on semantics.
- I can explain debounce, sample, coalesce, conflate, and drop.
- I know which source state disappears with process death.
11.2 Implementation
- Four real sources and one synthetic source emit one canonical envelope type.
- Raw and canonical timelines visibly differ.
- Every source has bounded capacity and documented overflow behavior.
- Metrics account for received, emitted, and reduced observations.
- Registration lifecycle and process recreation tests pass.
- Sensitive data is classified and redacted before persistence.
- A burst test remains responsive.
11.3 Growth
- I documented one callback sequence that surprised me.
- I can defend one intentional data-loss decision.
- I can explain this adapter boundary in an interview.
- I identified one source that needs durable catch-up rather than a live callback.
12. Submission / Completion Criteria
Minimum Viable Completion:
- Power, connectivity, lifecycle, and synthetic adapters work.
- Canonical envelopes contain type, source, version, timing, identity, and sensitivity.
- One repeated-state scenario deduplicates correctly.
- Raw and canonical timelines plus core metrics are visible.
- Core normalizer tests run without Android.
Full Completion:
- All minimum criteria plus:
- A fourth real device/accessory source has capability-aware fallback.
- Queue pressure, coalescing, drop counts, and source health are visible.
- Process recreation, lifecycle ownership, and redaction tests pass.
- Synthetic scripts reproduce at least five edge scenarios.
Excellence — Going Above & Beyond:
- Add a conformance test every future source adapter must pass.
- Produce a latency and memory report for a sustained event storm.
- Demonstrate a mathematically accounted closed window where received equals emitted plus reductions.
- Compare emulator and physical-device callback sequences and explain the differences.
This guide was generated from ANDROID_AUTOMATION_APPS_MASTERY.md. For the complete learning path, see the parent guide and directory README.