Un servicio de accesibilidad es una app que mejora la interfaz de usuario para ayudar a los usuarios con discapacidades o que no pueden interactuar por completo con un dispositivo temporalmente. Estos servicios se ejecutan en segundo plano y se comunican con el sistema para inspeccionar el contenido de la pantalla y realizar interacciones con las apps en nombre del usuario. Algunos ejemplos son los lectores de pantalla (como TalkBack), las herramientas de Accesibilidad con interruptores y los sistemas de control por voz.
En esta guía, se explican los conceptos básicos para crear un servicio de accesibilidad de Android.
Ciclo de vida del servicio de accesibilidad
Para crear un servicio de accesibilidad, debes extender la clase AccessibilityService y declarar el servicio en el manifiesto de tu app.
Crea la clase de servicio
Crea una clase que extienda AccessibilityService. Debes anular los siguientes métodos:
onAccessibilityEvent: Se llama cuando el sistema detecta un evento que coincide con la configuración de tu servicio (por ejemplo, un cambio de enfoque o un clic en un botón). Aquí es donde tu servicio interpreta la interfaz de usuario.onInterrupt: Se llama cuando el sistema interrumpe la respuesta hablada de tu servicio (por ejemplo, para detener la respuesta hablada cuando el usuario mueve el foco rápidamente).
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 } }
Declaración en el manifiesto
Registra tu servicio en el archivo AndroidManifest.xml. Debes aplicar estrictamente el permiso BIND_ACCESSIBILITY_SERVICE para que solo el sistema pueda vincularse a tu servicio.
Para asegurarte de que el botón de configuración funcione, declara 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>
Configura el servicio
Crea un archivo de configuración en res/xml/accessibility_service_config.xml. Este archivo define qué eventos controla tu servicio y qué comentarios proporciona.
Asegúrate de hacer referencia a ServiceSettingsActivity que declaraste en tu manifiesto:
<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" />
El archivo de configuración incluye los siguientes atributos clave:
android:accessibilityEventTypes: Son los eventos que deseas recibir. UsatypeAllMaskpara un servicio de uso general.android:canRetrieveWindowContent: Debe sertruesi tu servicio necesita inspeccionar la jerarquía de la IU (por ejemplo, para leer texto de la pantalla).android:canPerformGestures: Debe sertruesi deseas enviar gestos (como deslizamientos o toques) de forma programática.android:accessibilityFlags: Combina marcas para habilitar funciones.flagRequestFingerprintGestureses obligatorio para los gestos con huella dactilar. Se requiereflagRequestAccessibilityButtonpara el botón de accesibilidad del software.
Para obtener una lista completa de las opciones de configuración, consulta AccessibilityServiceInfo.
Configuración del entorno de ejecución
Si bien la configuración XML es estática, también puedes modificar la configuración de tu servicio de forma dinámica durante el tiempo de ejecución. Esto es útil para activar o desactivar funciones según las preferencias del usuario.
Anula onServiceConnected() para aplicar actualizaciones del tiempo de ejecución con 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) }
Interpreta el contenido de la IU
Cuando se activa onAccessibilityEvent(), el sistema proporciona un AccessibilityEvent. Este evento actúa como punto de entrada al árbol de accesibilidad, una representación jerárquica del contenido de la pantalla.
Tu servicio interactúa principalmente con objetos AccessibilityNodeInfo, que representan elementos de la IU, como botones, listas y texto. Los datos sobre estos elementos de la IU se normalizan en AccessibilityNodeInfo.
En el siguiente ejemplo, se muestra cómo recuperar la fuente de un evento y recorrer el árbol de accesibilidad para encontrar información.
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úa en nombre de los usuarios
Los servicios de accesibilidad pueden realizar acciones, como hacer clic en botones o desplazarse por listas, en nombre del usuario.
Para realizar una acción, llama a performAction() en un objeto AccessibilityNodeInfo.
fun performClick(node: AccessibilityNodeInfo) { if (node.isClickable) { node.performAction(AccessibilityNodeInfo.ACTION_CLICK) } }
Para las acciones globales que afectan a todo el sistema (como presionar el botón Atrás o abrir el panel de notificaciones), usa performGlobalAction().
// Navigate back fun navigateBack() { performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK) }
Cómo administrar el enfoque
Android tiene dos tipos distintos de enfoque: el enfoque de entrada (a donde va la entrada del teclado) y el enfoque de accesibilidad (lo que inspecciona el servicio de accesibilidad).
En el siguiente fragmento, se muestra cómo encontrar el elemento que actualmente tiene el enfoque de accesibilidad:
// 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. }
En el siguiente fragmento, se muestra cómo mover el enfoque de accesibilidad a un elemento específico:
// Request that the system give focus to a given node fun focusNode(node: AccessibilityNodeInfo) { node.performAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) }
Cuando crees un servicio de accesibilidad, respeta el estado de enfoque del usuario y evita robar el enfoque, a menos que el usuario lo active de forma explícita.
Usar gestos
Tu servicio puede enviar gestos personalizados a la pantalla, como deslizamientos, toques o interacciones multitáctiles. Para ello, declara android:canPerformGestures="true" en tu configuración para que puedas usar la API de dispatchGesture().
Gestos simples
Para realizar gestos simples, comienza por crear un objeto Path que represente el movimiento asociado con un gesto determinado. Luego, envuelve el Path en un GestureDescription para describir el trazo. Por último, llama a dispatchGesture para enviar el gesto.
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) }
Gestos continuos
Para interacciones complejas (como dibujar una forma de L o realizar un arrastre preciso de varios pasos), puedes encadenar trazos con el parámetro willContinue.
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) }
Administración de audio
Cuando crees un servicio de accesibilidad (en especial, un lector de pantalla), usa la transmisión de audio STREAM_ACCESSIBILITY. Esto permite a los usuarios controlar el volumen del servicio independientemente del volumen multimedia del sistema.
fun increaseAccessibilityVolume() { val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager audioManager.adjustStreamVolume( AudioManager.STREAM_ACCESSIBILITY, AudioManager.ADJUST_RAISE, 0 ) }
Asegúrate de incluir la marca FLAG_ENABLE_ACCESSIBILITY_VOLUME en tu configuración, ya sea en XML o a través de setServiceInfo en el tiempo de ejecución.
Funciones avanzadas
Gestos del sensor de huellas digitales
En dispositivos que ejecutan Android 10 (nivel de API 29) o versiones posteriores, tu servicio puede capturar deslizamientos direccionales en el sensor de huellas dactilares. Esto es útil para proporcionar controles de navegación alternativos.
Agrega la siguiente lógica a tu método onServiceConnected():
// 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) } }
Botón de accesibilidad
En los dispositivos que usan teclas de navegación por software, los usuarios pueden invocar tu servicio a través de un botón de accesibilidad en la barra de navegación.
Para usar esta función, agrega la marca FLAG_REQUEST_ACCESSIBILITY_BUTTON a la configuración del servicio. Luego, agrega la lógica de registro a tu método onServiceConnected().
// 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 } } ) }
Texto a voz multilingüe
Un servicio que lee texto en voz alta puede cambiar de idioma automáticamente si el texto fuente está etiquetado con LocaleSpan. Esto permite que tu servicio pronuncie correctamente el contenido en varios idiomas sin necesidad de cambiar de idioma manualmente.
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 )
Cuando tu servicio procese AccessibilityNodeInfo, inspecciona la propiedad text de los objetos LocaleSpan para determinar el idioma correcto de texto a voz.
Recursos adicionales
Para obtener más información, consulta los siguientes recursos:
Guías
- Cómo compilar apps accesibles
- Accesibilidad en Jetpack Compose
- Guía rápida: Accesibilidad en Compose