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.
152 lines
5.4 KiB
Python
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
|