Files
ha-nvidia-shield/docs/INTENTS_PLAN.md
T

195 lines
8.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`.