"""App friendly-name resolution from the package dump (P5-1, t66). Parses the label candidate LINES of a `pm dump ` / `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-:``) 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