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.
17 KiB
Shield firmware research results (§8 of MVP_PLAN.md, R1–R9)
Captured from the user's Shield. These outputs are the ground truth for parser
fixtures and resolved the R2 verdict. Plan deltas applied to MVP_PLAN.md are
listed at the end.
Environment
| Property | Value |
|---|---|
| Device | NVIDIA Shield (board mdarcy) |
| Android | 11 (ro.build.version.release=11) |
| Fingerprint | NVIDIA/mdarcy/mdarcy:11/RQ1A.210105.003/7825230_4231.0265:user/release-keys |
| SDK | 30 (Android 11; the getprop ro.build.version.sdk call failed on shell truncation — etprop: inaccessible or not found — but Android 11 ⇒ SDK 30) |
Note: the home launcher is not the stock Shield launcher — it is
com.spocky.projengmenu (Project Eternity menu). Foreground detection must not
assume com.google.android.tv.launcher.
R1 — TiviMate package name: ar.tvplayer.tv
$ adb shell pm list packages | grep -iE 'tivi|^package:iv'
package:ar.tvplayer.tv
Verdict: TiviMate's package is ar.tvplayer.tv — not iv.android.tv as
the plan assumed. All match keys, fixtures, and constants use ar.tvplayer.tv.
R2 — Does TiviMate publish a media session? NO (verdict: branch 3)
Full dumpsys media_session with TiviMate in the foreground playing a live
channel (captured twice, before/after zapping):
MEDIA SESSION SERVICE (dumpsys media_session)
9 sessions listeners.
Global priority session is null
User Records:
Record for full_user=0
...
Sessions Stack - have 1 sessions:
Netflix media session com.netflix.ninja/Netflix media session (userId=0)
ownerPid=5833, ownerUid=10080, userId=0
package=com.netflix.ninja
launchIntent=null
mediaButtonReceiver=null
active=false
flags=3
rating type=0
controllers: 0
state=null
audioAttrs=AudioAttributes: usage=USAGE_MEDIA content=CONTENT_TYPE_UNKNOWN flags=0x800 tags= bundle=null
volumeType=1, controlType=2, max=0, current=0
metadata: null
queueTitle=null, size=0
Audio playback (lastly played comes first)
uid=10117 packages=ar.tvplayer.tv
uid=10006 packages=com.google.android.apps.mediashell
Findings:
- No TiviMate session block at all. TiviMate registers no MediaSession — it cannot be reached via the session stack. It is a pure Phase-3 fallback app.
- The only session present is Netflix's, stale/inactive:
active=false,state=null,metadata=null. The current parser requiresactive=true+ metadata, so it correctly ignores it — but this exact dump must become a fixture pinning that behavior ("inactive no-state sessions are ignored"). - The before/after-zap captures are byte-identical — zapping channels
changes nothing in
dumpsys media_session.
NEW discovery — the "Audio playback" section
The bottom of dumpsys media_session lists apps that recently played audio,
even with no MediaSession:
Audio playback (lastly played comes first)
uid=10117 packages=ar.tvplayer.tv
uid=10006 packages=com.google.android.apps.mediashell
- TiviMate shows up here while playing live TV. This is a zero-cost
supplementary signal: it comes from the same
dumpsys media_sessioncall the integration already makes — no extra round trip. - Use: confirm "this foreground app actually has/played media" (foreground = TiviMate + recent audio playback ⇒ live TV likely playing). Distinguishes "sitting in TiviMate's settings" from "watching a channel".
- Note
com.google.android.apps.mediashell(Chromecast receiver process) also appears — a parse-time ignore-list entry.
R4 — Foreground detection format (Android 11): mResumedActivity is the pattern
$ adb shell dumpsys activity activities | grep -iE 'resumed'
mResumedActivity: ActivityRecord{4f0d030 u0 ar.tvplayer.tv/.ui.MainActivity
t8152}
state=RESUMED stopped=false delayedResume=false finishing=false
Resumed activities in task display areas (from top to bottom):
Resumed: ActivityRecord{4f0d030 u0 ar.tvplayer.tv/.ui.MainActivity t8152}
ResumedActivity: ActivityRecord{4f0d030 u0 ar.tvplayer.tv/.ui.MainActivity t8152}
$ adb shell dumpsys window | grep -E 'mCurrentFocus|mFocusedApp'
mCurrentFocus=null
mFocusedApp=null
mCurrentFocus=Window{980f3f1 u0 ar.tvplayer.tv/ar.tvplayer.tv.ui.MainActivity}
mFocusedApp=ActivityRecord{4f0d030 u0 ar.tvplayer.tv/.ui.MainActivity t8152}
Findings:
- No
topResumedActivityon this firmware (that pattern is newer Android). Primary pattern =mResumedActivity: ActivityRecord{… u0 <pkg>/<activity> …}. KeeptopResumedActivityparsing for future-proofing, checked aftermResumedActivity. dumpsys windowcontains multiplemCurrentFocusentries and the first can be literallymCurrentFocus=null— a naive first-match parser breaks.mResumedActivityis the reliable single source; window/focus parsing is a secondary cross-check at most.- Package extraction: from the
u0 <pkg>/<activity>fragment of the ActivityRecord line (handles multi-line wraps in the raw dump — match on the joined block, not per-line). - Launcher foreground (for the "off/idle in launcher" distinction): with the
user's custom launcher focused,
mResumedActivityshowscom.spocky.projengmenu/.ui.home.MainActivity.
R5 — Wake state: exact value match confirmed
Awake: mWakefulness=Awake
mWakefulnessChanging=false
Asleep: mWakefulness=Asleep
mWakefulnessChanging=false
Verdict: the current "Awake" in result substring heuristic happens to
work on this firmware, but switch to an exact value parse of the
mWakefulness= assignment (robust against future values like
mWakefulness=Dozing).
R6 — Recent-tasks: not run (optional; low risk)
Still outstanding if media_dag_running_apps needs verification on this
firmware.
R7 — Channel digit keyevents: skipped by decision
The user already uses media_player.play_media with
media_content_type: channel through the androidtv_remote integration and
reports it "works perfectly fine" — so we replicate core's exact behavior
rather than live-testing keyevent variants:
- digits only (no confirm/ENTER key), inter-digit delay 0.1 s (core default),
- cancel any prior in-flight channel task before sending,
- keep ENTER as an optional boolean (default off) in case a future app needs it.
R7 stays in the doc as an optional verification step if protocol-path digits ever misbehave.
R8 — Logcat event reliability: ActivityTaskManager: START lines confirmed
10-08 06:54:33.748 3693 5205 I ActivityTaskManager: START u0
{act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER]
flg=0x10200000 cmp=ar.tvplayer.tv/.ui.MainActivity} from uid 2000
10-08 06:54:50.439 3693 5212 I ActivityTaskManager: START u0
{act=android.intent.action.MAIN
cat=[android.intent.category.LEANBACK_LAUNCHER] flg=0x10220000
pkg=com.spocky.projengmenu cmp=com.spocky.projengmenu/.ui.home.MainActivity
(has extras)} from uid 10059
Findings:
ActivityTaskManager: START u0 {… cmp=<pkg>/<activity>}lines are emitted on every app open — including the home button (which sends aLEANBACK_LAUNCHERSTART for the launcher). App-switch detection via logcat delta snapshots is viable in both directions.WindowManagerlines were absent from the capture — rely onActivityTaskManagerfirst.- The user has
be.digitalia.mediasession2mqttinstalled (MediaSession2MQTT) — a notification-listener service that publishes media sessions over MQTT. It is part of their current glue (see appendix) and is a candidate to retire once our session + fallback path covers its sensors.
R9 — PlaybackState layout + a known-good Wholphin session
state=PlaybackState {state=3, position=17237, buffered position=70070,
speed=1.0, updated=1408392, actions=7339995, custom actions=[], active item
id=0, error=null}
audioAttrs=AudioAttributes: usage=USAGE_MEDIA content=CONTENT_TYPE_MOVIE flags=0x800 tags= bundle=null
volumeType=1, controlType=2, max=0, current=0
metadata: size=11, description=Twirlywoos, null, null
queueTitle=null, size=1
Findings:
PlaybackStatecarries everything we planned to parse:state=3(playing),position=17237(ms),speed=1.0,updated=1408392(uptime ms, needs conversion — position math must account for the uptime-vs-wallclock gap; mirror HA's convention by computingposition + (now - updated_time)with uptime anchoring).- No duration in PlaybackState — duration lives in metadata keys
(
android.media.metadata.DURATIONinside the size=11 metadata dict, beyond thedescription=field the parser currently reads). Parsing the full metadata key list is a Phase-1 stretch goal;description=gives title/artist/album positionally as before. - The session above is Wholphin (
com.github.damontecres.wholphin) — a Jellyfin client that publishes a proper session with usable metadata. Add it toJELLYFIN_APP_IDS(that constant is currently defined but unused — wire it up) so Jellyfin-family clients route to the rich-metadata path.
play_media channel — current working service call (replicate exactly)
action: media_player.play_media
data:
media:
media_content_id: '{{ channel_number|int }}'
media_content_type: channel
metadata: {}
target:
entity_id:
- media_player.shield_android_tv
Note the current HA service syntax uses the media: block; the entity's
async_play_media(media_type, media_id) receives "channel" / "<n>" the same
as before.
Appendix — the user's current glue this integration replaces
Recorded as requirements context. This is what works today and what MVP should make unnecessary.
Template sensor (app name from the androidtv media_player)
template:
- triggers:
- trigger: homeassistant
event: start
- trigger: state
entity_id:
- media_player.android_tv_10_50_204_4
attribute: app_name
sensor:
- name: Nvidia Shield App Name
unique_id: media_player_nvidia_shield_app_name
icon: "mdi:application"
state: >
{% set adb = "media_player.android_tv_10_50_204_4" %}
{% set adb_state = states(adb) %}
{% if adb_state not in ['unknown', 'unavailable', 'off'] %}
{{ state_attr(adb,'app_name') }}
{% elif adb_state in ['off'] %}
{{ "off" }}
{% else %}
{{ "unknown" }}
{% endif %}
Universal media player (state merging + command routing)
media_player:
platform: universal
name: Nvidia Shield
device_class: tv
unique_id: media_player_nvidia_shield
children:
- media_player.android_tv_10_50_204_4
browse_media_entity: media_player.android_tv_10_50_204_4
state_template: >-
{% set shield_state = states('media_player.android_tv_10_50_204_4') %}
{% set known_app = states('sensor.mediasession2mqtt_2044_application_id') in ["com.google.android.youtube.tv", "com.github.damontecres.wholphin"] %}
{% set wholphin = is_state('sensor.nvidia_shield_app_name', 'Wholphin') %}
{% if(shield_state == "unknown" or shield_state == "unavailable" or shield_state == "off") %}
off
{% elif wholphin %}
{% set playing_state = is_state('binary_sensor.jellyfin_nvidia_shield_playing', 'on') %}
{% set paused_state = is_state('binary_sensor.jellyfin_nvidia_shield_paused', 'on') %}
{% if paused_state %}
paused
{% elif playing_state %}
playing
{% else %}
idle
{% endif %}
{% elif known_app %}
{{ states("sensor.mediasession2mqtt_2044_playback_state") }}
{% else %}
{{ shield_state }}
{% endif %}
commands:
turn_on:
action: script.turn_on_nvidia_shield
turn_off:
action: script.turn_off_nvidia_shield
volume_mute:
action: switch.toggle
target: {entity_id: switch.tv_cabinet_livingroom_speaker_system}
volume_up:
action: script.volume_up_nvidia_shield
volume_down:
action: script.volume_down_nvidia_shield
volume_set:
action: script.volume_set_nvidia_shield
data: {level: "{{ volume_level }}"}
select_source:
action: script.set_specific_app_nvidia_shield
data: {app_name: "{{ source }}"}
attributes:
is_volume_muted: switch.inverse_tv_cabinet_speakers
volume_level: media_player.android_tv_10_50_204_4|volume_level
source: remote.shield_android_tv|current_activity
source_list: remote.shield_android_tv|activity_list
What the MVP replaces vs. keeps
| Current glue | MVP after Phase 2/3 |
|---|---|
sensor.nvidia_shield_app_name template sensor |
Current App sensor reads StateStore (protocol current_app push) |
mediasession2mqtt sensors (*_application_id, *_playback_state) |
ADB media sessions + fallback state (retire the MQTT bridge) |
Universal player state_template app special-casing |
Native state ladder |
browse_media_entity from androidtv_remote |
Phase 4 BrowseMedia app directory |
volume_* scripts (speaker switch + receiver interplay) |
Stays as user automations — but volume_set gains a real protocol path (Phase 2 VOLUME_SET); the receiver/speaker switch logic remains the user's or moves to media zone |
remote.shield_android_tv for source/activity |
Our own remote entity (Phase 2) |
Fixtures to save into tests/fixtures/
| File | Content |
|---|---|
dumpsys_media_session_tivimate_playing.txt |
R2 capture 1 (Netflix stale session + audio playback listing) |
dumpsys_media_session_tivimate_zapped.txt |
R2 capture 2 (identical — pins zap-invariance) |
dumpsys_media_session_wholphin_playing.txt |
R9 capture (playing session with position/metadata) |
dumpsys_activity_activities_tivimate.txt |
R4 capture 1 |
dumpsys_activity_activities_launcher.txt |
R4 capture 2 (custom launcher) |
dumpsys_window_focused.txt |
R4 window capture |
dumpsys_power_wakefulness_{awake,asleep}.txt |
R5 captures |
logcat_activitytaskman_start.txt |
R8 capture |
Plan deltas applied to MVP_PLAN.md
- TiviMate package corrected everywhere:
ar.tvplayer.tv. - R2 verdict resolved: no session → TiviMate is the headline Phase-3 fallback app; README gets it as a known case.
- New fallback signal adopted: "Audio playback (lastly played comes first)"
section of
dumpsys media_session— zero extra round trip; parsed alongside sessions;com.google.android.apps.mediashellin the ignore list. - Foreground parser:
mResumedActivityprimary (Android 11),topResumedActivitynewer-fallback;mCurrentFocus=nulltrap documented; launcher special-casing not stock. - Wake parse: exact
mWakefulness=value match. play_media(channel): replicate core behavior exactly (digits, 0.1 s, no ENTER); R7 marked skipped-by-decision.- Wholphin (
com.github.damontecres.wholphin) added toJELLYFIN_APP_IDS; wiring that constant becomes a Phase-0/1 item. - Position math:
updatedis uptime-ms — needs uptime anchoring in Phase 1. - Migration note: retire mediasession2mqtt sensors + universal-player YAML once Phase 3 lands (documented in README).
- Netflix stale-session fixture pins "inactive no-state sessions ignored".
Appendix — mediasession2mqtt retirement + universal-player glue (Phase 3)
The fork retires the surrounding glue once the state ladder (README, Phase 3) covers what the external pieces provided:
| External piece | What it provided | Now native (file) |
|---|---|---|
mediasession2mqtt bridge sensors |
per-app now-playing pushed over MQTT from the Shield's notification listener | ADB media-session polling + the foreground tier (connection.py, media_player.py); the reading is published as separate top-level now_playing_* attributes, and now_playing_via names the tier (jellyfin | media_session | app) |
Universal media player media_dag_running_apps template |
app switcher list with playback states | running_apps_from_tasks(..., last_known=...) (ha_adb_common/running_apps.py) — enriched with foreground/state/last_known_state/last_known_title |
| Universal player "currently in app X" template | fallback state for no-session apps | the foreground ladder tier (media_player.py state property, tier 3.5) |
| External androidtv_remote entity for commands | remote keys | the protocol-backed remote entity (Phase 2) — the external entity remains only as the transitional last command tier, removed in Phase 4 |
| Last-channel / "recently played" sensors | what was on when the session vanished | the TTL-capped last-known-state cache + _last_channel + the audio-playback hint (connection.py) |
The migration path: point the automations/scripts at the integration's DAG
attributes (media_dag_*) instead of the MQTT sensors, then remove
mediasession2mqtt and the universal-player config entries. No YAML glue is
needed for the MVP feature set; the appendix exists so retirement can be
staged per household.
TiviMate (ar.tvplayer.tv) is the documented no-session case (R2): it
publishes no MediaSession on Shield Android 11. Its visibility comes from the
foreground tier (tier 3.5) with the audio-playback hint and the
last-known-state cache — never from a session, never resurrected past the
state TTL.