docs(intents): plan the intent catalogue and registered actions
This commit is contained in:
1 parent
7b32de789a
commit
8a4e72b80a
1 file changed
+194
@@ -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`.
|
||||
Reference in new issue
Block a user