diff --git a/artemis/runtime/helper_manager.py b/artemis/runtime/helper_manager.py index 5830ad267..cce45cf2a 100644 --- a/artemis/runtime/helper_manager.py +++ b/artemis/runtime/helper_manager.py @@ -86,6 +86,23 @@ PACKAGE_NAME = "com.artemis.helper" SERVICE_NAME = f"{PACKAGE_NAME}/.ArtemisAccessibilityService" + + +def _is_helper_service(entry: str) -> bool: + """Whether one ``enabled_accessibility_services`` entry names the helper service. + + The same component has two spellings: adb writes the short form + (``com.artemis.helper/.ArtemisAccessibilityService``) while the Settings UI + writes the fully-qualified one (``com.artemis.helper/com.artemis.helper.Artemis…``). + ROMs that reject the adb write (e.g. ColorOS/realme) leave only the + Settings-UI spelling, so compare the normalized component, not the string. + """ + pkg, _, cls = entry.strip().partition("/") + if cls.startswith("."): + cls = pkg + cls + return pkg == PACKAGE_NAME and cls == f"{PACKAGE_NAME}.ArtemisAccessibilityService" + + TOKEN_RECEIVER = f"{PACKAGE_NAME}/.TokenReceiver" TOKEN_ACTION = f"{PACKAGE_NAME}.SET_TOKEN" #: Loopback port the service binds on the device. Never used as a host port. @@ -408,7 +425,11 @@ def is_service_enabled(self, serial: str) -> bool: serial, "shell", "settings", "get", "secure", "enabled_accessibility_services" ) enabled = (result.stdout or "").strip() - return SERVICE_NAME in enabled.split(":") if enabled and enabled != "null" else False + return ( + any(_is_helper_service(s) for s in enabled.split(":")) + if enabled and enabled != "null" + else False + ) def existing_forward(self, serial: str) -> int | None: """Host port of an adb forward that already targets the helper on ``serial``.""" @@ -602,7 +623,7 @@ def _enable_service(self, serial: str) -> bool: ) current = (result.stdout or "").strip() services = [s for s in current.split(":") if s and s != "null"] if current else [] - if SERVICE_NAME in services: + if any(_is_helper_service(s) for s in services): # Already enabled (the common, up-to-date case): no settle wait. self._adb( serial, "shell", "settings", "put", "secure", "accessibility_enabled", "1" @@ -640,7 +661,7 @@ def _revive_service(self, serial: str) -> None: serial, "shell", "settings", "get", "secure", "enabled_accessibility_services" ) current = (result.stdout or "").strip() - others = [s for s in current.split(":") if s and s != "null" and s != SERVICE_NAME] + others = [s for s in current.split(":") if s and s != "null" and not _is_helper_service(s)] self._adb( serial, "shell", @@ -793,7 +814,9 @@ def uninstall(self, serial: str) -> bool: serial, "shell", "settings", "get", "secure", "enabled_accessibility_services" ) current = (result.stdout or "").strip() - services = [s for s in current.split(":") if s and s != "null" and s != SERVICE_NAME] + services = [ + s for s in current.split(":") if s and s != "null" and not _is_helper_service(s) + ] self._adb( serial, "shell", diff --git a/tests/unit/runtime/test_helper_manager.py b/tests/unit/runtime/test_helper_manager.py index 6acc976fb..a6d2e497d 100644 --- a/tests/unit/runtime/test_helper_manager.py +++ b/tests/unit/runtime/test_helper_manager.py @@ -467,6 +467,34 @@ def test_uninstall_disables_service_and_removes_package(env): assert adb.forwards == [] +# The Settings UI stores the fully-qualified component; adb stores the short form. +# ROMs that reject the adb secure-settings write (ColorOS/realme) only ever have the former. +QUALIFIED_SERVICE_NAME = "com.artemis.helper/com.artemis.helper.ArtemisAccessibilityService" + + +def test_service_enabled_through_settings_ui_is_recognised(env): + adb, _, manager = env + adb.installed[SERIAL] = 2 + adb.enabled_services[SERIAL] = f"com.android.talkback/.TalkBackService:{QUALIFIED_SERVICE_NAME}" + + assert manager.is_service_enabled(SERIAL) + result = manager.provision(SERIAL) + assert result.ok + puts = _calls( + adb, ["-s", SERIAL, "shell", "settings", "put", "secure", "enabled_accessibility_services"] + ) + assert puts == [] # already enabled: the setting is left exactly as the user wrote it + + +def test_uninstall_removes_service_enabled_through_settings_ui(env): + adb, _, manager = env + adb.installed[SERIAL] = 2 + adb.enabled_services[SERIAL] = f"com.android.talkback/.TalkBackService:{QUALIFIED_SERVICE_NAME}" + + assert manager.uninstall(SERIAL) + assert adb.enabled_services[SERIAL] == "com.android.talkback/.TalkBackService" + + def test_transport_id_parsing_handles_missing_device(): adb = FakeAdb() manager = AccessibilityHelperManager(run_adb=adb, ping=lambda _p: None, bundled=None)