From f3184db9cac88bd9e3ce190ed2c9d361db09e433 Mon Sep 17 00:00:00 2001 From: Mazin Kanuga <97324132+TechnoDudeX@users.noreply.github.com> Date: Sun, 25 Oct 2026 16:15:38 -0400 Subject: [PATCH 1/2] tools: restore.sh turns back on what check.py --fix or a second disable.sh run disabled recovery.extend_record() adds the names the list gained to an existing recovery record, and records an absent package that is now installed and enabled as enabled; disable.sh's save() and check.py --fix run it before anything is disabled, and check.py still never creates a record. Not tried on a TV. Co-Authored-By: Claude Opus 5.5 --- README.md | 16 ++- docs/TROUBLESHOOTING.md | 6 +- tests/test_check_fix_record.py | 182 +++++++++++++++++++++++++++++ tests/test_drift_check.py | 4 +- tests/test_list_header_commands.py | 11 +- tools/check.py | 35 ++++-- tools/recovery.py | 65 ++++++++--- 7 files changed, 280 insertions(+), 39 deletions(-) create mode 100644 tests/test_check_fix_record.py diff --git a/README.md b/README.md index 58718172..882cb8d3 100644 --- a/README.md +++ b/README.md @@ -724,7 +724,9 @@ Before `tools/disable.sh` changes anything it writes down what the TV looked lik in the list whether it was enabled, already disabled or not installed, which component owned Home, and the three animation scales. That record lives in `~/.cleanyourtv/`, one file per TV and list, and a second run of the same list keeps the first record instead of saving the cleaned-up state over it. -If you added packages to the list since, that run adds them to the record before disabling them. +If you added packages to the list since, that run adds them to the record before disabling them, and +a listed package that was not installed then but is installed and enabled now is recorded as enabled. +`check.py --fix` does the same before it disables anything. Keep the list you applied. The restore script replays the record, so a package you had disabled yourself stays disabled and Home goes back to the component that owned it: @@ -756,7 +758,9 @@ TV_HOST=192.168.1.50 tools/venv/bin/python -I tools/check.py examples// It exits with `0` when the TV still matches the list, `1` when something drifted, and `2` when it could not check at all: the TV did not answer, nobody accepted the "Allow USB debugging?" prompt in -time, the list path is not there, or the list disables Android's core packages. Only 1 is drift, so +time, the list path is not there, the list disables Android's core packages, or, under `--fix`, the +recovery record for this TV and list cannot be read or updated (nothing is changed on the TV then). +Only 1 is drift, so a job that mails on 1 alone stays quiet while the check never ran; mail on anything but 0, or on 2 as well, and a weekly job on a home server or a Pi is enough to hear about it: @@ -770,9 +774,11 @@ your shell. `--fix` reapplies the list to what drifted: it switches Home to the launcher the list does not disable (pass a package as the second argument if you run a fork), then disables the listed packages -that came back, and reads the TV back to confirm both. It writes no recovery record, because the -record already holds the state from before the first run: `tools/restore.sh` still returns the TV to -stock afterwards. Home is fixed first; if no launcher is left to take it, `--fix` changes nothing +that came back, and reads the TV back to confirm both. It never writes a recovery record of its own, +because only the first run's record holds the state from before cleanup. When there is one, `--fix` +first adds to it what a second `tools/disable.sh` run would (names the list gained, and a package that +was not installed then and is installed and enabled now), so `tools/restore.sh` still returns the TV +to stock afterwards. Home is fixed first; if no launcher is left to take it, `--fix` changes nothing and says so. It runs the same safety checks as `tools/disable.sh` before it disables anything, since an update is diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 9476cc49..6002d7cd 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -179,8 +179,10 @@ Things that bit us or contributors, so they don't bite you. Commands use `tools/ such command; no TV here has run it yet). It comes back enabled, and without its data unless the uninstall used `-k`. To keep it, comment its line out of your list first: otherwise `check.py` reports it as back on, and `--fix` or the next `disable.sh` run disables it again. If you don't want it, let - the list disable it afterwards, which the TV's own settings can undo. The record still calls it - absent, so `restore.sh` keeps leaving it alone: `pm enable --user 0 ` is its undo. Two cases + the list disable it afterwards, which the TV's own settings can undo. `--fix` or that `disable.sh` run + first changes it from absent to enabled in the record, so `restore.sh` turns it back on too. A record + an older version updated, or a package that was already disabled when that run saw it, still says + absent, and `restore.sh` leaves it alone: `pm enable --user 0 ` is its undo. Two cases this does not cover. An app with no copy on the system partition that only user 0 had is deleted with its APK: the command answers `Package doesn't exist`, and only reinstalling the APK brings it back (the Toshiba's `com.hisense.business.ad`). And a `User 0:` line with diff --git a/tests/test_check_fix_record.py b/tests/test_check_fix_record.py new file mode 100644 index 00000000..2014858d --- /dev/null +++ b/tests/test_check_fix_record.py @@ -0,0 +1,182 @@ +"""check.py --fix and a second disable.sh run keep Undo able to turn back on what they disable. + +restore.sh puts back only what the recovery record holds. check.py --fix never touched the record, so +a name the list gained after the last disable.sh run was disabled by --fix and then left off by +restore.sh for good ("not in the record, leaving alone"). A package the record calls absent, because +the TV did not have it when disable.sh first ran, was left off the same way once an update or the Play +Store installed it and --fix or disable.sh disabled it: save() only ever added missing names. Both now +go through recovery.extend_record(), which adds the names the record does not hold and records an +absent package that is installed and enabled now as enabled. check.py only extends a record that +exists: a record of the TV after cleanup would make Undo leave the whole list off. + +The TV is the drift check's fake tools/tv.py (tests/test_drift_check.py), which applies what --fix and +a batch send, and the real disable.sh, restore.sh, check.py and recovery.py run against it. + +Run from the repository root: python3 -m unittest discover -s tests -v +""" +import json +import os +from pathlib import Path +import shutil +import sys +import unittest +from unittest import mock + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import test_drift_check as drift # noqa: E402 (imported as a module so its tests do not run twice) +import test_recovery_record as record_test # noqa: E402 (the same) + +ROOT = drift.ROOT +STOCK = drift.STOCK +WEATHER = "com.example.weather" + + +class CheckFixRecordTest(unittest.TestCase): + # The drift check's harness: a temporary repo with the fake tv.py, HOME, the list and the TV state. + write_list = drift.DriftCheckTest.write_list + tv = drift.DriftCheckTest.tv + run_check = drift.DriftCheckTest.run_check + sent = drift.DriftCheckTest.sent + # The recovery record test's runner for disable.sh and restore.sh, on the same repo and state. + run_script = record_test.RecoveryRecordTest.run_script + + def setUp(self): + # The record's old name carries the port, so a TV_PORT left in the environment (by the user, + # or by an earlier test in the same run) must not reach the scripts. + environ = mock.patch.dict(os.environ) + environ.start() + self.addCleanup(environ.stop) + os.environ.pop("TV_PORT", None) + drift.DriftCheckTest.setUp(self) + for script in ("disable.sh", "restore.sh"): + shutil.copy2(ROOT / "tools" / script, self.repo / "tools" / script) + # The scripts call tools/venv/bin/python: run the real interpreter, which imports the fake + # tv.py for the transport and the real recovery.py for the logic. + client = self.repo / "tools" / "venv" / "bin" / "python" + client.parent.mkdir(parents=True) + client.write_text(f'#!/bin/sh\nexec "{sys.executable}" "$@"\n') + client.chmod(0o755) + self.write_list(["com.example.music", "com.example.store"]) + + def state(self): + return json.loads(self.state_path.read_text()) + + def set_state(self, **changes): + state = self.state() + state.update(changes) + self.state_path.write_text(json.dumps(state)) + + def record_file(self): + files = list((self.home / ".cleanyourtv").glob("recovery-*.json")) + self.assertEqual(len(files), 1, files) + return files[0] + + def record(self): + return json.loads(self.record_file().read_text())["packages"] + + def apply_list(self): + result = self.run_script("disable.sh", str(self.list_path)) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + return result + + def undo(self): + self.set_state(commands=[]) + result = self.run_script("restore.sh", str(self.list_path)) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + return self.sent(), result.stderr + + def test_a_name_the_list_gained_is_recorded_before_fix_disables_it(self): + self.tv(installed=drift.BASE + [WEATHER], disabled=[]) + self.apply_list() + # A git pull adds a line; the next --fix disables it. + self.write_list(["com.example.music", "com.example.store", WEATHER]) + result = self.run_check("--fix", str(self.list_path)) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn(f"pm disable-user --user 0 {WEATHER}", self.sent()) + self.assertIn("added 1 names the list gained since then", result.stdout) + self.assertEqual(self.record()[WEATHER], "enabled") + + sent, err = self.undo() + self.assertIn(f"pm enable {WEATHER}", sent) + self.assertNotIn("not in the record", err) + + def test_a_package_installed_after_the_record_is_recorded_before_fix_disables_it(self): + self.write_list(["com.example.music", "com.example.store", "com.example.later"]) + self.tv(installed=drift.BASE, disabled=[STOCK]) + self.apply_list() + self.assertEqual(self.record()["com.example.later"], "absent") + # A TV refuses pm disable-user for a package it does not have, which this fake does not; then + # an update installs it, enabled. + state = self.state() + self.set_state(installed=state["installed"] + ["com.example.later"], + disabled=[name for name in state["disabled"] if name != "com.example.later"]) + result = self.run_check("--fix", str(self.list_path)) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("pm disable-user --user 0 com.example.later", self.sent()) + self.assertEqual(self.record()["com.example.later"], "enabled") + + sent, err = self.undo() + self.assertIn("pm enable com.example.later", sent) + self.assertNotIn("leaving com.example.later alone", err) + + def test_disable_run_again_records_a_package_installed_since(self): + self.write_list(["com.example.music", "com.example.store", "com.example.later"]) + self.tv(installed=drift.BASE, disabled=[STOCK]) + self.apply_list() + state = self.state() + self.set_state(installed=state["installed"] + ["com.example.later"], + disabled=[name for name in state["disabled"] if name != "com.example.later"]) + self.apply_list() + self.assertEqual(self.record()["com.example.later"], "enabled") + sent, _ = self.undo() + self.assertIn("pm enable com.example.later", sent) + + def test_entries_from_before_cleanup_are_not_overwritten(self): + self.write_list(["com.example.music", "com.example.store", "com.example.later"]) + self.tv(installed=drift.BASE, disabled=["com.example.store", STOCK]) + self.apply_list() + first = self.record() + self.assertEqual(first["com.example.store"], "disabled") + # Installed since, but disabled: nobody can say by whom, so it stays absent, which restore + # leaves alone exactly as it would leave a disabled one. + state = self.state() + self.set_state(installed=state["installed"] + ["com.example.later"], + disabled=[name for name in state["disabled"] if name != "com.example.music"]) + result = self.run_check("--fix", str(self.list_path)) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual(self.record(), first) + + def test_a_report_without_fix_leaves_the_record_alone(self): + self.tv(installed=drift.BASE + [WEATHER], disabled=[]) + self.apply_list() + before = self.record_file().read_bytes() + self.write_list(["com.example.music", "com.example.store", WEATHER]) + result = self.run_check(str(self.list_path)) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertEqual(self.record_file().read_bytes(), before) + + def test_a_record_fix_cannot_use_stops_it_before_the_tv(self): + for case in ("damaged", "two names"): + with self.subTest(case=case): + self.setUp() + self.tv(disabled=[]) + self.apply_list() + path = self.record_file() + if case == "damaged": + path.write_text("{not json") + else: + # The name older versions gave a record on the network, next to the one it gets now. + shutil.copy2(path, path.with_name(path.name.replace("192.168.1.50-", + "192.168.1.50_5555-"))) + before = path.read_bytes() + # An update turned one listed package back on. + self.set_state(commands=[], disabled=["com.example.store"]) + result = self.run_check("--fix", str(self.list_path)) + self.assertEqual(result.returncode, 2, result.stdout + result.stderr) + self.assertIn(str(path), result.stderr) + self.assertEqual(self.sent(), [], "nothing reached the TV") + self.assertEqual(path.read_bytes(), before) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_drift_check.py b/tests/test_drift_check.py index 1369f9be..924a160f 100644 --- a/tests/test_drift_check.py +++ b/tests/test_drift_check.py @@ -1,8 +1,8 @@ """tools/check.py against a simulated TV (#359). The property worth testing is the one no TV shows on demand: what the drift report says after a -firmware update, and that --fix puts back only what drifted, without touching the recovery record -undo depends on. tools/tv.py is replaced by a fake that answers the queries from a JSON file, so the +firmware update, and that --fix puts back only what drifted, without writing a recovery record of +its own. tools/tv.py is replaced by a fake that answers the queries from a JSON file, so the real check.py logic runs and only the ADB transport is simulated. Run from the repository root: python3 -m unittest discover -s tests -v diff --git a/tests/test_list_header_commands.py b/tests/test_list_header_commands.py index 45e2bd88..e89f598e 100644 --- a/tests/test_list_header_commands.py +++ b/tests/test_list_header_commands.py @@ -9,8 +9,8 @@ replaced by a stand-in that keeps what `tools/tv.py --batch` was given. examples/anker-nebula-mars-3-air: the header and README said `tools/check.py --fix` re-disables Assistant after a boot, but the list keeps com.google.android.katniss commented out and check.py reads only active lines, so --fix never touched it. They now say to uncomment it and its LED service, then -run Apply again: check.py --fix never writes the recovery record, and restore.sh leaves alone every -listed name the record does not hold, so only disable.sh can make Undo bring them back. The owner's +run Apply again: disable.sh adds them to the recovery record first, and restore.sh leaves alone every +listed name the record does not hold, so that is what makes Undo bring them back. The owner's choice stays: the shipped list still leaves Assistant on. That half reuses the drift check's harness and fake tools/tv.py from tests/test_drift_check.py, and runs the real disable.sh and restore.sh against it with run_script from tests/test_recovery_record.py. @@ -167,12 +167,11 @@ class AnkerAssistantTest(unittest.TestCase): self.assertEqual(self.sent(), [f"pm disable-user --user 0 {name}" for name in ASSISTANT]) def test_undo_brings_assistant_back_after_the_header_steps(self): - # check.py --fix applies the list without touching the recovery record, and restore.sh leaves - # alone every listed name the record does not hold, so a package only check.py disabled stays - # off after Undo. disable.sh adds the names a list gained to the record before disabling them. + # restore.sh leaves alone every listed name the recovery record does not hold, and disable.sh + # adds the names a list gained to the record before disabling them. names = self.told_to_uncomment() told = self.paragraph.split("uncomment", 1)[1].split("check.py", 1)[0] - self.assertIn("run Apply again", told, "only disable.sh records the uncommented lines for Undo") + self.assertIn("run Apply again", told, "Apply records the uncommented lines for Undo") lines = header(ANKER / "debloat.txt") apply = next(line for line in lines if line.startswith("# Apply:")) undo = next(line for line in lines if line.startswith("# Undo:")) diff --git a/tools/check.py b/tools/check.py index b44bd08c..2519a44d 100644 --- a/tools/check.py +++ b/tools/check.py @@ -8,11 +8,13 @@ A firmware update re-enables listed packages, hands Home back to the stock launcher and adds new preloads, and nothing in this repo notices: nobody runs disable.sh again, and restore.sh would undo the cleanup instead of redoing it. This command compares the TV with the list and exits 1 when the -TV drifted, so it fits a cron job; --fix applies the list again without touching the recovery -record (the packages were already recorded before the first run, so undo still means undo). +TV drifted, so it fits a cron job; --fix applies the list again. It never writes a recovery record of +its own (a record of the TV after cleanup would make undo leave the whole list disabled), but when +this TV and list have one, it first adds what disable.sh would: the names the list gained since then, +and a package the record calls absent that is installed and enabled now. Undo then still means undo. -It reads the list, not the record: the record holds what the TV looked like *before* cleanup, which -is what restore needs, while the target here is the list itself. +It compares the TV with the list, not the record: the record holds what the TV looked like *before* +cleanup, which is what restore needs, while the target here is the list itself. --fix runs the same safety checks as disable.sh before it disables anything (the Wi-Fi network setupwraith owns, the packages under the HDMI/AV inputs), because a firmware update is exactly the @@ -24,7 +26,8 @@ Exit codes: 0 in sync (or fixed), 1 drift, an incomplete fix, or packages a safe --fix may be partial, and the next check says what is left), or the command line asked for something this script does not do, which includes naming a launcher the TV does not have. That last one matters: README.md tells an owner to run this from a weekly job and hear about it on exit 1, so a mistyped flag -must not come back as drift, nor a mistyped launcher as "nothing drifted". +must not come back as drift, nor a mistyped launcher as "nothing drifted". A recovery record --fix +cannot read or update is 2 as well, with nothing changed on the TV. """ import os import re @@ -347,7 +350,8 @@ def main(): return 2 fixing = False try: - installed, _, home, back_on, gone, home_drifted, expected = inspect(dev, listed, preferred) + installed, disabled, home, back_on, gone, home_drifted, expected = inspect(dev, listed, + preferred) if preferred and preferred not in installed: # The other half of the launcher check before the list was read: only the TV can say # whether the package is there. @@ -357,12 +361,27 @@ def main(): return 2 code = report(list_path, listed, home, back_on, gone, home_drifted, expected) if code and wanted: + # restore.sh puts back only what the recovery record holds, so before anything is disabled, + # a record this TV and list already have gets the entries a second disable.sh run would add + # (recovery.extend_record). Without a record nothing is written: one taken now would hold + # the TV after cleanup. A record that cannot be used stops the run before anything reaches + # the TV, as it stops disable.sh. + path = recovery.find_record(list_path, os.environ.get("TV_HOST", "")) + if path.exists(): + try: + recovery.extend_record(path, recovery.load_record(path), listed, installed, disabled) + except OSError as e: + sys.stderr.write(f"could not update the recovery record ({e}), so nothing was " + f"changed on the TV: without it, restore.sh would leave what " + f"--fix disables off.\n") + return 2 fixing = True code = fix(dev, listed, back_on, home_drifted, preferred) except SystemExit as e: # recovery.query_packages stops with its own message when the TV answers no package at all: - # the TV could not be read, so exit 2, not the 1 Python gives that message. Under --fix the - # exits keep their code, as above. + # the TV could not be read, so exit 2, not the 1 Python gives that message. find_record and + # load_record do the same for a recovery record --fix cannot use. Under --fix the exits keep + # their code, as above. if fixing: raise sys.stderr.write(f"{e}\n") diff --git a/tools/recovery.py b/tools/recovery.py index fdfe5c03..7a7f2928 100644 --- a/tools/recovery.py +++ b/tools/recovery.py @@ -11,8 +11,9 @@ before. Both scripts now go through this record: `disable.sh` writes it before t reaches the TV, and `restore.sh` replays it. Records live in ~/.cleanyourtv/, one per TV and list. The first one wins: a retry after a -half-applied list must not record the broken state as the original. A later run only adds the names -the list gained since then, with their state before this list disabled them. +half-applied list must not record the broken state as the original. A later run, and check.py --fix, +only add the names the list gained since then, with their state before this list disabled them, and +record a package that was absent then and is installed and enabled now as enabled. """ import hashlib import ipaddress @@ -185,7 +186,8 @@ def load_record(path): # would lose that: restore would then enable every listed package, and a new disable.sh run would # record the TV after cleanup as the original. So the chooser and that one entry are dropped here, # which reads the record the way save() writes it now. Any other bad name is still refused below, - # and save() writes the record back this way only when the list has gained names. + # and extend_record() (from save() and check.py --fix) writes the record back this way only when the + # list has gained names or a package recorded absent has since arrived. home = record.get("home") if isinstance(home, str) and HOME_RE.fullmatch(home) and is_chooser(home): record["packages"].pop(home.split("/", 1)[0], None) @@ -298,18 +300,50 @@ def package_state(name, installed, disabled): else "enabled") +def extend_record(path, record, names, installed, disabled): + """Bring an existing record up to date before the list is applied to the TV again. + + save() runs this for disable.sh, and check.py --fix runs it before it disables anything, because + restore puts back only what the record holds. The list may have been edited since the record was + taken. Entries already in the record are never overwritten (a retry must not record the broken + state as the original), but names it does not hold yet have not been touched by this list, so + their state now is their state before cleanup. So is the state of a package recorded as absent + that is installed and enabled now: an update or the Play Store put it there after the record was + taken. Without them, restore would leave those packages disabled for good. One that is installed + but disabled stays absent, as nobody can say what disabled it, and restore leaves both alone. + The file is written only when something changed. Returns the names added and the ones that + arrived. + """ + states = record["packages"] + arrived = [name for name in names if states.get(name) == "absent" + and package_state(name, installed, disabled) == "enabled"] + added = [name for name in names if name not in states] + for name in added: + states[name] = package_state(name, installed, disabled) + for name in arrived: + states[name] = "enabled" + if not added and not arrived: + return added, arrived + write_record(path, record) + if added: + print(f"added {len(added)} names the list gained since then to the recovery record: " + f"{', '.join(added)}") + if arrived: + print(f"recorded {len(arrived)} as enabled, absent when the record was taken and installed " + f"since: {', '.join(arrived)}") + print("every other entry keeps the state from before the first run.") + return added, arrived + + def save(list_path, host): path = find_record(list_path, host) if path.exists(): record = load_record(path) packages = read_list(list_path) - # The list may have been edited since the record was taken. Entries already in the record are - # never overwritten (a retry must not record the broken state as the original), but names it - # does not hold yet have not been touched by this list, so their state now is their state - # before cleanup. Without them, restore would leave those packages disabled for good. - added = [name for name in packages if name not in record["packages"]] print(f"keeping the recovery record from {record.get('created')}: {path}") - if not added: + # Only a name the record does not hold, or holds as absent, can change it (extend_record), and + # only then is the TV asked. + if all(record["packages"].get(name, "absent") != "absent" for name in packages): print("nothing was recorded again: it holds the state from before the first run.") return 0 dev = connect() @@ -317,11 +351,9 @@ def save(list_path, host): installed, disabled = query_packages(dev) finally: dev.close() - for name in added: - record["packages"][name] = package_state(name, installed, disabled) - write_record(path, record) - print(f"added {len(added)} names the list gained since then, entries already recorded " - f"are unchanged: {', '.join(added)}") + added, arrived = extend_record(path, record, packages, installed, disabled) + if not added and not arrived: + print("nothing was recorded again: it holds the state from before the first run.") return 0 packages = read_list(list_path) @@ -434,8 +466,9 @@ def restore_commands(list_path, stock, host): commands.append(f"settings put global {key} {value}") else: sys.stderr.write(f" skipping {key}: the record holds {value!r}, not a number\n") - # disable.sh adds names the list gained to the record before disabling them, so a name still - # missing here was added after the last disable run and never disabled by it: leave it alone. + # disable.sh and check.py --fix add names the list gained to the record before disabling them, so + # a name still missing here was added after the last of those runs and never disabled by it: + # leave it alone. edited = [name for name in packages if name not in states] if edited: sys.stderr.write(f"not in the record, leaving alone: {', '.join(edited)}\n") From 4842144976c1d168828aed2edb3663945509ee39 Mon Sep 17 00:00:00 2001 From: Mazin Kanuga <97324132+TechnoDudeX@users.noreply.github.com> Date: Sun, 25 Oct 2026 18:26:04 -0400 Subject: [PATCH 2/2] docs: install-existing brings a package back disabled when a disable.sh run already reached it The TROUBLESHOOTING entry on packages removed for user 0 said install-existing brings one back enabled, but a disable.sh run over a list that names it since the removal already set it disabled-user, which install-existing keeps; the entry now says so and gives pm enable --user 0 for keeping it, and a comment in the new test says a TV refuses pm disable-user for a package it has no copy of. Not tried on a TV. Co-Authored-By: Claude Opus 5.5 --- docs/TROUBLESHOOTING.md | 29 +++++++++++++++++------------ tests/test_check_fix_record.py | 2 +- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 6002d7cd..c96c3d5c 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -176,18 +176,23 @@ Things that bit us or contributors, so they don't bite you. Commands use `tools/ "not installed any more". Bring it back with `tools/tv.py 'cmd package install-existing --user 0 '` (AOSP's `cmd package` has it from Android 8.0; the Android 6 set in [#1185](https://github.com/Gathin23/cleanyourtv/issues/1185) had no - such command; no TV here has run it yet). It comes back enabled, and without its data unless the - uninstall used `-k`. To keep it, comment its line out of your list first: otherwise `check.py` reports - it as back on, and `--fix` or the next `disable.sh` run disables it again. If you don't want it, let - the list disable it afterwards, which the TV's own settings can undo. `--fix` or that `disable.sh` run - first changes it from absent to enabled in the record, so `restore.sh` turns it back on too. A record - an older version updated, or a package that was already disabled when that run saw it, still says - absent, and `restore.sh` leaves it alone: `pm enable --user 0 ` is its undo. Two cases - this does not cover. An app with no copy on the system partition that only user 0 had is deleted with - its APK: the command answers `Package doesn't exist`, and only reinstalling the APK brings - it back (the Toshiba's `com.hisense.business.ad`). And a `User 0:` line with - `installed=true hidden=true` was hidden, not uninstalled. install-existing changes nothing there even - though it prints `Package installed for user: 0`, and the shell cannot unhide it: + such command; no TV here has run it yet). It comes back without its data unless the uninstall used + `-k`, and disabled if a list that names it has gone through `disable.sh` since it was removed: that + run sent it `pm disable-user --user 0`, which AOSP applies to a package removed for user 0 too (the + [Shield list](../examples/nvidia-shield-tv-pro-2019/debloat.txt) notes `disable.sh` reporting + "disabled-user" for such packages), and install-existing marks it installed without changing that + state. `pm list packages -d --user 0 ` names it if it came back disabled. To keep it, comment + its line out of your list, then run `tools/tv.py 'pm enable --user 0 '`: with the line still + in, `check.py` reports it as back on, and `--fix` or the next `disable.sh` run disables it again. If + you don't want it, keep the line: the list disables it, or keeps it disabled, which the TV's own + settings can undo. `--fix` or a `disable.sh` run that finds it enabled first changes it from absent to + enabled in the record, so `restore.sh` turns it back on too. Until then, or in a record an older + version updated, it stays absent, and `restore.sh` leaves it alone: `pm enable --user 0 ` is + its undo. Two cases this does not cover. An app with no copy on the system partition that only user 0 + had is deleted with its APK: the command answers `Package doesn't exist`, and only + reinstalling the APK brings it back (the Toshiba's `com.hisense.business.ad`). And a `User 0:` line + with `installed=true hidden=true` was hidden, not uninstalled. install-existing changes nothing there + even though it prints `Package installed for user: 0`, and the shell cannot unhide it: `pm unhide` needs `MANAGE_USERS`, which the shell does not hold from Android 7.0. Unhide it from the device-owner app that hid it, or leave it, as the [Chromecast with Google TV 4K list](../examples/google-chromecast-gtv-4k/debloat.txt) does with diff --git a/tests/test_check_fix_record.py b/tests/test_check_fix_record.py index 2014858d..2201c74e 100644 --- a/tests/test_check_fix_record.py +++ b/tests/test_check_fix_record.py @@ -105,7 +105,7 @@ class CheckFixRecordTest(unittest.TestCase): self.tv(installed=drift.BASE, disabled=[STOCK]) self.apply_list() self.assertEqual(self.record()["com.example.later"], "absent") - # A TV refuses pm disable-user for a package it does not have, which this fake does not; then + # A TV refuses pm disable-user for a package it has no copy of, which this fake does not; then # an update installs it, enabled. state = self.state() self.set_state(installed=state["installed"] + ["com.example.later"],