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

@@ -1,10 +1,15 @@
"""Shared ADB utilities for Home Assistant custom integrations."""
from .adb_client import AdbClientBase, parse_wakefulness
from .app_label import parse_application_label
from .app_label import (
parse_app_label,
parse_application_label,
parse_label_res,
parse_non_localized_label,
resolve_package_name,
)
from .const import (
ATTR_MEDIA_DAG_JELLYFIN_ENTITY,
ATTR_MEDIA_DAG_NOW_PLAYING,
ATTR_MEDIA_DAG_RECEIVER_ENTITY,
ATTR_MEDIA_DAG_RECEIVER_INPUT,
ATTR_MEDIA_DAG_ROLE,
@@ -31,7 +36,6 @@ from .running_apps import running_apps_from_sessions, running_apps_from_tasks
__all__ = [
"ATTR_MEDIA_DAG_JELLYFIN_ENTITY",
"ATTR_MEDIA_DAG_NOW_PLAYING",
"ATTR_MEDIA_DAG_RECEIVER_ENTITY",
"ATTR_MEDIA_DAG_RECEIVER_INPUT",
"ATTR_MEDIA_DAG_ROLE",
@@ -41,7 +45,11 @@ __all__ = [
"PlaybackIntent",
"ensure_adb_key",
"parse_all_media_sessions",
"parse_app_label",
"parse_application_label",
"parse_label_res",
"parse_non_localized_label",
"resolve_package_name",
"parse_audio_playback",
"parse_foreground",
"foreground_from_logcat",
@@ -1,19 +1,55 @@
"""App friendly-name resolution from the package manifest (P5-1).
"""App friendly-name resolution from the package dump (P5-1, t66).
Parses `pm dump <package>` output for the ``application-label`` line — the
default-locale variant; localized suffixed variants
(``application-label-<lang>:``) are skipped, and multi-label blocks across
locales are tolerated (first default-locale label wins), mirroring the
tolerant pattern of the androidtv-adb core helper.
Parses the label candidate LINES of a `pm dump <package>` / `dumpsys package`
answer. Three shapes are searched, in priority order, because Android firmware
prints different ones (and one of them on this user's Shield prints nothing at
all):
1. ``application-label: 'Netflix'`` — the AAPT-style manifest label, with its
localized suffixed variants (``application-label-<lang>:``) as a fallback.
2. ``nonLocalizedLabel=Netflix`` — the ApplicationInfo field. It is usually
``null`` on a TV device (the label comes from a resource), so a null/empty
value is not a name.
3. ``labelRes=0x7f0f0001`` — a RESOURCE ID, not text. A dump can only name the
app here if it prints a quoted string, so a numeric/hex id is deliberately
NOT used as a name: we would be showing ``0x7f0f0001`` as if it were a name.
Nothing here is a guess about which line exists: every branch is anchored on the
literal prefix Android prints, and a miss returns None so the caller falls
through to the curated map (the answer on a device with no labels at all).
Also exports the ONE package -> name precedence chain, so the media player, the
Current App sensor, the discovery table and the options editor cannot drift:
user map > device label > curated map > raw package id.
"""
import re
#: ``application-label: 'X'`` / ``application-label-de: "X"`` — the AAPT form.
_APPLICATION_LABEL_RE = re.compile(
r"application-label(?:-[A-Za-z_-]+)?:\s*'([^']*)'"
r'|application-label(?:-[A-Za-z_-]+)?:\s*"([^"]*)"'
)
#: ``nonLocalizedLabel=X`` — bare word, quoted string, or ``null``.
_NON_LOCALIZED_LABEL_RE = re.compile(
r"nonLocalizedLabel=(?:'([^']*)'|\"([^\"]*)\"|([^\s,}]+))"
)
#: ``labelRes=0x7f0f0001`` — only a QUOTED value is text; an id is not a name.
_LABEL_RES_RE = re.compile(r"labelRes=(?:'([^']*)'|\"([^\"]*)\")")
#: Values a dump prints that are not names.
_NOT_A_NAME = ("", "null", "0x0", "0")
def _clean(value: str | None) -> str | None:
"""A candidate label, or None when it is empty/``null``."""
text = (value or "").strip()
if text.lower() in _NOT_A_NAME:
return None
return text
def parse_application_label(dump: str) -> str | None:
"""The package's default-locale application label, or None.
@@ -24,7 +60,92 @@ def parse_application_label(dump: str) -> str | None:
if not dump:
return None
for match in _APPLICATION_LABEL_RE.finditer(dump):
label = (match.group(1) or match.group(2) or "").strip()
if label and label not in ("null", ""):
label = _clean(match.group(1) or match.group(2))
if label:
return label
return None
def parse_non_localized_label(dump: str) -> str | None:
"""The ``nonLocalizedLabel=`` value, or None (t66).
``null`` is the common case on a TV and is NOT a name.
"""
if not dump:
return None
for match in _NON_LOCALIZED_LABEL_RE.finditer(dump):
label = _clean(match.group(1) or match.group(2) or match.group(3))
if label:
return label
return None
def parse_label_res(dump: str) -> str | None:
"""A QUOTED ``labelRes=`` value, or None (t66).
``labelRes=0x7f0f0001`` is a resource id — showing it as an app name would be
worse than showing the package id, so only a quoted string counts.
"""
if not dump:
return None
for match in _LABEL_RES_RE.finditer(dump):
label = _clean(match.group(1) or match.group(2))
if label:
return label
return None
def parse_app_label(dump: str) -> str | None:
"""The best device label in ``dump``, trying every shape Android prints.
Priority follows how specific the shape is: the AAPT manifest label first,
then the ApplicationInfo field, then a quoted resource label. None means the
device did not name this app — which, on a device whose dump carries no
labels at all, is the definitive answer that sends the caller to the curated
map (t66).
"""
return (
parse_application_label(dump)
or parse_non_localized_label(dump)
or parse_label_res(dump)
)
def resolve_package_name(
package: str | None,
*,
user_map=None,
device_label=None,
curated=None,
) -> str | None:
"""THE package -> display-name chain (t66): one order, every surface.
1. the user's explicit apps map (their own edit always wins);
2. the device's own label, when the device exposes one;
3. the curated community map (the answer on a device with no labels: see
``DEFAULT_APP_DIRECTORY``);
4. the raw package id, shown ONCE.
``device_label`` may be a VALUE or a CALLABLE. A callable is looked up only
after the user map has missed, which is what keeps tier 2 LAZY: the manager's
``app_label()`` accessor enqueues an on-demand device fetch on a miss, so
calling it for an app the user has already named would spend a shell to learn
something nobody reads.
Returns None only for an empty package, so a caller can always display
something. The id is never doubled into a ``pkg=pkg`` row: tier 4 IS the id,
and there is no tier that formats it as a name as well.
"""
if not package:
return None
mapped = (user_map or {}).get(package)
if mapped:
return str(mapped)
raw_label = device_label(package) if callable(device_label) else device_label
label = _clean(raw_label)
if label:
return label
curated_name = (curated or {}).get(package)
if curated_name:
return str(curated_name)
return package
@@ -17,9 +17,11 @@ ATTR_MEDIA_DAG_RECEIVER_INPUT = "media_dag_receiver_input"
# Jellyfin media_player entity for rich metadata
ATTR_MEDIA_DAG_JELLYFIN_ENTITY = "media_dag_jellyfin_entity"
# Now-playing dict: {state, title, media_type, series?, season?, episode?,
# duration?, position?, thumbnail?, artist?, album?, media_info?, source?}
ATTR_MEDIA_DAG_NOW_PLAYING = "media_dag_now_playing"
# t65 flattened the now-playing reading into separate top-level ``now_playing_*``
# attributes on the media player (``now_playing_via`` names the tier), so the single
# nested ``media_dag_now_playing`` key this file used to define is GONE — the last
# reference to it was removed in t66's cleanup. The flattened names live in
# ``custom_components/nvidia_shield/media_player.py`` (``now_playing_attribute_names``).
# List of running app dicts: [{package, state, title?, artist?}, ...]
ATTR_MEDIA_DAG_RUNNING_APPS = "media_dag_running_apps"