An accessibility service is an app that enhances the user interface to assist users with disabilities or who might temporarily be unable to fully interact with a device. These services run in the background and communicate with the system to inspect screen content and interact with apps on the user's behalf. Examples include screen readers (like TalkBack), Switch Access tools, and voice control systems.
This guide covers the basics of building an Android accessibility service.
Accessibility service lifecycle
To create an accessibility service, you must extend the
AccessibilityService
class and declare the service in your app's manifest.
Create the service class
Create a class that extends AccessibilityService. You must override the
following methods:
onAccessibilityEvent: Called when the system detects an event that matches your service's configuration (for example, focus change or a button click). This is where your service interprets the user interface.onInterrupt: Called when the system interrupts your service's feedback (for example, to stop speech output when the user moves focus quickly).
package com.example.android.apis.accessibility import android.accessibilityservice.AccessibilityService import android.accessibilityservice.AccessibilityServiceInfo import android.accessibilityservice.FingerprintGestureController import android.accessibilityservice.AccessibilityButtonController import android.accessibilityservice.GestureDescription import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo import android.graphics.Path import android.os.Build import android.media.AudioManager import android.content.Context class MyAccessibilityService : AccessibilityService() { override fun onAccessibilityEvent(event: AccessibilityEvent) { // Interpret the event and provide feedback to the user } override fun onInterrupt() { // Interrupt any ongoing feedback } override fun onServiceConnected() { // Perform initialization here } }
Declare in the manifest
Register your service in the AndroidManifest.xml file. You must strictly
enforce the
BIND_ACCESSIBILITY_SERVICE
permission so that only the system can bind to your service.
To make sure the settings button works, declare the ServiceSettingsActivity.
<application> <service android:name=".accessibility.MyAccessibilityService" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" android:exported="true" android:label="@string/accessibility_service_label"> <intent-filter> <action android:name="android.accessibilityservice.AccessibilityService" /> </intent-filter> <meta-data android:name="android.accessibilityservice" android:resource="@xml/accessibility_service_config" /> </service> <activity android:name=".accessibility.ServiceSettingsActivity" android:exported="true" android:label="@string/accessibility_service_settings_label" /> </application>
Configure the service
Create a configuration file in res/xml/accessibility_service_config.xml. This
file defines what events your service handles and what feedback it provides.
Make sure to reference the ServiceSettingsActivity that you declared in your
manifest:
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:description="@string/accessibility_service_description" android:accessibilityEventTypes="typeAllMask" android:accessibilityFlags="flagDefault|flagRequestFingerprintGestures|flagRequestAccessibilityButton" android:accessibilityFeedbackType="feedbackSpoken" android:notificationTimeout="100" android:canRetrieveWindowContent="true" android:canPerformGestures="true" android:settingsActivity="com.example.android.apis.accessibility.ServiceSettingsActivity" />
The configuration file includes the following key attributes:
android:accessibilityEventTypes: The events you want to receive. UsetypeAllMaskfor a general-purpose service.android:canRetrieveWindowContent: Must betrueif your service needs to inspect the UI hierarchy (for example, to read text from the screen).android:canPerformGestures: Must betrueif you intend to dispatch gestures (like swipes or taps) programmatically.android:accessibilityFlags: Combine flags to enable features.flagRequestFingerprintGesturesis required for fingerprint gestures.flagRequestAccessibilityButtonis required for the software accessibility button.
For a full list of configuration options, see
AccessibilityServiceInfo.
Runtime configuration
While XML configuration is static, you can also modify your service configuration dynamically at runtime. This is useful for toggling features based on user preferences.
Override onServiceConnected() to apply runtime updates using
setServiceInfo():
override fun onServiceConnected() { val info = AccessibilityServiceInfo() // Set the type of events that this service wants to listen to. info.eventTypes = AccessibilityEvent.TYPE_VIEW_CLICKED or AccessibilityEvent.TYPE_VIEW_FOCUSED // Set the type of feedback your service provides. info.feedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN // Set flags at runtime. info.flags = AccessibilityServiceInfo.FLAG_DEFAULT or AccessibilityServiceInfo.FLAG_REQUEST_FINGERPRINT_GESTURES this.setServiceInfo(info) }
Interpret UI content
When onAccessibilityEvent() triggers, the system provides an
AccessibilityEvent. This event acts as the entry point to the
accessibility tree, a hierarchical representation of the screen content.
Your service interacts primarily with
AccessibilityNodeInfo
objects, which represent UI elements like buttons, lists, and text. Data about
these UI elements is normalized into AccessibilityNodeInfo.
The following example shows how to retrieve the source of an event and traverse the accessibility tree to find information.
override fun onAccessibilityEvent(event: AccessibilityEvent) { // Get the source node of the event val sourceNode: AccessibilityNodeInfo? = event.source if (sourceNode == null) return // Inspect properties if (sourceNode.isCheckable) { val state = if (sourceNode.isChecked) "Checked" else "Unchecked" val label = sourceNode.text ?: sourceNode.contentDescription // Provide feedback (for example, speak to the user) speakToUser("$label is $state") } // Always recycle nodes to prevent memory leaks sourceNode.recycle() } private fun speakToUser(text: String) { // Your text-to-speech implementation goes here }
Act on behalf of users
Accessibility services can perform actions, such as clicking buttons or scrolling lists, on behalf of the user.
To perform an action, call performAction() on an AccessibilityNodeInfo
object.
fun performClick(node: AccessibilityNodeInfo) { if (node.isClickable) { node.performAction(AccessibilityNodeInfo.ACTION_CLICK) } }
For global actions that affect the entire system (like pressing the Back button
or opening the notification shade), use performGlobalAction().
// Navigate back fun navigateBack() { performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK) }
Manage focus
Android has two distinct types of focus: input focus (where keyboard input goes) and accessibility focus (what the accessibility service is inspecting).
The following snippet shows how to find the element that currently has accessibility focus:
// Find the node that currently has accessibility focus // Note: rootInActiveWindow can be null if the window is not available val root = rootInActiveWindow if (root != null) { val focusedNode = root.findFocus(AccessibilityNodeInfo.FOCUS_ACCESSIBILITY) // Do something with focusedNode // Always recycle nodes focusedNode?.recycle() // rootInActiveWindow doesn't need to be recycled, but obtained nodes do. }
The following snippet shows how to move accessibility focus to a specific element:
// Request that the system give focus to a given node fun focusNode(node: AccessibilityNodeInfo) { node.performAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) }
When creating an accessibility service, respect the user's focus state and avoid stealing focus unless explicitly triggered by a user action.
Perform gestures
Your service can dispatch custom gestures to the screen, such as swipes, taps,
or multi-touch interactions. To do this, declare
android:canPerformGestures="true" in your configuration so that you can use
the dispatchGesture() API.
Simple gestures
To perform simple gestures, start by creating a Path object to represent the
movement associated with a given gesture. Then, wrap the Path in a
GestureDescription to describe the stroke. Finally, call dispatchGesture
to dispatch the gesture.
fun swipeRight() { // Create a path for the swipe (from x=100 to x=500) val swipePath = Path() swipePath.moveTo(100f, 500f) swipePath.lineTo(500f, 500f) // Build the stroke description (0ms delay, 500ms duration) val stroke = GestureDescription.StrokeDescription(swipePath, 0, 500) // Build the gesture description val gestureBuilder = GestureDescription.Builder() gestureBuilder.addStroke(stroke) // Dispatch the gesture dispatchGesture(gestureBuilder.build(), object : AccessibilityService.GestureResultCallback() { override fun onCompleted(gestureDescription: GestureDescription?) { super.onCompleted(gestureDescription) // Gesture finished successfully } }, null) }
Continued gestures
For complex interactions (like drawing an L shape or performing a precise
multi-step drag), you can chain strokes together using the willContinue
parameter.
fun performLShapedGesture() { val path1 = Path().apply { moveTo(200f, 200f) lineTo(400f, 200f) } val path2 = Path().apply { moveTo(400f, 200f) lineTo(400f, 400f) } // First stroke: willContinue = true val stroke1 = GestureDescription.StrokeDescription(path1, 0, 500, true) // Second stroke: continues immediately after stroke1 val stroke2 = stroke1.continueStroke(path2, 0, 500, false) val builder = GestureDescription.Builder() builder.addStroke(stroke1) builder.addStroke(stroke2) dispatchGesture(builder.build(), null, null) }
Audio management
When creating an accessibility service (especially a screen reader), use the
STREAM_ACCESSIBILITY audio stream. This lets users control the service volume
independently from the system media volume.
fun increaseAccessibilityVolume() { val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager audioManager.adjustStreamVolume( AudioManager.STREAM_ACCESSIBILITY, AudioManager.ADJUST_RAISE, 0 ) }
Be sure to include the FLAG_ENABLE_ACCESSIBILITY_VOLUME flag in your
configuration, either in XML or through setServiceInfo at runtime.
Advanced features
Fingerprint gestures
On devices running Android 10 (API level 29) or higher, your service can capture directional swipes on the fingerprint sensor. This is useful for providing alternative navigation controls.
Add the following logic to your onServiceConnected() method:
// Import: android.os.Build // Import: android.accessibilityservice.FingerprintGestureController private var gestureController: FingerprintGestureController? = null override fun onServiceConnected() { // Check if the device is running Android 10 (Q) or higher if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { gestureController = fingerprintGestureController val callback = object : FingerprintGestureController.FingerprintGestureCallback() { override fun onGestureDetected(gesture: Int) { when (gesture) { FingerprintGestureController.FINGERPRINT_GESTURE_SWIPE_DOWN -> { // Handle swipe down } FingerprintGestureController.FINGERPRINT_GESTURE_SWIPE_UP -> { // Handle swipe up } } } } gestureController?.registerFingerprintGestureCallback(callback, null) } }
Accessibility button
On devices using software navigation keys, users can invoke your service through an accessibility button in the navigation bar.
To use this feature, add the FLAG_REQUEST_ACCESSIBILITY_BUTTON flag to your
service configuration. Then add the registration logic to your
onServiceConnected() method.
// Import: android.accessibilityservice.AccessibilityButtonController override fun onServiceConnected() { // ... existing initialization code ... val controller = accessibilityButtonController controller.registerAccessibilityButtonCallback( object : AccessibilityButtonController.AccessibilityButtonCallback() { override fun onClicked(controller: AccessibilityButtonController) { // Respond to button tap } } ) }
Multilingual text-to-speech
A service that reads text aloud can automatically switch languages if the source
text is tagged with LocaleSpan. This lets your service correctly pronounce
mixed-language content without manual switching.
import android.text.Spannable import android.text.SpannableStringBuilder import android.text.style.LocaleSpan import java.util.Locale // Wrap text in LocaleSpan to indicate language val spannable = SpannableStringBuilder("Bonjour") spannable.setSpan( LocaleSpan(Locale.FRANCE), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE )
When your service processes AccessibilityNodeInfo, inspect the text property
for LocaleSpan objects to determine the correct text-to-speech language.
Additional resources
To learn more, see the following resources: