disable.sh/disable.ps1/check.py: keep the apps that open the inputs and Settings

The launcher switches inputs with ACTION_VIEW on a passthrough channel URI and
opens Settings with android.settings.SETTINGS. Disabling whichever app answers
those leaves the input tiles dead or Settings unreachable. Ask the TV which
apps answer (query-activities) and skip them; HANDLERS_ALLOW=1 overrides.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
VGSandClaude Opus 5.5 committed 2026-10-23 11:15:56 +07:00
1 parent c5d75e8e80
commit 6fa18972f9
7 files changed
+177 -7

No files matched your search

+3 -2
View File
@@ -673,8 +673,9 @@ and says so.
It runs the same safety checks as `tools/disable.sh` before it disables anything, since an update is
the moment they matter again: it leaves setupwraith on while the saved Wi-Fi network belongs to it
(`FORCE_SETUPWRAITH=1` on Ethernet), leaves the packages under the HDMI/AV inputs on
(`TVINPUT_ALLOW=1` if you really want them off), and never takes Home to a vendor `FallbackHome` or
a factory-test app.
(`TVINPUT_ALLOW=1` if you really want them off), leaves on the apps the TV opens its inputs and
Settings with (`HANDLERS_ALLOW=1`), and never takes Home to a vendor `FallbackHome` or a factory-test
app.
Each package it leaves on comes with the reason and exits 1, because the TV is not back to the list
until you deal with that one by hand.
+13 -1
View File
@@ -54,8 +54,10 @@ def answer(command):
return dumpsys_package(command.split()[-1])
if "resolve-activity" in command:
return (state["home"] or "") + "\n"
if "query-activities" in command:
if "query-activities" in command and "category.HOME" in command:
return "".join(f" {component}\n" for component in state["home_activities"])
if "query-activities" in command and "cursor.item/channel" in command:
return "".join(f" {name}/.MainActivity\n" for name in state.get("input_apps", []))
return ""
@@ -306,6 +308,16 @@ class DriftCheckTest(unittest.TestCase):
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn(f"pm disable-user --user 0 {SETUP}", self.sent())
def test_fix_leaves_the_app_that_opens_the_inputs_on(self):
# The update turned the Live TV app back on; it is what the launcher's HDMI tiles open.
self.write_list(["com.example.livetv", "com.example.music"])
self.tv(installed=BASE + ["com.example.livetv"], input_apps=["com.example.livetv"],
disabled=["com.example.store", STOCK])
result = self.run_check("--fix", str(self.list_path))
self.assertIn("leaving com.example.livetv on", result.stdout)
self.assertIn("pm disable-user --user 0 com.example.music", self.sent())
self.assertNotIn("pm disable-user --user 0 com.example.livetv", self.sent())
def test_fix_leaves_setupwraith_on_when_the_wifi_cannot_be_read(self):
self.write_list([SETUP])
self.tv(installed=BASE + [SETUP], disabled=["com.example.store", STOCK], wifi="")
+75
View File
@@ -0,0 +1,75 @@
"""tools/disable.sh must not disable the apps the TV opens its inputs and Settings with.
Run from the repository root: python3 -m unittest discover -s tests -v
"""
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
LIVE_TV = "com.example.livetv"
# com.android.tv.settings itself is refused as a core package; this is a vendor one.
SETTINGS = "com.example.tvsettings"
LIST = f"# My TV\ncom.example.before\n{LIVE_TV}\n{SETTINGS}\ncom.example.after\n"
# What `query-activities --brief` prints: the VIEW-channel answer, then the Settings ones.
HANDLERS = (f'printf "%s\\n" "1 activities found:" " {LIVE_TV}/.MainActivity" == '
f'" {SETTINGS}/.MainSettings" "No activities found"')
class HandlerGuardTest(unittest.TestCase):
def run_disable(self, handlers=HANDLERS, extra_env=None):
with tempfile.TemporaryDirectory() as tmp:
repo = Path(tmp) / "repo"
tools = repo / "tools"
tools.mkdir(parents=True)
shutil.copy2(ROOT / "tools" / "disable.sh", tools / "disable.sh")
client = tools / "venv" / "bin" / "python"
client.parent.mkdir(parents=True)
# Fake tv.py client: answers the handler query, records --batch input.
client.write_text(
'#!/bin/sh\n'
'case "$*" in\n'
f' *cursor.item/channel*) {handlers} ;;\n'
' *resolve-activity*) echo "com.google.android.tvlauncher/.MainActivity" ;;\n'
' *query-activities*) echo " com.google.android.tvlauncher/.MainActivity" ;;\n'
' *--batch*) cat > "$CAPTURE" ;;\n'
' *) ;;\n'
'esac\n')
client.chmod(0o755)
(repo / "debloat.txt").write_text(LIST)
capture = Path(tmp) / "commands.txt"
env = dict(os.environ, CAPTURE=str(capture), **(extra_env or {}))
result = subprocess.run(["bash", str(tools / "disable.sh"), "debloat.txt"], cwd=repo,
env=env, capture_output=True, text=True, stdin=subprocess.DEVNULL)
self.assertEqual(result.returncode, 0, result.stderr)
return capture.read_text().splitlines(), result.stderr
def test_keeps_the_input_and_settings_apps(self):
commands, stderr = self.run_disable()
self.assertEqual(commands[:2], ["pm disable-user --user 0 com.example.before",
"pm disable-user --user 0 com.example.after"])
self.assertIn(f"Skipping {LIVE_TV}: the TV opens its HDMI/AV inputs", stderr)
self.assertIn(f"Skipping {SETTINGS}: the TV opens Settings", stderr)
def test_disables_them_when_nothing_answers(self):
commands, _ = self.run_disable(handlers='echo "No activities found"; echo ==')
self.assertIn(f"pm disable-user --user 0 {LIVE_TV}", commands)
self.assertIn(f"pm disable-user --user 0 {SETTINGS}", commands)
def test_warns_and_keeps_going_when_the_query_fails(self):
commands, stderr = self.run_disable(handlers="exit 1")
self.assertIn(f"pm disable-user --user 0 {LIVE_TV}", commands)
self.assertIn("Could not check which listed packages open", stderr)
def test_allow(self):
commands, _ = self.run_disable(extra_env={"HANDLERS_ALLOW": "1"})
self.assertIn(f"pm disable-user --user 0 {LIVE_TV}", commands)
self.assertIn(f"pm disable-user --user 0 {SETTINGS}", commands)
if __name__ == "__main__":
unittest.main()
+2 -1
View File
@@ -51,7 +51,8 @@ def answer(command):
return "".join(f"package:{name}\n" for name in state["installed"])
if command == "pm list packages -d":
return "".join(f"package:{name}\n" for name in state["disabled"])
if command.startswith("cmd package query-activities"):
# Only the HOME query: the input/Settings handler guard asks the same command for other intents.
if command.startswith("cmd package query-activities") and "category.HOME" in command:
if state["claims_fail"]:
sys.exit(1)
return "".join(f" {c if '/' in c else c + '/.MainActivity'}\n" for c in state["claims"])
+26 -3
View File
@@ -155,13 +155,24 @@ def asks_for_input_hardware(dev, name):
return bool(block and INPUT_PERMISSION in block.group(1))
def handler_apps(dev):
"""The apps the TV opens its inputs with (VIEW on a passthrough channel) and the ones it opens
Settings with, as two sets of package names, the same query disable.sh makes."""
q = "cmd package query-activities --brief"
inputs = recovery.shell(dev, f"{q} -a android.intent.action.VIEW -t vnd.android.cursor.item/channel")
settings = (recovery.shell(dev, f"{q} -a android.settings.SETTINGS") + "\n"
+ recovery.shell(dev, f"{q} -a android.intent.action.MAIN -c android.intent.category.PREFERENCE"))
names = lambda out: set(re.findall(r"^\s*([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z0-9_]+)+)/", out, re.M))
return names(inputs), names(settings)
def safety_checks(dev, names):
"""The names disable.sh would leave enabled, each with the reason, as a list of (name, why).
These are the checks disable.sh runs, in the same order, repeated here for the same reason they
exist there: each one is a package an owner cannot put back from the couch. FORCE_SETUPWRAITH=1
and TVINPUT_ALLOW=1 mean what they mean in disable.sh, so a TV on Ethernet or an owner who wants
the inputs off does not have to remember two switches.
exist there: each one is a package an owner cannot put back from the couch. FORCE_SETUPWRAITH=1,
TVINPUT_ALLOW=1 and HANDLERS_ALLOW=1 mean what they mean in disable.sh, so a TV on Ethernet or an
owner who wants the inputs off does not have to remember more switches.
"""
skipped = []
if SETUPWRAITH in names and not os.environ.get("FORCE_SETUPWRAITH"):
@@ -185,6 +196,18 @@ def safety_checks(dev, names):
"disabled those ports stay black and only a full restore and "
"a cold boot bring them back (set TVINPUT_ALLOW=1 to disable "
"it)"))
if not os.environ.get("HANDLERS_ALLOW") and any(n not in {k for k, _ in skipped} for n in names):
inputs, settings = handler_apps(dev)
for name in names:
if any(name == skipped_name for skipped_name, _ in skipped):
continue
if name in inputs:
skipped.append((name, "the TV opens its HDMI/AV inputs and channels with it, so with "
"it disabled the launcher's input tiles do nothing (set "
"HANDLERS_ALLOW=1 to disable it)"))
elif name in settings:
skipped.append((name, "the TV opens Settings with it, so with it disabled Settings "
"does not open (set HANDLERS_ALLOW=1 to disable it)"))
return skipped
+25
View File
@@ -129,6 +129,31 @@ if (-not $env:TVINPUT_ALLOW -and $pkgs.Count -gt 0) {
}
}
}
# Same as disable.sh: keep the apps the TV opens its HDMI inputs (VIEW on a passthrough channel) and
# Settings with, asked from the TV rather than guessed by name. `$env:HANDLERS_ALLOW=1` disables them anyway.
if (-not $env:HANDLERS_ALLOW -and $pkgs.Count -gt 0) {
$q = 'cmd package query-activities --brief'
$ErrorActionPreference = 'Continue'
$handlers = @(& $py -I (Join-Path $repo 'tools\tv.py') "$q -a android.intent.action.VIEW -t vnd.android.cursor.item/channel 2>/dev/null; echo ==; $q -a android.settings.SETTINGS 2>/dev/null; $q -a android.intent.action.MAIN -c android.intent.category.PREFERENCE 2>/dev/null; true" 2>$null)
$checked = $LASTEXITCODE -eq 0
$ErrorActionPreference = 'Stop'
if (-not $checked) {
Write-Warning 'Could not check which listed packages open the TV''s inputs and Settings (tv.py failed). After this run, open an HDMI input and Settings from the launcher before you put the remote down.'
} else {
$split = [array]::IndexOf(($handlers | ForEach-Object { $_.Trim() }), '==')
for ($i = 0; $i -lt $handlers.Count; $i++) {
if ($handlers[$i] -notmatch '^\s*([A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z0-9_]+)+)/') { continue }
$p = $Matches[1]
if ($pkgs -notcontains $p) { continue }
if ($split -lt 0 -or $i -lt $split) {
Write-Warning "Skipping ${p}: the TV opens its HDMI/AV inputs and channels with it, so with it disabled the launcher's input tiles do nothing. Set `$env:HANDLERS_ALLOW=1 to disable it anyway."
} else {
Write-Warning "Skipping ${p}: the TV opens Settings with it, so with it disabled Settings does not open (and with it the way back to Developer options). Set `$env:HANDLERS_ALLOW=1 to disable it anyway."
}
$pkgs = @($pkgs | Where-Object { $_ -ne $p })
}
}
}
# Same as disable.sh: the state undo needs is recorded before the first command reaches the TV, and
# a failed save leaves the TV untouched (#239).
& $py -I (Join-Path $repo 'tools\recovery.py') --save $List
+33
View File
@@ -190,6 +190,39 @@ if [ -n "$PKGS" ] && [ -z "${TVINPUT_ALLOW:-}" ]; then
fi
fi
# The launcher switches to an HDMI input with ACTION_VIEW on a passthrough channel URI (type
# vnd.android.cursor.item/channel), and its Settings entry opens android.settings.SETTINGS. Whichever
# app answers those on this TV (Live TV, a vendor input app, the Settings app) has to stay: disabled,
# the input tiles do nothing and Settings does not open, which on a set with no Settings button on the
# remote leaves no way to turn ADB back on (a Sony KDL-43W800G lost its inputs, a Sony W800C its
# Settings). Ask the TV which apps answer instead of going by name; HANDLERS_ALLOW=1 disables them anyway.
if [ -n "$PKGS" ] && [ -z "${HANDLERS_ALLOW:-}" ]; then
Q='cmd package query-activities --brief'
# The two lists are split at the "==" line; `; true` so a firmware without a match still answers.
if HANDLERS="$("$PY" -I tools/tv.py \
"$Q -a android.intent.action.VIEW -t vnd.android.cursor.item/channel 2>/dev/null; echo ==; $Q -a android.settings.SETTINGS 2>/dev/null; $Q -a android.intent.action.MAIN -c android.intent.category.PREFERENCE 2>/dev/null; true" \
</dev/null 2>/dev/null | tr -d '\r')"; then
NAME_RE='s#^[[:space:]]*([A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z0-9_]+)+)/.*#\1#p'
INPUT_APPS="$(printf '%s\n' "$HANDLERS" | sed '/^==$/,$d' | sed -n -E "$NAME_RE" | sort -u)"
SETTINGS_APPS="$(printf '%s\n' "$HANDLERS" | sed '1,/^==$/d' | sed -n -E "$NAME_RE" | sort -u)"
for p in $INPUT_APPS; do
printf '%s\n' "$PKGS" | grep -xF "$p" >/dev/null || continue
echo "Skipping $p: the TV opens its HDMI/AV inputs and channels with it, so with it disabled the" >&2
echo "launcher's input tiles do nothing. Set HANDLERS_ALLOW=1 to disable it anyway." >&2
PKGS="$(printf '%s\n' "$PKGS" | grep -vxF "$p" || true)"
done
for p in $SETTINGS_APPS; do
printf '%s\n' "$PKGS" | grep -xF "$p" >/dev/null || continue
echo "Skipping $p: the TV opens Settings with it, so with it disabled Settings does not open (and" >&2
echo "with it the way back to Developer options). Set HANDLERS_ALLOW=1 to disable it anyway." >&2
PKGS="$(printf '%s\n' "$PKGS" | grep -vxF "$p" || true)"
done
else
echo "Could not check which listed packages open the TV's inputs and Settings (tv.py failed). After" >&2
echo "this run, open an HDMI input and Settings from the launcher before you put the remote down." >&2
fi
fi
# The state undo needs goes to disk before the first command reaches the TV, and a failed save stops
# here with the TV untouched: a record written afterwards would describe a TV that is already broken,
# and a retry must not overwrite the one taken before the first change (#239).