t66: app names on a device that exposes none

The reported Shield names no app: `pm dump com.netflix.ninja` is ~2241 lines of
dumpsys and grep for `application-label` over the whole capture finds nothing.
The probe shipped `| head -300`, which cut the dump BEFORE the package section,
so "no label" was a truncation artefact; the sweep then named every app after
its own package id, and the apps editor pre-filled the `pkg=pkg` row the user
read as a bug.

  * the probe greps ON THE DEVICE for the three shapes Android prints
    (`application-label`, `nonLocalizedLabel`, `labelRes`); a quoted value is a
    name, `null` and a resource id are not. The dump is never truncated.
  * after APP_LABEL_PROBE_LIMIT consecutive "ran, no label" answers the device
    is taken at its word: the sweep stops spending a shell per package
    (APP_LABEL_TTL expiry re-checks, and a package whose probe FAILED still owes
    its retry — t57 UNIT 0 survives, driven by the app on screen).
  * ONE precedence chain in `ha_adb_common/app_label.resolve_package_name`, used
    by the media player, the Current App sensor, the discovery table, the source
    list and the apps editor: user map > device label > curated map > package id
    (the id shown ONCE, `name` empty when nothing names the app).
  * the curated table covers the user's 21 apps (community-maintained, not
    device truth) and the Configure row says when the device named nothing.
  * the real device shape is a test fixture (trimmed, and marked as such in the
    file: the raw capture is not in the repo) pinning that no label is
    extracted, that the probe count is bounded, and that `Netflix` surfaces.

Also drops the dead `ATTR_MEDIA_DAG_NOW_PLAYING` re-exports left by the t65
flattening and updates the RESEARCH_SHIELD now-playing note to the flat names.

No real Shield was available: the fetch/burst behaviour is exercised against the
documented Android output and the trimmed reproduction of the reported capture.
This commit is contained in:
James Mackay committed 2026-10-12 10:54:38 +01:00
1 parent f29f62ddae
commit 7b32de789a
16 files changed
+973 -176

No files matched your search

+75 -28
View File
@@ -244,7 +244,7 @@ What each retired piece used to provide, and what covers it now:
| Retired piece | Covered by |
|---------------|------------|
| `androidtv` playback state | the ADB media-session state ladder (`playing` / `paused` / `buffering` / `idle` / `off`, plus `on` only when no source reported a power or playback fact) |
| `androidtv` app name / `app_id` lookups | the manager's store + the app-name map + the manifest label of the app that is currently on screen (fetched on demand; a failed fetch is retried on the next poll instead of being cached as a permanent miss) |
| `androidtv` app name / `app_id` lookups | the manager's store + the one app-name chain: your apps map, then the device's own label for the app on screen (fetched on demand; a failed fetch is retried on the next poll instead of being cached as a permanent miss), then the built-in app names, then the package id |
| `androidtv_remote` power, volume and key commands | the protocol transport: the `remote` entity, `media_player` volume, `KEYCODE_*` commands |
| `androidtv_remote` availability | the availability contract (protocol `is_on`, ADB awake, power switch) |
| `universal` media player templates | the media player entity's own attributes plus the `media_dag_*` attributes |
@@ -391,11 +391,16 @@ The **Apps** row opens a small editor rather than one long form. Home Assistant
*Edit* and *Remove* are offered only while there is something to act on: a table with
no remembered rows and no discovery yet shows just the text field and *Add an app*. The
editor's description lists every app it knows — the rows you saved, and the ones
discovery found, marked `[detected]`. **Edit** acts on both (that is the point: the
Shield exposes no app labels of its own, so a detected app reads as `name (package id)`
or as `package id (no name yet)`, and picking it is how you give it a name — saving
stores it as one of your own rows). **Remove** acts only on rows you saved, because a
detected app has no line to delete (it would simply come back with the next sweep).
discovery found, marked `[detected]`. **Edit** acts on both (that is the point: an app
the device does not name reads as `Your Name (package id)` when the map or the built-in
app names know it, and as `package id (no name yet)` when nothing does — picking it is how
you give it a name, and saving stores it as one of your own rows). **Remove** acts only on
rows you saved, because a detected app has no line to delete (it would simply come back
with the next sweep).
A discovered app that nothing can name is **never pre-filled into the text field** as
`package=package`: a row like that would store the package id as if it were a name. It is
offered in the row editor instead, where "no name yet" says what it is.
Removing a row never uninstalls anything from the Shield — the app simply stops being
offered. Rows you do not touch are left exactly as they were.
@@ -508,10 +513,10 @@ outcome that leaves you guessing.
### App discovery (what is actually installed)
The Configure page can ask the Shield which apps it has, so the **Apps** table does not
start from a four-entry guess. Pick **Discover installed apps** on the Configure menu and
the table is rebuilt from two ADB queries — and the row then **says what happened** (see
*What the Discover row tells you* below), because an action that appears to do nothing is
indistinguishable from a broken integration:
start from the built-in app names alone. Pick **Discover installed apps** on the Configure
menu and the table is rebuilt from two ADB queries — and the row then **says what happened**
(see *What the Discover row tells you* below), because an action that appears to do nothing
is indistinguishable from a broken integration:
| Command | What it returns | How it is treated |
| --- | --- | --- |
@@ -529,15 +534,44 @@ capture: no real Shield was available when this was written, so
fixtures — the same "inferred, not observed" mark the state=6 buffering value
carries in the source ladder above.)*
Names come from the same cached path the Current App sensor uses: a `pm dump` per
package, parsed for `application-label`, cached for 24 h. A full sweep runs the dumps
in a small burst (4 at a time, 50 ms apart), which finishes a typical Shield in
seconds instead of the minutes a strictly serialised sweep would take; the commands
share the watchdog's own ADB connection, and `adb-shell` serialises the I/O on it, so
the burst queues politely instead of interleaving on the socket. A label fetch
that **fails is not cached**, so that app shows its package id in this table and the
next sweep tries again — this is the same rule as the entity path, and it is why a
transient ADB hiccup no longer sticks.
Names come from the same cached path the Current App sensor uses, and there is ONE
chain behind every surface — this table, `media_player.app_name`/`source`, the Current
App sensor and the Apps editor. In order, the first answer wins:
1. **your Apps map** — your own edit always wins;
2. **the device's own label** for that app, read from `pm dump <package>`: the probe
greps **on the device** for the label shapes Android prints — `application-label:`,
`nonLocalizedLabel=` and `labelRes=` — so only those lines cross ADB (a real dump on
one reported Shield is ~2241 lines of dumpsys, and the old `head -300` cut it before
the package section even began, which is why it looked like "no labels");
a quoted value counts, `null` and a resource id like `0x7f0f0d31` do not;
3. **the built-in app names** (see *Sources* below) — a curated, community-maintained
`package → name` table;
4. **the package id**, shown **once** (never as `pkg=pkg` in a row).
A label probe is cached for 24 h, and its outcome is remembered either way: a probe that
**ran** and found nothing is a definitive negative (zero further round trips for that
app), while a probe that **could not run** is not cached at all and is retried on the next
poll — this is the same rule as the entity path, and it is why a transient ADB hiccup no
longer sticks.
A full sweep runs a handful of probes at a time (4 at a time, 50 ms apart), which
finishes a typical Shield in seconds instead of the minutes a strictly serialised sweep
would take; the commands share the watchdog's own ADB connection, and `adb-shell`
serialises the I/O on it, so the burst queues politely instead of interleaving on the
socket.
**Some devices expose no labels at all.** The reported Shield is one: nothing in that
2241-line dump names any app. So after 5 probes in a row come back "ran, no label" the
integration stops spending a shell per package for the rest of the 24 h TTL: the sweep
answers the remaining packages from the map instead of probing them, names come from
tiers 1, 3 and 4, and a later device change or update is re-checked when the TTL
expires. The Configure menu says so in its verdict when it happens, and the table's
`name` is then your map's name, the built-in name, or **empty** — an app nothing can name
keeps the id in `package` alone rather than inventing a name out of it.
An app that nothing names is still offered by the **Apps** row editor (it reads
"`com.example.app` (no name yet)"), which is where you give it one.
Icons are **best effort and explicitly heuristic**: `pm dump` does not expose an app's
icon resource (an extracted APK's icon is a bitmap only its own code can resolve), so
@@ -563,6 +597,7 @@ one-line verdict, which replaces the usual "not discovered yet" note:
| --- | --- |
| `N apps found on the Shield — the table is updated.` | the sweep ran, and the editor's text field now carries the discovered rows |
| `the Shield reported no installed apps — the table keeps your saved rows.` | the sweep ran and found nothing to add; your rows are untouched |
| `N apps found — this Shield exposes no app labels over ADB, so names come from the built-in app names and your own rows.` | the sweep ran, and the device itself named nothing: the per-package probes have stopped (see *App discovery* above) |
| `the discovery sweep could not reach the Shield — check the ADB connection; your saved rows are unchanged.` | the sweep failed; nothing was cached as a result and nothing you wrote was lost |
| `ADB is not set up for this entry — nothing could be discovered.` | this entry has no ADB manager (a remote-protocol-only setup), so there is nothing to sweep |
@@ -588,11 +623,20 @@ sweep could not run), while the Configure row forces one.
### Sources (the app list in a dropdown)
The media player's source list is your **Apps** map — the same map app-name resolution
uses. With no map configured it falls back to a small built-in list
(TiviMate, Wholphin, Netflix, YouTube), so a universal-media-player card always has
something to show. Discovery fills the **Configure** table, not this fallback: the
dropdown stays exactly the map you saved, so what a card offers is always what you
chose. An app that discovery found and you want in the dropdown is one row to add.
uses. With no map configured it falls back to the built-in app names, which on a device
that exposes no labels *is* the naming: the table covers the apps the reported Shield has
(Projectivy Launcher, Samba (SMB), X-plore, Plex Media Server, TiviMate, Hyperion Grabber,
Prime Video, MediaSession2MQTT, Speech Services by Google, Button Mapper, Wholphin,
Findroid, Pluto TV, YouTube, Downloader, TV Quick Actions, Argosy, Netflix, RetroArch and
the Google TV launcher, plus Google's speech services), so a card shows
"Netflix" instead of `com.netflix.ninja` with nothing configured at all.
That table is a **community-maintained convenience, not device truth**: package ids are
stable, but display names change with vendors and app updates, and an app it has never
heard of falls through to its package id. Your own map overrides any row in it — one line
in **Apps** — and discovery fills the **Configure** table, not this fallback, so the
dropdown stays exactly what you saved. An app that discovery found and you want in the
dropdown is one row to add.
**Apps are sources, not browser media.** This integration no longer publishes the app
directory through HA's media **Browser**: the Browse dialog shows an **empty tree** for
@@ -610,9 +654,11 @@ entity: media_player.living_room_shield
```
- **`source_list`** — the app names. They come from the map (in the order it lists
them), so editing the map renames the dropdown entries.
- **`source`** — the app currently on screen, resolved exactly like the Current App
sensor: your map first, then the cached device label, then the raw package.
them), so editing the map renames the dropdown entries; with no map, the built-in
app names above.
- **`source`** — the app currently on screen, resolved by the same chain the Current App
sensor uses: your map first, then the cached device label, then the built-in app name,
then the raw package id.
- **`media_player.select_source`** — picks one **by name** and launches it over the
protocol, the same path `play_media` uses for apps. A raw package id works too (that
holds the package id), names match case-insensitively, and a name that
@@ -640,7 +686,8 @@ The map is your own override, so two shapes are accepted rather than papered ove
- A **padded value** typed by hand (`com.x= Netflix `) is shown cleaned in the
dropdown, but `app_name`, `source` and the Current App sensor
display the map value **verbatim** — so `source` can be a string that is not in
`source_list`. A blank value falls through to the raw package instead.
`source_list`. A blank value falls through the rest of the chain instead: the device
label, then the built-in app name, then the raw package id.
### Remote command script