# Intent catalogue and registered actions (plan) Scope: let the user discover the Android **intents** an installed app advertises, register the ones worth having, and fire them from Home Assistant — one button per registered intent, plus a `send_intent` action for the ones that need inputs. Status: design agreed in principle by the user (2026-10-12). Not built. Companion docs: `docs/MVP_PLAN.md` (architecture), `docs/RESEARCH_SHIELD.md` (device captures), `docs/RESEARCH_IDEAS.md` (adjacent ideas). **Decisions locked (user):** - Registered **buttons**, set up inside our options menu, plus a **`send_intent`** action. Verbatim: *"The registered buttons, pre-setup within our menu options and a send_intent command seem the best method here. Less messy than the current adb command method of doing that."* - Discovery is **explicit** (the user scans an app), never an automatic sweep. - YAML stays **seed-only** (the standing ruling: we load it only when no UI config exists). --- ## 1. Where we are today | Surface | State | |---|---| | `media_player.play_media` with a URL | Works — sends a `VIEW` intent through the ADB path | | `nvidia_shield.adb_command` | Works but is a raw escape hatch, and until t14 cannot even be aimed at the Shield from the UI | | Intent **discovery** | Does not exist | | Intent **registration** | Does not exist | | `ha_adb_common/intent.py` | **Not Android intents** — it is `PlaybackIntent`, the optimistic-UI dataclass. The name collision is a readability trap worth fixing (§7.4) | So the user's memory of "scanning the available commands for ADB between applications" is a real gap, not something built and forgotten. ### What the device actually exposes (observed, one Shield, Android 11) From the user's capture of `pm dump com.netflix.ninja` on 2026-10-12: - the output is ~**2241 lines** and is a *whole* dumpsys — `DUMP OF SERVICE package:`, then `activity`, `meminfo`, `procstats`, `usagestats`, `batterystats`; - the package section begins with an **`Activity Resolver Table:`** listing, per component/filter, the `Action:`, `Category:`, `Scheme:`, `Authority:` and `Path:` lines, followed by `Receiver Resolver Table:` and `Service Resolver Table:`; - Netflix's table contains real, usable entries (`cast:`, `http`/`https` with `www.netflix.com` path globs, `nflx:`, `netflix:`, `MAIN` + `LEANBACK_LAUNCHER`) and a large amount of internal noise (`com.netflix.action.DEBUG_*`, `androidx.*`, `com.google.firebase.*`, `ConstraintProxy*`). Everything in §2 depends on that shape. It is **observed on one firmware**, so the parser must degrade honestly rather than assume. --- ## 2. Discovery Source of truth: the app's **activity resolver table**. Filter **on the device** — on this Shield an unfiltered `pm dump` ships 2241 lines over ADB for one app: ```sh pm dump | sed -n '/Activity Resolver Table/,/Service Resolver Table/p' ``` with a bounded `grep -A` fallback when the device's `sed`/toolbox cannot do ranges. Keep the transfer to the resolver section; the rest of the dump is never needed. Rules: - **One package at a time, on demand**, triggered by the user from the Intents sub-screen. Never a sweep of every installed app (that is 20 × ~2241 lines on this device). - **Cached** per package with a TTL, in the same spirit as the app-label cache; a scan that *ran* and found nothing is a real answer, a scan whose transport failed is not (the lesson from the discovery-cache defect). - **Rank, do not hide.** Flag internal-looking filters (`DEBUG_*`, `androidx.*`, `com.google.firebase.*`) rather than deleting them — the user decides what is worth registering, and we do not get to guess. A cheaper action-first query also exists (`cmd package query-activities --brief -a …`). **Not verified on this device**; treat as a possible optimisation, not the primary path. Candidates are deduplicated on (component, action, categories, scheme/authority/path). --- ## 3. Data model — a registered intent ```yaml registered_intents: - id: netflix_title # slug; also the button's key name: Netflix — open title # what the user sees package: com.netflix.ninja activity: .MainActivity # either a component… action: android.intent.action.VIEW # …or an action (+ categories) categories: [android.intent.category.DEFAULT] data: "https://www.netflix.com/watch/{id}" # URI template, optional fields: # declared inputs, with defaults - {key: id, type: string, required: true} ``` Mapping to `am start` (through the existing ADB path): | Declared | `am start` | |---|---| | component | `-n com.netflix.ninja/.MainActivity` | | action | `-a android.intent.action.VIEW` | | categories | `-c …` (repeated) | | data template | `-d ` | | `type: string` | `--es key value` | | `type: int` | `--ei key value` | | `type: bool` | `--ez key value` | Notes that matter: - **No eval, no shell interpolation.** `{placeholder}` substitution is plain string replacement, and every value is `shlex.quote`d before it reaches the device. - A missing **required** field is a `ServiceValidationError` naming the field. - An optional `raw` field type passes extras verbatim for advanced cases, documented as **unvalidated** — the escape hatch, not the default. --- ## 4. Surfaces 1. **One button entity per registered intent** — `button.`, pressing it runs the intent with the declared defaults. This is the "register an action for each one" the user asked for, and it is the whole point for no-input intents. 2. **`nvidia_shield.send_intent`** — `intent: ` (a dropdown of registered intents) plus `variables: {…}` supplying that intent's declared fields. This is how per-intent inputs arrive from automations, scripts and dashboards; it mirrors `script.turn_on` with variables because HA offers no clean way to register a service per config entry. The action **returns the device's `am start` output**, so a failure is visible. 3. **Intents sub-screen** in the options flow, the same idiom as the Apps screen: *Add an intent* (pick package → pick a discovered candidate or enter one manually → name it → declare its fields), *Edit*, *Remove*, *Scan an app's intents*. 4. **YAML seeding** — a `registered_intents:` block, honoured only when the UI holds nothing (seed-only ruling). --- ## 5. Failure modes and honesty - `am start` failures (unresolved intent, permission denied, activity gone after an app update) **surface the device's own message** and raise; never a silent success. - **ADB is required.** The protocol transport cannot send intents; when ADB is down the error says so instead of failing obscurely. - An unknown intent name raises, listing the known names. - An intent that the app rejects is a **config** problem, and the docs say so plainly. - Nothing registered ever runs by itself, and discovery never auto-registers anything. --- ## 6. Costs - Discovery: one filtered dump per scanned package, cached. - Invocation: one ADB shell per press. - Storage: registered intents are ordinary options data. --- ## 7. Open questions 1. Does `send_intent` returning a response need `SupportsResponse.OPTIONAL` on our HA floor? Verify against the real installed HA before building on it. 2. Typed fields (string/int/bool) for v1, or string-plus-raw only? Recommendation: typed, since that is what makes the dropdown-plus-variables model pleasant. 3. Broadcasts/receivers (`am broadcast`) — the tables are right there — are **out of scope for v1**; activities and actions only. 4. Rename `ha_adb_common/intent.py` → `playback_intent.py` so the new `intents.py` is not confused with it? Small churn, real clarity gain. 5. Should discovery be exposed as a service as well as a UI screen? Recommendation: UI only in v1. 6. `play_media(media_content_type="intent")`? Recommendation: **no** — the button and the action are clearer, and play_media already carries the URL/VIEW case. --- ## 8. Proposed tickets | Unit | Contents | |---|---| | Discovery | `ha_adb_common/intents.py` + the filtered on-device dump + parser + cache + tests, with a fixture cut from the user's real Netflix capture | | Registration & invocation | Intents sub-screen, per-intent fields, button entities, `send_intent` action, mapping, docs, strings, tests | | Review | Adversarial verification of both, including a rejected-intent and an ADB-down path | --- ## 9. Non-goals - No automatic intent execution or auto-registration from discovery. - No arbitrary shell passthrough in `send_intent` (`adb_command` remains that door). - No broadcasts/receivers in v1. - No extras beyond string/int/bool/raw. - Not a replacement for the app source list or `play_media`.