Show which accessibility services are enabled

Parse both installed and enabled accessibility services, adding an
"enabled" field to each result. This lets users see at a glance
whether any installed accessibility service is actually active.

Fixes #744
This commit is contained in:
Janik Besendorf
2026-04-12 10:16:49 +02:00
parent f26303c930
commit 9bcbaac3a2
4 changed files with 110 additions and 17 deletions
@@ -22,13 +22,13 @@ class DumpsysAccessibilityArtifact(AndroidArtifact):
def parse(self, content: str) -> None: def parse(self, content: str) -> None:
""" """
Parse the Dumpsys Accessibility section/ Parse the Dumpsys Accessibility section.
Adds results to self.results (List[Dict[str, str]]) Adds results to self.results (List[Dict[str, Any]])
:param content: content of the accessibility section (string) :param content: content of the accessibility section (string)
""" """
# "Old" syntax # Parse installed services
in_services = False in_services = False
for line in content.splitlines(): for line in content.splitlines():
if line.strip().startswith("installed services:"): if line.strip().startswith("installed services:"):
@@ -39,7 +39,6 @@ class DumpsysAccessibilityArtifact(AndroidArtifact):
continue continue
if line.strip() == "}": if line.strip() == "}":
# At end of installed services
break break
service = line.split(":")[1].strip() service = line.split(":")[1].strip()
@@ -48,21 +47,66 @@ class DumpsysAccessibilityArtifact(AndroidArtifact):
{ {
"package_name": service.split("/")[0], "package_name": service.split("/")[0],
"service": service, "service": service,
"enabled": False,
} }
) )
# "New" syntax - AOSP >= 14 (?) # Parse enabled services from both old and new formats.
# Looks like: #
# Enabled services:{{com.azure.authenticator/com.microsoft.brooklyn.module.accessibility.BrooklynAccessibilityService}, {com.agilebits.onepassword/com.agilebits.onepassword.filling.accessibility.FillingAccessibilityService}} # Old format (multi-line block):
# enabled services: {
# 0 : com.example/.MyService
# }
#
# New format (single line, AOSP >= 14):
# Enabled services:{{com.example/com.example.MyService}, {com.other/com.other.Svc}}
enabled_services = set()
in_enabled = False
for line in content.splitlines(): for line in content.splitlines():
if line.strip().startswith("Enabled services:"): stripped = line.strip()
matches = re.finditer(r"{([^{]+?)}", line)
if in_enabled:
if stripped == "}":
in_enabled = False
continue
service = line.split(":")[1].strip()
enabled_services.add(service)
continue
if re.match(r"enabled services:\s*\{\s*$", stripped, re.IGNORECASE):
# Old multi-line format: "enabled services: {"
in_enabled = True
continue
if re.match(r"enabled services:\s*\{", stripped, re.IGNORECASE):
# New single-line format: "Enabled services:{{pkg/svc}, {pkg2/svc2}}"
matches = re.finditer(r"\{([^{}]+)\}", stripped)
for match in matches: for match in matches:
# Each match is in format: <package_name>/<service> enabled_services.add(match.group(1).strip())
package_name, _, service = match.group(1).partition("/")
self.results.append( # Mark installed services that are enabled.
{"package_name": package_name, "service": service} # Installed service names may include trailing annotations like
) # "(A11yTool)" that are absent from the enabled services list,
# so strip annotations before comparing.
def _strip_annotation(s: str) -> str:
return re.sub(r"\s+\(.*?\)\s*$", "", s)
installed_stripped = {
_strip_annotation(r["service"]): r for r in self.results
}
for enabled in enabled_services:
if enabled in installed_stripped:
installed_stripped[enabled]["enabled"] = True
# Add enabled services not found in the installed list
for service in enabled_services:
if service not in installed_stripped:
package_name, _, _ = service.partition("/")
self.results.append(
{
"package_name": package_name,
"service": service,
"enabled": True,
}
)
@@ -49,9 +49,14 @@ class DumpsysAccessibility(DumpsysAccessibilityArtifact, BugReportModule):
for result in self.results: for result in self.results:
self.log.info( self.log.info(
'Found installed accessibility service "%s"', result.get("service") 'Found installed accessibility service "%s" (enabled: %s)',
result.get("service"),
result.get("enabled", False),
) )
enabled_count = sum(1 for r in self.results if r.get("enabled"))
self.log.info( self.log.info(
"Identified a total of %d accessibility services", len(self.results) "Identified a total of %d accessibility services, %d enabled",
len(self.results),
enabled_count,
) )
@@ -25,6 +25,9 @@ class TestDumpsysAccessibilityArtifact:
da.results[0]["service"] da.results[0]["service"]
== "com.android.settings/com.samsung.android.settings.development.gpuwatch.GPUWatchInterceptor" == "com.android.settings/com.samsung.android.settings.development.gpuwatch.GPUWatchInterceptor"
) )
# All services are installed but none enabled in this fixture
for result in da.results:
assert result["enabled"] is False
def test_parsing_v14_aosp_format(self): def test_parsing_v14_aosp_format(self):
da = DumpsysAccessibilityArtifact() da = DumpsysAccessibilityArtifact()
@@ -36,7 +39,32 @@ class TestDumpsysAccessibilityArtifact:
da.parse(data) da.parse(data)
assert len(da.results) == 1 assert len(da.results) == 1
assert da.results[0]["package_name"] == "com.malware.accessibility" assert da.results[0]["package_name"] == "com.malware.accessibility"
assert da.results[0]["service"] == "com.malware.service.malwareservice" assert (
da.results[0]["service"]
== "com.malware.accessibility/com.malware.service.malwareservice"
)
assert da.results[0]["enabled"] is True
def test_parsing_installed_and_enabled(self):
da = DumpsysAccessibilityArtifact()
file = get_artifact("android_data/dumpsys_accessibility_enabled.txt")
with open(file) as f:
data = f.read()
assert len(da.results) == 0
da.parse(data)
assert len(da.results) == 5
enabled = [r for r in da.results if r["enabled"]]
assert len(enabled) == 1
assert enabled[0]["package_name"] == "com.samsung.accessibility"
assert (
enabled[0]["service"]
== "com.samsung.accessibility/.universalswitch.UniversalSwitchService (A11yTool)"
)
not_enabled = [r for r in da.results if not r["enabled"]]
assert len(not_enabled) == 4
def test_ioc_check(self, indicator_file): def test_ioc_check(self, indicator_file):
da = DumpsysAccessibilityArtifact() da = DumpsysAccessibilityArtifact()
@@ -0,0 +1,16 @@
ACCESSIBILITY MANAGER (dumpsys accessibility)
currentUserId=0
User state[
attributes:{id=0, touchExplorationEnabled=false, installedServiceCount=5}
installed services: {
0 : com.google.android.apps.accessibility.voiceaccess/.JustSpeakService (A11yTool)
1 : com.microsoft.appmanager/com.microsoft.mmx.screenmirroringsrc.accessibility.ScreenMirroringAccessibilityService
2 : com.samsung.accessibility/.assistantmenu.serviceframework.AssistantMenuService (A11yTool)
3 : com.samsung.accessibility/.universalswitch.UniversalSwitchService (A11yTool)
4 : com.samsung.android.accessibility.talkback/com.samsung.android.marvin.talkback.TalkBackService (A11yTool)
}
Bound services:{}
Enabled services:{{com.samsung.accessibility/.universalswitch.UniversalSwitchService}}
Binding services:{}
Crashed services:{}