Files
James Mackay 7b32de789a 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.
2026-10-12 10:54:38 +01:00

152 lines
5.4 KiB
Python

"""App friendly-name resolution from the package dump (P5-1, t66).
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.
Returns None on empty/failed dumps too — callers fall through to the
next name tier.
"""
if not dump:
return None
for match in _APPLICATION_LABEL_RE.finditer(dump):
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