docs(intents): plan the intent catalogue and registered actions

This commit is contained in:
James Mackay committed 2026-10-12 10:55:32 +01:00
1 parent 7b32de789a
commit 8a4e72b80a
1 file changed
+194
+194
View File
@@ -0,0 +1,194 @@
# 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 <pkg> | 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 <resolved>` |
| `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.<name>`, 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: <name>` (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`.