Project 7: Command Surface Deck
Expose one automation command through a Quick Settings tile, shortcut, widget, and notification without duplicating business logic or lying about stale state.
Quick Reference
- Main language: Kotlin
- Alternative language: Java
- Difficulty: Level 2 - Intermediate
- Estimated time: 16-22 hours
- Primary APIs:
TileService, App Shortcuts, App Widgets,PendingIntent - Supporting tools: Jetpack Compose, Glance or RemoteViews, notifications
- Main book: Design It! by Michael Keeling
- Safety boundary: every surface creates the same bounded command envelope
1. Learning Objectives
You will learn to:
- Treat Android entry surfaces as disposable adapters rather than miniature automation engines.
- Define a versioned
CommandEnvelopeshared by every surface. - Use stable routine identifiers instead of serializing mutable routine content.
- Re-resolve current routine state when a command arrives.
- Represent enabled, disabled, deleted, locked, busy, and unavailable outcomes truthfully.
- Design
PendingIntentidentity and mutability deliberately. - Make a tile useful even when its service instance is recreated.
- Keep shortcuts and widgets correct after process death and app upgrades.
- Avoid launching background UI when Android or user policy does not allow it.
- Define repeat-click concurrency before implementation.
- Correlate commands from multiple surfaces in one redacted trace.
- Test stale surfaces as normal product states.
Mastery signal: Four Android surfaces invoke one routine through one command bus, and every stale or blocked state produces a visible, recoverable outcome.
2. Theoretical Foundation
2.1 Ports, Adapters, and One Command Bus
A tile, shortcut, widget, and notification action differ in lifecycle and presentation. They should not differ in the meaning of “run routine.” Each is an input adapter that validates surface-specific input and emits the same domain command.
tile ---------+
shortcut -----+--> CommandEnvelope --> command bus --> current routine --> plan
widget -------+
notification -+
If each surface contains its own execution logic, behavior drifts. One surface may ignore lock state, another may run a deleted routine, and another may use an obsolete action definition.
2.2 Stable Identity Versus Mutable State
External surfaces can outlive the process and the state snapshot that created them. Therefore, store only a stable routine ID, an envelope version, the source surface, and correlation metadata. Resolve the current routine from durable storage at invocation time.
Core invariant:
A surface may identify intent, but it may not assert that the target is still executable.
2.3 PendingIntent Identity
Android may reuse equivalent PendingIntent instances. Identity depends on operation type, target, action, data, categories, and request code; extras alone do not reliably distinguish identities. A good design uses explicit action names, stable data URIs, scoped components, intentional flags, and the narrowest mutability.
Use immutable tokens unless a documented platform integration must fill in data. Explicitly target your own receiver or activity. Never export a command receiver without a justified caller-authentication design.
2.4 Disposable Lifecycle
TileService instances are not permanent. Widgets are rendered from persisted configuration. Shortcuts may be cached by the launcher. Notifications may disappear or be recreated. None should be the source of truth.
The command bus and durable routine repository form the truth. Surfaces are projections that can be refreshed or rebuilt.
2.5 Lock State and Background Launch Restrictions
A command may arrive while the device is locked, the app is backgrounded, or an action requires confirmation. Do not assume you can start an activity immediately. Return a typed user_resolution_required outcome and expose a user-initiated path through a supported surface.
Never use a background-activity launch as a hidden dependency. The command should either complete without UI or explain why direct user action is required.
2.6 Repeat Semantics
Two clicks can arrive nearly together from the same or different surfaces. Decide whether a routine is:
- Drop while running: duplicate requests are rejected.
- Coalesce: duplicates attach to the active run.
- Queue once: one follow-up run is retained.
- Parallel safe: separate executions are allowed.
- Toggle: a second command requests stop rather than another start.
This is domain policy, not a surface-specific accident.
2.7 State Projection
Each surface shows a lossy projection of current state:
- A tile may show active, inactive, or unavailable.
- A shortcut title may be stale until refreshed.
- A widget can show last result and current eligibility.
- A notification can expose Run and Stop actions.
Projections should include “last updated” semantics internally and avoid implying real-time certainty when none exists.
2.8 Why This Matters
Automation tools win adoption through convenient entry points. Convenience becomes technical debt when each entry point is its own product. A shared command contract lets you add surfaces without multiplying execution paths.
2.9 Common Misconceptions
Misconception: The Quick Settings tile service can hold current routine state.
Correction: its process and instance are disposable; resolve durable state on demand.
Misconception: Extras make two PendingIntent objects unique.
Correction: identity requires deliberate action, data, target, and request-code design.
Misconception: A shortcut always represents a valid current target.
Correction: launchers can retain pinned shortcuts after the routine changes.
Misconception: A blocked command should silently do nothing.
Correction: visible typed feedback is part of a trustworthy command surface.
Misconception: Every click should start another run.
Correction: concurrency must follow routine semantics and safety policy.
2.10 Key Terms
- Command envelope: versioned message describing requested intent and provenance.
- Surface adapter: Android-specific component translating interaction into a command.
- Stable ID: durable identity independent of mutable presentation.
- Projection: derived UI state that may lag source truth.
- Correlation ID: identifier linking one command to its run and result.
- Resolution: user action required to continue a blocked command.
- Idempotency key: identifier used to suppress unintended duplicate effects.
- Explicit intent: intent naming a specific component or package.
3. Project Specification
3.1 What You Will Build
Create a “Focus Routine” and expose it through:
- one Quick Settings tile;
- one dynamic or pinned shortcut;
- one home-screen widget;
- one ongoing notification with Run and Stop actions.
Every interaction emits the same envelope and appears in the same trace viewer.
3.2 Functional Requirements
- Use a stable routine ID on all surfaces.
- Include envelope schema version and source surface.
- Resolve current routine state when handling the command.
- Support Run and Stop command kinds.
- Show tile active, inactive, and unavailable states.
- Show the widget’s last result and last refresh time.
- Disable or update dynamic shortcuts when targets become unavailable.
- Preserve an explanatory landing page for stale pinned shortcuts.
- Report locked, disabled, deleted, busy, and unavailable outcomes.
- Use the same concurrency policy across all surfaces.
- Correlate command, plan, run, and result.
- Survive process death without incorrect success state.
3.3 Non-Functional Requirements
- Surface adapters contain no rule-evaluation logic.
- Receivers and services are scoped to the narrowest exposure.
- All
PendingIntentmutability choices are documented. - Command validation occurs before execution.
- No long-lived service assumption is permitted.
- State refresh remains bounded and battery-conscious.
- The result is accessible by label, state description, and contrast.
3.4 Example Interaction
Quick Settings: Focus Routine [inactive]
User taps tile
CommandEnvelope
version: 1
command: run
routine_id: routine.focus
source: quick_settings
correlation_id: 8f2...
Result
status: completed
tile: active
widget: Last run succeeded 10:42
notification: Stop
Deleted-target case:
Pinned shortcut opened
Result: target_deleted
Resolution: Choose another routine
3.5 Real-World Outcome
A reviewer invokes Focus Routine from all four surfaces and sees identical command semantics. Killing the process does not break correctness. Locking the device or deleting the routine results in a clear explanation. Repeated taps follow the documented concurrency rule rather than creating accidental parallel runs.
4. Solution Architecture
4.1 High-Level Architecture
+----------+ +----------+ +--------+ +--------------+
| Tile | | Shortcut | | Widget | | Notification |
+----+-----+ +----+-----+ +---+----+ +------+-------+
| | | |
+-------------+------------+--------------+
|
envelope factory
|
command validator
|
command bus
|
+-----------------+------------------+
| |
current routine repository run registry
| |
+-----------------+------------------+
|
execution planner
|
typed result
|
projection refresh coordinator
4.2 Component Responsibilities
| Component | Responsibility | Must Not Do |
|---|---|---|
| Envelope factory | create versioned bounded command | embed mutable routine definition |
| Surface adapter | translate user interaction | execute actions directly |
| Validator | verify version, kind, target, caller | infer missing authority |
| Command bus | serialize and correlate dispatch | depend on one surface lifecycle |
| Repository | return current routine and eligibility | trust cached launcher state |
| Run registry | enforce repeat semantics | silently fork duplicates |
| Result projector | refresh visible surfaces | claim freshness it cannot prove |
| Resolution activity | explain blocked command | auto-run on open without consent |
4.3 Command Schema
CommandEnvelope {
schemaVersion
commandId
commandKind = Run | Stop
routineId
sourceSurface = Tile | Shortcut | Widget | Notification
issuedAtElapsed
idempotencyKey
}
CommandOutcome = Completed | AlreadyRunning | Disabled | Deleted |
Locked | Unavailable | UserResolutionRequired | Failed
The schema is illustrative and not runnable code.
4.4 Dispatch Algorithm
receive explicit surface intent
parse bounded envelope
reject unsupported schema version
resolve current routine by stable ID
if missing: return Deleted
evaluate current capability and lock requirements
if user action required: return UserResolutionRequired
apply repeat policy using run registry
build current execution plan
execute or attach to existing run
persist typed result
request bounded surface refresh
4.5 Truth Table
| Target state | Run behavior | Surface message |
|---|---|---|
| enabled and idle | start | Running |
| enabled and active | apply repeat policy | Already running or queued |
| disabled | reject | Enable in app |
| deleted | reject | Choose another routine |
| capability absent | reject | Capability unavailable |
| locked, confirmation needed | pause | Unlock to continue |
| process restarted | re-resolve | Current durable state |
4.6 Critical Invariants
- All surfaces emit the same envelope type.
- Mutable routine definitions never live in surface tokens.
- Current eligibility is resolved at command time.
- Every command produces one correlated outcome.
PendingIntentidentity is intentional and tested.- Blocked commands do not silently launch background UI.
- Projections never become source truth.
5. Implementation Guide
5.1 Environment Setup
- Use current stable Android Studio and SDK Platform 36.
- Test on Android versions that cover tiles, dynamic shortcuts, and pinned shortcuts.
- Add a launcher that supports widgets and shortcut pinning.
- Enable notification permission where required by the tested Android version.
- Keep a second emulator snapshot for stale pinned-shortcut scenarios.
5.2 Suggested Structure
command-deck/
domain/commands/
domain/routines/
dispatch/
surfaces/tile/
surfaces/shortcuts/
surfaces/widget/
surfaces/notification/
projections/
resolution/
tests/
5.3 Core Question
How can many Android entry surfaces invoke one current routine without each becoming a separate, stale automation implementation?
5.4 Concepts to Understand First
- Android component and process lifecycle.
- Explicit intents and
PendingIntentidentity. - Tile state and launcher-owned shortcut state.
- App-widget update constraints.
- Command buses and stable identifiers.
- Idempotency and concurrency policy.
- Background activity launch restrictions.
5.5 Design Questions
- Which fields are required in every envelope?
- How will old envelope versions fail safely?
- What makes two command tokens distinct?
- What happens when two surfaces invoke within 100 milliseconds?
- How does a pinned shortcut explain a deleted target?
- Which surface states are authoritative versus advisory?
- How will a locked-device resolution remain user initiated?
5.6 Thinking Exercise
Trace these cases with process, durable state, token, and visible projection:
- tile click after process eviction;
- pinned shortcut after routine deletion;
- widget Run followed immediately by notification Run;
- Stop while no run is active;
- app upgrade with an old envelope version;
- device locked when routine needs confirmation.
5.7 Interview Questions
- Why should extras not be the only
PendingIntentdifferentiator? - What state can safely live in a Quick Settings tile?
- How do pinned and dynamic shortcuts differ operationally?
- How would you prevent duplicate routine effects?
- Why must command handling re-read the routine?
- What is a safe response when background UI cannot launch?
5.8 Hints in Layers
Hint 1 - Build one in-app command first
Prove the command bus before adding system surfaces.
Hint 2 - Add stable identity
Pass only routine ID, command kind, version, and provenance.
Hint 3 - Make stale states deliberate
Delete and disable the target while surfaces still exist.
Hint 4 - Add one adapter at a time
Tile, shortcut, widget, then notification is a useful order.
Hint 5 - Kill the process repeatedly
Correctness must derive from durable truth, not instance fields.
5.9 Books That Will Help
| Topic | Book | Suggested focus |
|---|---|---|
| Architecture | Design It! | ports, adapters, tradeoffs |
| Messaging | Enterprise Integration Patterns | command messages and correlation |
| Reliability | Release It! | duplicate work and failure handling |
| UX | Designing with the Mind in Mind | clear state and feedback |
5.10 Implementation Phases
Phase 1 - Domain command
- Define the versioned envelope.
- Define typed results.
- Create stable routine IDs.
- Write validator tests.
Phase 2 - Command bus
- Resolve current routine state.
- Enforce capability checks.
- Add correlation and redacted traces.
- Define repeat policy.
Phase 3 - Quick Settings tile
- Register the tile service.
- Map clicks to envelopes.
- Refresh from repository state.
- Test service recreation.
Phase 4 - Shortcuts
- Publish a dynamic shortcut.
- Request a pinned shortcut through user action.
- Handle disabled and deleted targets.
- Test an old launcher projection.
Phase 5 - Widget
- Configure a routine target.
- Add Run interaction.
- Display last result and freshness.
- Recover after process death.
Phase 6 - Notification surface
- Create Run and Stop actions.
- Give each token intentional identity.
- Update projection after results.
- Handle notification removal.
Phase 7 - Resolution and hardening
- Add locked-device handling.
- Add user-resolution screens.
- Test duplicate cross-surface clicks.
- Document all exported components.
5.11 Key Decisions
- One bus, not four execution paths.
- Stable IDs, not serialized routines.
- Immutable
PendingIntentby default. - Explicit components for internal commands.
- Typed stale-state results.
- Refresh projections after source-truth changes.
6. Testing Strategy
6.1 Unit Tests
- Every adapter produces an equivalent semantic envelope.
- Unsupported versions are rejected.
- Missing, disabled, and unavailable targets map correctly.
- Repeat policy handles duplicates deterministically.
- Stop while idle returns a typed no-op outcome.
- Correlation persists from command to result.
6.2 Integration Tests
- Invoke Focus Routine from each surface.
- Kill the process before each invocation.
- Delete the routine while a pinned shortcut remains.
- Disable the routine while the tile appears active.
- Tap two surfaces concurrently.
- Lock the device before a confirmation-required command.
- Upgrade from a previous envelope schema fixture.
6.3 Token Tests
- Verify Run and Stop resolve to distinct tokens.
- Verify two routines cannot collide.
- Verify token target is explicit.
- Verify mutability matches documented need.
- Verify stale extras cannot override current repository state.
6.4 Accessibility Tests
- Tile label and state description are meaningful.
- Widget controls have descriptive labels.
- Notification actions are unambiguous.
- Resolution screen explains cause and next action.
- State is not communicated by color alone.
6.5 Failure Matrix
| Failure | Expected behavior |
|---|---|
| process killed | command re-resolves current state |
| target deleted | Deleted plus selection resolution |
| target disabled | Disabled plus enable resolution |
| device locked | no hidden launch; user resolution |
| duplicate command | documented repeat policy |
| old envelope | typed unsupported-version result |
| notification absent | other surfaces remain functional |
6.6 Acceptance Evidence
Record one trace per surface with the same routine ID and command semantics. Include process-death, deletion, lock, repeat-click, Run, and Stop cases. Capture surface screenshots only with fictional data.
7. Common Pitfalls & Debugging
Problem 1: Tile state resets unexpectedly
- Why: instance memory was treated as source truth.
- Fix: re-read repository and active-run state during lifecycle callbacks.
- Quick test: evict process, reopen tile panel, compare with durable truth.
Problem 2: Wrong routine runs from widget
- Why:
PendingIntentidentities collided. - Fix: use explicit target, unique stable data, and tested request identity.
- Quick test: configure two widgets and inspect resolved targets.
Problem 3: Pinned shortcut opens dead screen
- Why: deletion was not modeled.
- Fix: route through a resolution activity that revalidates the target.
- Quick test: pin, delete, then invoke.
Problem 4: Double tap runs twice
- Why: no central run registry or idempotency decision.
- Fix: enforce repeat policy in the command bus.
- Quick test: issue two correlated commands concurrently.
Problem 5: Command disappears on locked device
- Why: background UI launch was assumed.
- Fix: return resolution required and notify through a supported user surface.
- Quick test: lock before invoking a confirmation-required routine.
Problem 6: Four surfaces show four answers
- Why: each adapter owns independent status.
- Fix: project all status from one result repository.
- Quick test: compare timestamps and run IDs after one command.
8. Extensions & Challenges
Extension 1: Surface Configuration
Let a user assign different routines to several widget instances.
Extension 2: Parameter Presets
Reference a durable preset ID without embedding secret or mutable values in tokens.
Extension 3: App Actions
Explore voice invocation using supported App Actions and built-in intents, preserving the same command contract.
Extension 4: Multi-Device Projection
Design, but do not assume, how a wearable companion would request the same command.
Extension 5: Freshness Diagnostics
Show when each projection was last reconciled and why it may be stale.
Forbidden Extensions
- Hidden background activity launches.
- Mutable tokens without documented need.
- Exported unauthenticated command receivers.
- Passing passwords, tokens, or full routines through extras.
- Claiming real-time state when only cached state is known.
9. Real-World Connections
The adapter-and-command-bus pattern appears in home automation, media controls, enterprise launchers, accessibility utilities, and incident-response tools. Production products must handle launcher diversity, OEM tile behavior, localization, app upgrades, analytics privacy, and supportable stale-state recovery.
The same design generalizes beyond Android: menu-bar commands, CLI invocations, webhooks, and voice assistants should all create bounded intent and converge on one current execution model.
Production Gap Analysis
| Learning build | Production requirement |
|---|---|
| one routine | migration-safe catalogs and account sync |
| one launcher | OEM and launcher compatibility matrix |
| local traces | retention, export, support consent |
| basic labels | full localization and accessibility QA |
| one envelope version | negotiated migration and telemetry |
10. Resources
Official Android Documentation
- Create custom Quick Settings tiles
- TileService reference
- Create shortcuts
- Pinned shortcuts
- App widgets overview
- PendingIntent reference
- Background activity launch restrictions
- Secure background activity launches
- App Actions overview
Policy
Suggested Reading Order
- Read tile lifecycle guidance.
- Compare static, dynamic, and pinned shortcuts.
- Study
PendingIntentidentity and mutability. - Review background-launch restrictions.
- Revisit the parent guide’s command-surface chapter.
11. Self-Assessment Checklist
Knowledge
- I can explain why all four surfaces are adapters.
- I understand
PendingIntentidentity. - I can distinguish pinned and dynamic shortcut lifecycle.
- I can explain background launch restrictions.
- I can define repeat semantics for a routine.
Design
- Every surface emits the same envelope schema.
- Tokens contain stable IDs, not mutable routines.
- Current eligibility is resolved at invocation.
- My command receiver exposure is justified.
- Projection freshness is explicit.
Verification
- I invoked the routine from all four surfaces.
- I tested after process death.
- I tested disabled and deleted targets.
- I tested concurrent cross-surface taps.
- I tested locked-device resolution.
Reflection
- I can defend my token uniqueness design.
- I can explain which surface state is advisory.
- I documented one rejected architecture alternative.
- I can describe the migration path for version 2 envelopes.
12. Submission / Completion Criteria
Submit:
- Shared command-envelope specification.
- Diagram showing four adapters and one command bus.
PendingIntentidentity and mutability decision record.- Routine-state and outcome truth table.
- Repeat-click concurrency policy.
- Component export and caller-scope audit.
- Test report covering all surfaces and process death.
- Redacted traces with correlation IDs.
- Accessibility review for labels and state.
- Background-launch and user-resolution explanation.
Definition of Done
- Tile, shortcut, widget, and notification use one versioned envelope.
- Every surface resolves the current routine by stable ID.
- Disabled, deleted, locked, busy, and unavailable are visible.
- Run and Stop tokens cannot collide.
- Repeat behavior is deterministic across surfaces.
- Process death does not create false success state.
- No surface contains execution business logic.
- No long-lived service is assumed.
- All exported components have a documented need and protection.
- Another developer can reproduce every acceptance case.
Expanded from ANDROID_AUTOMATION_APPS_MASTERY.md. For the complete learning path, see the parent guide.