Files
ha-nvidia-shield/tests/fixtures/pm_dump_netflix_resolver.txt
James Mackay 91d9fb3883 feat(intents): discover an app's intents from its activity resolver table (t16)
First half of the approved intents feature (docs/INTENTS_PLAN.md §2): the scan
that answers "what can this app actually be asked to do". No UI is wired here —
this is the discovery module, the manager path it rides on, and the tests.

THE DUMP IS FILTERED ON THE DEVICE. On the user's Shield `pm dump
com.netflix.ninja` is ~2241 lines of whole dumpsys for ONE app, so shipping it
over ADB to read a few dozen resolver lines repeats the mistake `| head -300`
made for app labels (t66 — where a truncation artefact was read as "this app has
no label"). The fetch now sends:

    pm dump <pkg> 2>/dev/null | sed -n "/Activity Resolver Table/,/Service Resolver Table/p" | head -n 400

with a bounded `grep -A 400 -m 1 -F 'Activity Resolver Table:'` fallback for a
device whose toolbox cannot do sed ranges, tried only when the primary answer did
not contain the section. The package id is the only caller-supplied text in
either command and is always shlex.quote'd, and the sed script is built from two
constants — tests drive eight hostile package ids (semicolons, &&, backticks,
$(), pipes, both quote kinds, a newline) and assert that the shell sees the
package as exactly one verbatim token and that no extra command token appears.

WHAT A CANDIDATE CARRIES: component, action, categories and the data
(scheme/authority/path, plus the matcher kind — `/title.*` is a GLOB, not a
path, and a caller that treats it as literal builds the wrong URI). A filter
carrying several schemes/authorities/paths expands to one candidate per
combination, because each is a concrete match, and dedup collapses the
coincidences. MIME keys are deliberately NOT modelled (they are not one of §3's
fields) — such a filter is still reported through its component, action and
categories rather than being invented into a URI.

RANK, NEVER HIDE. The four families the plan names as internal-looking noise
(DEBUG_*, androidx.*, com.google.firebase.*, ConstraintProxy*) are FLAGGED, not
deleted: they sort last so the useful rows come first, and the user still
decides. Ordering is total (flag, action, scheme, authority, path, component,
categories) so a re-scan is byte-identical, and the 200-candidate cap is applied
after dedup and DISCLOSED through truncated/found — a capped answer never looks
complete.

THE CACHE KEEPS AN ANSWER AND A FAILURE APART, structurally: put() is its only
writer, only a scan that RAN calls it, and it derives the entry's state — an
empty candidate list is INTENT_ENTRY_EMPTY, a real ANSWER the device gave, while
a scan whose transport failed caches nothing and is retried. A stale entry is
dropped on read rather than served. The scan itself has one status per outcome
(fresh / fresh_empty / cached / adb_disabled / adb_unavailable / unreadable /
invalid_package) so "ADB is off", "the device did not answer" and "this app
advertises nothing" can never be confused, and the not-answered statuses carry no
candidates at all — an ADB-down scan cannot look like an app with no intents.
ADB disabled and ADB unreachable are told apart from the entry's own option.
Discovery is EXPLICIT and single: one package per request, never a sweep over the
installed apps, and a test drives a watchdog cycle and asserts it starts no scan.
Concurrent requests for the same package share one dump.

THE FIXTURE IS A TRANSCRIPTION, NOT IMAGINATION. tests/fixtures/
pm_dump_netflix_resolver.txt reproduces the user's 2026-10-12 capture structure
(whole-dumpsys envelope, the activity table with its Schemes/Non-Data Actions
groups, the receiver/service/provider tables that bound it) and the test file's
docstring states line by line what is VERBATIM from the ticket/plan and what is
reconstructed — no real Shield was available, and the raw capture is not in this
repository (the same limit t13 recorded). The capture's own entries are each
pinned by an assertion: the cast entry, http/https with the four categories and
the five www.netflix.com path globs, nflx:, netflix:, MAIN with LEANBACK_LAUNCHER
+ LAUNCHER, NETFLIX_KEY_START, DIAL_START and the two DEBUG_* noise actions —
18 candidates, deduplicated, with the receiver/provider tables proven unread.

Evidence: 59 new tests in tests/test_intents.py (parser against the transcribed
capture, degradation on shapes never seen, the command shape and quoting safety,
the cache states and TTL, and the manager: one shell, cached, retried on failure,
fallback recovery, ADB-off versus ADB-down, the cap disclosed, the watchdog
silent, concurrent sharing). No device was available: the parser is proven
against the transcription, and every "runs/does not run" claim is an assertion on
the poller's recorded commands rather than on intentions.

Verify: suites 778/4 and 779/3; ruff clean; drift exit 0 (26 modules, 76 imports,
242 relative imports, 1417 statement blocks, 57 call arguments) with the
relative-import floor raised 195 -> 235 and the statement-block floor 1300 ->
1400 by the new module rather than dodged; the real-HA glob still runs green
(56 passed, 0 skipped).
2026-10-12 12:27:01 +01:00

102 lines
4.3 KiB
Plaintext

DUMP OF SERVICE package:
Activity Resolver Table:
Schemes:
cast: deca7fa com.netflix.ninja/.MainActivity filter fc483a1
Action: "com.google.cast.action.START"
Category: "android.intent.category.DEFAULT"
Scheme: "cast"
http: deca7fa com.netflix.ninja/.MainActivity filter fc483a1
Action: "android.intent.action.VIEW"
Category: "android.intent.category.DEFAULT"
Category: "android.intent.category.BROWSABLE"
Category: "android.intent.category.LEANBACK_LAUNCHER"
Category: "android.intent.category.LAUNCHER"
Scheme: "http"
Authority: "www.netflix.com": -1
Path: "PatternMatcher{GLOB: /title.*}"
Path: "PatternMatcher{GLOB: /watch.*}"
Path: "PatternMatcher{LITERAL, /browse}"
Path: "PatternMatcher{LITERAL, /home}"
Path: "PatternMatcher{GLOB: /deeplink.*}"
https: deca7fa com.netflix.ninja/.MainActivity filter fc483a1
Action: "android.intent.action.VIEW"
Category: "android.intent.category.DEFAULT"
Category: "android.intent.category.BROWSABLE"
Category: "android.intent.category.LEANBACK_LAUNCHER"
Category: "android.intent.category.LAUNCHER"
Scheme: "https"
Authority: "www.netflix.com": -1
Path: "PatternMatcher{GLOB: /title.*}"
Path: "PatternMatcher{GLOB: /watch.*}"
Path: "PatternMatcher{LITERAL, /browse}"
Path: "PatternMatcher{LITERAL, /home}"
Path: "PatternMatcher{GLOB: /deeplink.*}"
netflix: deca7fa com.netflix.ninja/.MainActivity filter fc483a1
Action: "android.intent.action.VIEW"
Category: "android.intent.category.DEFAULT"
Scheme: "netflix"
nflx: deca7fa com.netflix.ninja/.MainActivity filter fc483a1
Action: "android.intent.action.VIEW"
Category: "android.intent.category.DEFAULT"
Scheme: "nflx"
Non-Data Actions:
android.intent.action.MAIN:
deca7fa com.netflix.ninja/.MainActivity filter fc483a2
Action: "android.intent.action.MAIN"
Category: "android.intent.category.LEANBACK_LAUNCHER"
Category: "android.intent.category.LAUNCHER"
com.netflix.action.NETFLIX_KEY_START:
deca7fa com.netflix.ninja/.MainActivity filter fc483a3
Action: "com.netflix.action.NETFLIX_KEY_START"
com.netflix.action.DIAL_START:
deca7fa com.netflix.ninja/.MainActivity filter fc483a4
Action: "com.netflix.action.DIAL_START"
com.netflix.action.DEBUG_PUSH_NOTIFICATION:
deca7fa com.netflix.ninja/.debug.DebugReceiver filter fc483a5
Action: "com.netflix.action.DEBUG_PUSH_NOTIFICATION"
com.netflix.action.DEBUG_FORCE_EXIT:
deca7fa com.netflix.ninja/.debug.DebugReceiver filter fc483a6
Action: "com.netflix.action.DEBUG_FORCE_EXIT"
Receiver Resolver Table:
Non-Data Actions:
androidx.work.DiagnosticsReportReceiver:
deca7fa com.netflix.ninja/androidx.work.impl.diagnostics.DiagnosticsReceiver
com.google.firebase.MESSAGING_EVENT:
deca7fa com.netflix.ninja/com.google.firebase.messaging.FirebaseMessagingService
com.netflix.action.DEBUG_PUSH_NOTIFICATION:
deca7fa com.netflix.ninja/.push.DebugPushReceiver
Service Resolver Table:
Non-Data Actions:
com.google.firebase.INSTANCE_ID_EVENT:
deca7fa com.netflix.ninja/com.google.firebase.iid.FirebaseInstanceIdService
Provider Resolver Table:
Authorities:
com.netflix.ninja.androidx-startup:
deca7fa com.netflix.ninja/androidx.startup.InitializationProvider
Permissions:
Permission [com.netflix.ninja.C2D_MESSAGE]:
sourcePackage=com.netflix.ninja
Packages:
Package [com.netflix.ninja] (7f1a2b3c):
userId=10123
pkg=Package{7f1a2b3c com.netflix.ninja}
nonLocalizedLabel=null
labelRes=0x7f0f0d31
versionCode=61102 minSdk=21 targetSdk=29
versionName=6.11.2 build 3413
codePath=/data/app/com.netflix.ninja-1/base.apk
ConstraintProxy.BatteryBroadcastReceiver=
com.netflix.ninja/androidx.work.impl.constraints.trackers.BatteryChargingProxy
Activity Resolver Table: (nothing else listed for this package)
DUMP OF SERVICE activity:
ACTIVITY MANAGER PENDING INTENTS (dumpsys activity pending)
(nothing)
DUMP OF SERVICE meminfo:
Applications Memory Usage (in Kilobytes):
DUMP OF SERVICE procstats:
Process Stats:
DUMP OF SERVICE usagestats:
In-memory rollup:
DUMP OF SERVICE batterystats:
Battery Stats: