Cómo crear un servicio de accesibilidad personalizado

Un servicio de accesibilidad es una app que mejora la interfaz de usuario para ayudar a los usuarios con discapacidades o que temporalmente no pueden interactuar por completo con un dispositivo. Por ejemplo, es posible que los usuarios que conducen, cuidan a un niño pequeño o asisten a una fiesta muy ruidosa necesiten comentarios adicionales o alternativos sobre la interfaz.

Android ofrece servicios de accesibilidad estándar, como TalkBack, y los desarrolladores pueden crear y distribuir sus propios servicios. En este documento, se explican los conceptos básicos de la compilación de un servicio de accesibilidad.

Un servicio de accesibilidad se puede incluir en un paquete con una app normal o crearse como un proyecto de Android independiente. Los pasos para crear el servicio son los mismos en cualquier caso.

Crea tu servicio de accesibilidad

Dentro de tu proyecto, crea una clase que extienda AccessibilityService:

Kotlin

package com.example.android.apis.accessibility

import android.accessibilityservice.AccessibilityService
import android.view.accessibility.AccessibilityEvent

class MyAccessibilityService : AccessibilityService() {
...
    override fun onInterrupt() {}

    override fun onAccessibilityEvent(event: AccessibilityEvent?) {}
...
}

Java

package com.example.android.apis.accessibility;

import android.accessibilityservice.AccessibilityService;
import android.view.accessibility.AccessibilityEvent;

public class MyAccessibilityService extends AccessibilityService {
...
    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
    }

    @Override
    public void onInterrupt() {
    }

...
}

Si creas un proyecto nuevo para este Service y no planeas tener una app asociada, puedes quitar la clase Activity de inicio de la fuente.

Permisos y declaraciones de manifiesto

Las apps que brindan servicios de accesibilidad deben incluir declaraciones específicas en sus manifiestos para que el sistema Android las considere servicios de accesibilidad. En esta sección, se explica la configuración obligatoria y opcional para los servicios de accesibilidad.

Declaración del servicio de accesibilidad

Para que tu app se considere un servicio de accesibilidad, incluye un elemento service (en lugar del elemento activity) dentro del elemento application de tu manifiesto. Además, dentro del elemento service, incluye un filtro de intents del servicio de accesibilidad. El manifiesto también debe proteger el servicio agregando el permiso BIND_ACCESSIBILITY_SERVICE para garantizar que solo el sistema pueda vincularse a él. Por ejemplo:

  <application>
    <service android:name=".MyAccessibilityService"
        android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
        android:label="@string/accessibility_service_label">
      <intent-filter>
        <action android:name="android.accessibilityservice.AccessibilityService" />
      </intent-filter>
    </service>
  </application>

Configuración del servicio de accesibilidad

Los servicios de accesibilidad deben proporcionar una configuración que especifique los tipos de eventos de accesibilidad que controla el servicio, así como información adicional sobre el servicio. La configuración de un servicio de accesibilidad está incluida en la clase AccessibilityServiceInfo. Tu servicio puede compilar y establecer una configuración mediante una instancia de esta clase y setServiceInfo() en el entorno de ejecución. Sin embargo, no todas las opciones de configuración están disponibles con este método.

Puedes incluir un elemento <meta-data> en tu manifiesto con una referencia a un archivo de configuración, que te permite establecer todo el rango de opciones para tu servicio de accesibilidad, como se muestra en el siguiente ejemplo:

<service android:name=".MyAccessibilityService">
  ...
  <meta-data
    android:name="android.accessibilityservice"
    android:resource="@xml/accessibility_service_config" />
</service>

Este elemento <meta-data> hace referencia a un archivo en formato XML que creas en el directorio de recursos de tu app: <project_dir>/res/xml/accessibility_service_config.xml>. El siguiente código muestra un ejemplo del contenido del archivo de configuración de servicio:

<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/accessibility_service_description"
    android:packageNames="com.example.android.apis"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFlags="flagDefault"
    android:accessibilityFeedbackType="feedbackSpoken"
    android:notificationTimeout="100"
    android:canRetrieveWindowContent="true"
    android:settingsActivity="com.example.android.accessibility.ServiceSettingsActivity"
/>

Para obtener más información sobre los atributos XML que se pueden usar en el archivo de configuración del servicio de accesibilidad, consulta la siguiente documentación de referencia:

Para obtener más información sobre qué parámetros de configuración se pueden establecer de forma dinámica en el tiempo de ejecución, consulta la documentación de referencia de AccessibilityServiceInfo.

Configura tu servicio de accesibilidad

Cuando configures las variables de configuración de tu servicio de accesibilidad, ten en cuenta lo siguiente para indicarle al sistema cómo y cuándo ejecutarlo:

  • ¿A qué tipos de eventos quieres que responda?
  • ¿El servicio debe estar activo para todas las apps o solo para nombres de paquetes específicos?
  • ¿Qué tipos de comentarios usa?

Tienes dos opciones para configurar estas variables. La opción retrocompatible es establecerlas en el código mediante setServiceInfo(android.accessibilityservice.AccessibilityServiceInfo). Para ello, anula el método onServiceConnected() y configura tu servicio allí, como se muestra en el siguiente ejemplo:

Kotlin

override fun onServiceConnected() {
    info.apply {
        // Set the type of events that this service wants to listen to. Others
        // aren't passed to this service.
        eventTypes = AccessibilityEvent.TYPE_VIEW_CLICKED or AccessibilityEvent.TYPE_VIEW_FOCUSED

        // If you only want this service to work with specific apps, set their
        // package names here. Otherwise, when the service is activated, it
        // listens to events from all apps.
        packageNames = arrayOf("com.example.android.myFirstApp", "com.example.android.mySecondApp")

        // Set the type of feedback your service provides.
        feedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN

        // Default services are invoked only if no package-specific services are
        // present for the type of AccessibilityEvent generated. This service is
        // app-specific, so the flag isn't necessary. For a general-purpose
        // service, consider setting the DEFAULT flag.

        // flags = AccessibilityServiceInfo.DEFAULT;

        notificationTimeout = 100
    }

    this.serviceInfo = info

}

Java

@Override
public void onServiceConnected() {
    // Set the type of events that this service wants to listen to. Others
    // aren't passed to this service.
    info.eventTypes = AccessibilityEvent.TYPE_VIEW_CLICKED |
            AccessibilityEvent.TYPE_VIEW_FOCUSED;

    // If you only want this service to work with specific apps, set their
    // package names here. Otherwise, when the service is activated, it listens
    // to events from all apps.
    info.packageNames = new String[]
            {"com.example.android.myFirstApp", "com.example.android.mySecondApp"};

    // Set the type of feedback your service provides.
    info.feedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN;

    // Default services are invoked only if no package-specific services are
    // present for the type of AccessibilityEvent generated. This service is
    // app-specific, so the flag isn't necessary. For a general-purpose service,
    // consider setting the DEFAULT flag.

    // info.flags = AccessibilityServiceInfo.DEFAULT;

    info.notificationTimeout = 100;

    this.setServiceInfo(info);

}

La segunda opción consiste en configurar el servicio usando un archivo XML. Algunas opciones de configuración, como canRetrieveWindowContent, solo están disponibles si configuras tu servicio con XML. Las opciones de configuración del ejemplo anterior se ven de la siguiente manera cuando se definen con XML:

<accessibility-service
     android:accessibilityEventTypes="typeViewClicked|typeViewFocused"
     android:packageNames="com.example.android.myFirstApp, com.example.android.mySecondApp"
     android:accessibilityFeedbackType="feedbackSpoken"
     android:notificationTimeout="100"
     android:settingsActivity="com.example.android.apis.accessibility.TestBackActivity"
     android:canRetrieveWindowContent="true"
/>

Si usas XML, haz referencia a él en tu manifiesto agregando una etiqueta <meta-data> a la declaración del servicio que dirija al archivo en formato XML. Si almacenas tu archivo en formato XML en res/xml/serviceconfig.xml, la etiqueta nueva se verá de la siguiente manera:

<service android:name=".MyAccessibilityService">
     <intent-filter>
         <action android:name="android.accessibilityservice.AccessibilityService" />
     </intent-filter>
     <meta-data android:name="android.accessibilityservice"
     android:resource="@xml/serviceconfig" />
</service>

Métodos del servicio de accesibilidad

Un servicio de accesibilidad debe extender la clase AccessibilityService y anular los siguientes métodos de esa clase. Estos métodos se presentan en el orden en que el sistema Android los llama: desde el momento en que se inicia el servicio (onServiceConnected()), mientras se ejecuta (onAccessibilityEvent(), onInterrupt()) y hasta cuando se cierra (onUnbind()).

  • onServiceConnected(): El sistema llama a este método cuando se conecta a tu servicio de accesibilidad (opcional). Usa este método para realizar pasos de configuración únicos en tu servicio, como conectarte a los servicios del sistema de comentarios del usuario, como el administrador de audio o el vibrador del dispositivo. Si deseas establecer la configuración del servicio en el tiempo de ejecución o realizar ajustes únicos, esta es una ubicación conveniente para llamar a setServiceInfo().

  • onAccessibilityEvent(): (obligatorio) El sistema llama a este método cuando detecta un AccessibilityEvent que coincide con los parámetros de filtrado de eventos especificados por tu servicio de accesibilidad, como cuando el usuario presiona un botón o se enfoca en un control de la interfaz de usuario en una app para la que tu servicio de accesibilidad proporciona comentarios. Cuando el sistema llama a este método, pasa el AccessibilityEvent asociado, que el servicio puede interpretar y usar para proporcionar comentarios al usuario. Se puede llamar a este método muchas veces a lo largo del ciclo de vida de tu servicio.

  • onInterrupt(): (obligatorio) El sistema llama a este método cuando quiere interrumpir los comentarios que proporciona tu servicio, por lo general, en respuesta a una acción del usuario, como mover el enfoque a un control diferente. Se puede llamar a este método muchas veces a lo largo del ciclo de vida de tu servicio.

  • onUnbind(): El sistema llama a este método cuando está a punto de cerrar el servicio de accesibilidad (opcional). Usa este método para realizar cualquier procedimiento de cierre único, incluida la desasignación de servicios del sistema de comentarios del usuario, por ejemplo, el administrador de audio o el vibrador del dispositivo.

Estos métodos de devolución de llamada proporcionan la estructura básica de tu servicio de accesibilidad. Puedes decidir cómo procesar los datos que proporciona el sistema Android en forma de objetos AccessibilityEvent y enviar comentarios a los usuarios. Para conocer más detalles sobre cómo obtener información de un evento de accesibilidad, consulta Cómo obtener detalles del evento.

Regístrate para ver los eventos de accesibilidad

Una de las funciones más importantes de los parámetros de configuración del servicio de accesibilidad es permitirte especificar qué tipos de eventos de accesibilidad puede controlar tu servicio. Especificar esta información permite que los servicios de accesibilidad cooperan entre sí y te da la flexibilidad de controlar solo tipos de eventos específicos de apps específicas. El filtrado de eventos puede incluir los siguientes criterios:

  • Nombres de paquetes: Especifica los nombres de paquetes de las apps cuyos eventos de accesibilidad quieres que controle tu servicio. Si omites este parámetro, tu servicio de accesibilidad se considera disponible para entregar eventos de accesibilidad de cualquier app. Puedes configurar este parámetro en los archivos de configuración del servicio de accesibilidad con el atributo android:packageNames como una lista separada por comas o usar el miembro AccessibilityServiceInfo.packageNames.

  • Tipos de eventos: Especifica los tipos de eventos de accesibilidad que deseas que controle tu servicio. Puedes establecer este parámetro en los archivos de configuración del servicio de accesibilidad con el atributo android:accessibilityEventTypes como una lista separada por el carácter |, por ejemplo, accessibilityEventTypes="typeViewClicked|typeViewFocused". También puedes configurarla con el miembro AccessibilityServiceInfo.eventTypes.

Cuando configures tu servicio de accesibilidad, considera con atención qué eventos puede controlar tu servicio y registra únicamente esos eventos. Dado que los usuarios pueden activar más de un servicio de accesibilidad a la vez, tu servicio no debe consumir eventos que no pueda controlar. Recuerda que otros servicios pueden controlar esos eventos para mejorar la experiencia del usuario.

Volumen de accesibilidad

Los dispositivos con Android 8.0 (nivel de API 26) y versiones posteriores incluyen la categoría de volumen STREAM_ACCESSIBILITY, que te permite controlar el volumen de la salida de audio de tu servicio de accesibilidad independientemente de otros sonidos en el dispositivo.

Los servicios de accesibilidad pueden configurar la opción FLAG_ENABLE_ACCESSIBILITY_VOLUME para usar este tipo de transmisión. Luego, puedes cambiar el volumen del audio de accesibilidad del dispositivo. Para ello, llama al método adjustStreamVolume() en la instancia de AudioManager del dispositivo.

En el siguiente fragmento de código, se muestra cómo un servicio de accesibilidad puede usar la categoría de volumen de STREAM_ACCESSIBILITY:

Kotlin

import android.media.AudioManager.*

class MyAccessibilityService : AccessibilityService() {

    private val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager

    override fun onAccessibilityEvent(accessibilityEvent: AccessibilityEvent) {
        if (accessibilityEvent.source.text == "Increase volume") {
            audioManager.adjustStreamVolume(AudioManager.STREAM_ACCESSIBILITY, ADJUST_RAISE, 0)
        }
    }
}

Java

import static android.media.AudioManager.*;

public class MyAccessibilityService extends AccessibilityService {
    private AudioManager audioManager =
            (AudioManager) getSystemService(AUDIO_SERVICE);

    @Override
    public void onAccessibilityEvent(AccessibilityEvent accessibilityEvent) {
        AccessibilityNodeInfo interactedNodeInfo =
                accessibilityEvent.getSource();
        if (interactedNodeInfo.getText().equals("Increase volume")) {
            audioManager.adjustStreamVolume(AudioManager.STREAM_ACCESSIBILITY,
                ADJUST_RAISE, 0);
        }
    }
}

Para obtener más información, consulta el video de la sesión Novedades de la accesibilidad de Android de Google I/O 2017 (desde el minuto 6:35).

Acceso directo de accesibilidad

En los dispositivos con Android 8.0 (nivel de API 26) y versiones posteriores, los usuarios pueden habilitar e inhabilitar su servicio de accesibilidad preferido desde cualquier pantalla si mantienen presionadas ambas teclas de volumen al mismo tiempo. Aunque esta combinación de teclas habilita e inhabilita TalkBack de forma predeterminada, los usuarios pueden configurar el botón para habilitar o inhabilitar cualquier servicio que esté instalado en sus dispositivos.

Para que los usuarios accedan a un servicio de accesibilidad específico desde el acceso directo de accesibilidad, el servicio debe solicitar la función durante el tiempo de ejecución.

Para obtener más información, consulta el video de la sesión Novedades de la accesibilidad de Android de Google I/O 2017 (desde el minuto 13:25).

Botón de accesibilidad

En los dispositivos que usan un área de navegación procesada por software y ejecutan Android 8.0 (nivel de API 26) o versiones posteriores, el lado derecho de la barra de navegación incluye un botón de accesibilidad. Cuando los usuarios presionan este botón, pueden invocar uno de varios servicios y funciones de accesibilidad habilitados, según el contenido que se muestra actualmente en la pantalla.

Para permitir que los usuarios invoquen un servicio de accesibilidad determinado con el botón de accesibilidad, el servicio debe agregar la marca FLAG_REQUEST_ACCESSIBILITY_BUTTON en el atributo android:accessibilityFlags de un objeto AccessibilityServiceInfo. Luego, el servicio puede registrar devoluciones de llamada con registerAccessibilityButtonCallback().

En el siguiente fragmento de código, se muestra cómo puedes configurar un servicio de accesibilidad para que responda cuando un usuario presiona el botón de accesibilidad:

Kotlin

private var mAccessibilityButtonController: AccessibilityButtonController? = null
private var accessibilityButtonCallback:
        AccessibilityButtonController.AccessibilityButtonCallback? = null
private var mIsAccessibilityButtonAvailable: Boolean = false

override fun onServiceConnected() {
    mAccessibilityButtonController = accessibilityButtonController
    mIsAccessibilityButtonAvailable =
            mAccessibilityButtonController?.isAccessibilityButtonAvailable ?: false

    if (!mIsAccessibilityButtonAvailable) return

    serviceInfo = serviceInfo.apply {
        flags = flags or AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON
    }

    accessibilityButtonCallback =
        object : AccessibilityButtonController.AccessibilityButtonCallback() {
            override fun onClicked(controller: AccessibilityButtonController) {
                Log.d("MY_APP_TAG", "Accessibility button pressed!")

                // Add custom logic for a service to react to the
                // accessibility button being pressed.
            }

            override fun onAvailabilityChanged(
                    controller: AccessibilityButtonController,
                    available: Boolean
            ) {
                if (controller == mAccessibilityButtonController) {
                    mIsAccessibilityButtonAvailable = available
                }
            }
    }

    accessibilityButtonCallback?.also {
        mAccessibilityButtonController?.registerAccessibilityButtonCallback(it, null)
    }
}

Java

private AccessibilityButtonController accessibilityButtonController;
private AccessibilityButtonController
        .AccessibilityButtonCallback accessibilityButtonCallback;
private boolean mIsAccessibilityButtonAvailable;

@Override
protected void onServiceConnected() {
    accessibilityButtonController = getAccessibilityButtonController();
    mIsAccessibilityButtonAvailable =
            accessibilityButtonController.isAccessibilityButtonAvailable();

    if (!mIsAccessibilityButtonAvailable) {
        return;
    }

    AccessibilityServiceInfo serviceInfo = getServiceInfo();
    serviceInfo.flags
            |= AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON;
    setServiceInfo(serviceInfo);

    accessibilityButtonCallback =
        new AccessibilityButtonController.AccessibilityButtonCallback() {
            @Override
            public void onClicked(AccessibilityButtonController controller) {
                Log.d("MY_APP_TAG", "Accessibility button pressed!");

                // Add custom logic for a service to react to the
                // accessibility button being pressed.
            }

            @Override
            public void onAvailabilityChanged(
              AccessibilityButtonController controller, boolean available) {
                if (controller.equals(accessibilityButtonController)) {
                    mIsAccessibilityButtonAvailable = available;
                }
            }
        };

    if (accessibilityButtonCallback != null) {
        accessibilityButtonController.registerAccessibilityButtonCallback(
                accessibilityButtonCallback, null);
    }
}

Para obtener más información, consulta el video de la sesión Novedades de la accesibilidad de Android de Google I/O 2017 (desde el minuto 16:28).

Gestos del sensor de huellas dactilares

Los servicios de accesibilidad en dispositivos con Android 8.0 (nivel de API 26) o versiones posteriores pueden responder a los deslizamientos direccionales (arriba, abajo, izquierda y derecha) a lo largo del sensor de huellas dactilares de un dispositivo. Si deseas configurar un servicio para que reciba devoluciones de llamada sobre estas interacciones, completa la siguiente secuencia:

  1. Declara el permiso USE_BIOMETRIC y la capacidad CAPABILITY_CAN_REQUEST_FINGERPRINT_GESTURES.
  2. Establece la marca FLAG_REQUEST_FINGERPRINT_GESTURES dentro del atributo android:accessibilityFlags.
  3. Regístrate para recibir devoluciones de llamada con registerFingerprintGestureCallback().

Ten en cuenta que no todos los dispositivos tienen sensores de huellas digitales. Para identificar si un dispositivo admite el sensor, usa el método isHardwareDetected(). Incluso en un dispositivo que incluye un sensor de huellas dactilares, tu servicio no puede usar el sensor cuando está en uso con fines de autenticación. Para identificar cuándo está disponible el sensor, llama al método isGestureDetectionAvailable() e implementa la devolución de llamada onGestureDetectionAvailabilityChanged().

En el siguiente fragmento de código, se muestra un ejemplo de cómo usar los gestos del sensor de huellas digitales para navegar por un tablero de juegos virtual.

// AndroidManifest.xml
<manifest ... >
    <uses-permission android:name="android.permission.USE_FINGERPRINT" />
    ...
    <application>
        <service android:name="com.example.MyFingerprintGestureService" ... >
            <meta-data
                android:name="android.accessibilityservice"
                android:resource="@xml/myfingerprintgestureservice" />
        </service>
    </application>
</manifest>
// myfingerprintgestureservice.xml
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    ...
    android:accessibilityFlags=" ... |flagRequestFingerprintGestures"
    android:canRequestFingerprintGestures="true"
    ... />

Kotlin

// MyFingerprintGestureService.kt
import android.accessibilityservice.FingerprintGestureController.*

class MyFingerprintGestureService : AccessibilityService() {

    private var gestureController: FingerprintGestureController? = null
    private var fingerprintGestureCallback:
            FingerprintGestureController.FingerprintGestureCallback? = null
    private var mIsGestureDetectionAvailable: Boolean = false

    override fun onCreate() {
        gestureController = fingerprintGestureController
        mIsGestureDetectionAvailable = gestureController?.isGestureDetectionAvailable ?: false
    }

    override fun onServiceConnected() {
        if (mFingerprintGestureCallback != null || !mIsGestureDetectionAvailable) return

        fingerprintGestureCallback =
                object : FingerprintGestureController.FingerprintGestureCallback() {
                    override fun onGestureDetected(gesture: Int) {
                        when (gesture) {
                            FINGERPRINT_GESTURE_SWIPE_DOWN -> moveGameCursorDown()
                            FINGERPRINT_GESTURE_SWIPE_LEFT -> moveGameCursorLeft()
                            FINGERPRINT_GESTURE_SWIPE_RIGHT -> moveGameCursorRight()
                            FINGERPRINT_GESTURE_SWIPE_UP -> moveGameCursorUp()
                            else -> Log.e(MY_APP_TAG, "Error: Unknown gesture type detected!")
                        }
                    }

                    override fun onGestureDetectionAvailabilityChanged(available: Boolean) {
                        mIsGestureDetectionAvailable = available
                    }
                }

        fingerprintGestureCallback?.also {
            gestureController?.registerFingerprintGestureCallback(it, null)
        }
    }
}

Java

// MyFingerprintGestureService.java
import static android.accessibilityservice.FingerprintGestureController.*;

public class MyFingerprintGestureService extends AccessibilityService {
    private FingerprintGestureController gestureController;
    private FingerprintGestureController
            .FingerprintGestureCallback fingerprintGestureCallback;
    private boolean mIsGestureDetectionAvailable;

    @Override
    public void onCreate() {
        gestureController = getFingerprintGestureController();
        mIsGestureDetectionAvailable =
                gestureController.isGestureDetectionAvailable();
    }

    @Override
    protected void onServiceConnected() {
        if (fingerprintGestureCallback != null
                || !mIsGestureDetectionAvailable) {
            return;
        }

        fingerprintGestureCallback =
               new FingerprintGestureController.FingerprintGestureCallback() {
            @Override
            public void onGestureDetected(int gesture) {
                switch (gesture) {
                    case FINGERPRINT_GESTURE_SWIPE_DOWN:
                        moveGameCursorDown();
                        break;
                    case FINGERPRINT_GESTURE_SWIPE_LEFT:
                        moveGameCursorLeft();
                        break;
                    case FINGERPRINT_GESTURE_SWIPE_RIGHT:
                        moveGameCursorRight();
                        break;
                    case FINGERPRINT_GESTURE_SWIPE_UP:
                        moveGameCursorUp();
                        break;
                    default:
                        Log.e(MY_APP_TAG,
                                  "Error: Unknown gesture type detected!");
                        break;
                }
            }

            @Override
            public void onGestureDetectionAvailabilityChanged(boolean available) {
                mIsGestureDetectionAvailable = available;
            }
        };

        if (fingerprintGestureCallback != null) {
            gestureController.registerFingerprintGestureCallback(
                    fingerprintGestureCallback, null);
        }
    }
}

Para obtener más información, consulta el video de la sesión Novedades de la accesibilidad de Android de Google I/O 2017 (desde el minuto 9:03).

Texto a voz multilingüe

A partir de Android 8.0 (nivel de API 26), el servicio de texto a voz (TTS) de Android puede identificar y pronunciar frases en varios idiomas dentro de un solo bloque de texto. Para habilitar esta capacidad de cambio automático de idioma en un servicio de accesibilidad, une todas las strings en objetos LocaleSpan, como se muestra en el siguiente fragmento de código:

Kotlin

val localeWrappedTextView = findViewById<TextView>(R.id.my_french_greeting_text).apply {
    text = wrapTextInLocaleSpan("Bonjour!", Locale.FRANCE)
}

private fun wrapTextInLocaleSpan(originalText: CharSequence, loc: Locale): SpannableStringBuilder {
    return SpannableStringBuilder(originalText).apply {
        setSpan(LocaleSpan(loc), 0, originalText.length - 1, 0)
    }
}

Java

TextView localeWrappedTextView = findViewById(R.id.my_french_greeting_text);
localeWrappedTextView.setText(wrapTextInLocaleSpan("Bonjour!", Locale.FRANCE));

private SpannableStringBuilder wrapTextInLocaleSpan(
        CharSequence originalText, Locale loc) {
    SpannableStringBuilder myLocaleBuilder =
            new SpannableStringBuilder(originalText);
    myLocaleBuilder.setSpan(new LocaleSpan(loc), 0,
            originalText.length() - 1, 0);
    return myLocaleBuilder;
}

Para obtener más información, consulta el video de la sesión Novedades de la accesibilidad de Android de Google I/O 2017 (desde el minuto 10:59).

Actuar en nombre de los usuarios

A partir de 2011, los servicios de accesibilidad pueden actuar en nombre de los usuarios, lo que incluye cambiar el enfoque de entrada y seleccionar (activar) elementos de la interfaz de usuario. En 2012, se expandió el rango de acciones para incluir listas de desplazamiento y la interacción con campos de texto. Los servicios de accesibilidad también pueden realizar acciones globales, como navegar a la pantalla principal, presionar el botón Atrás y abrir la pantalla de notificaciones y la lista de apps recientes. Desde 2012, Android incluye el enfoque de accesibilidad, que hace que un servicio de accesibilidad pueda seleccionar todos los elementos visibles.

Estas funciones permiten a los desarrolladores de servicios de accesibilidad crear modos alternativos de navegación, como la navegación por gestos, y brindar a los usuarios con discapacidades un control mejorado de sus dispositivos Android.

Detecta gestos

Los servicios de accesibilidad pueden escuchar gestos específicos y responder actuando en nombre de un usuario. Esta función requiere que actives la solicitud de servicio de accesibilidad de la función Exploración táctil. Para solicitar esta activación, tu servicio puede configurar el miembro flags de la instancia AccessibilityServiceInfo del servicio en FLAG_REQUEST_TOUCH_EXPLORATION_MODE, como se muestra en el siguiente ejemplo.

Kotlin

class MyAccessibilityService : AccessibilityService() {

    override fun onCreate() {
        serviceInfo.flags = AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE
    }
    ...
}

Java

public class MyAccessibilityService extends AccessibilityService {
    @Override
    public void onCreate() {
        getServiceInfo().flags = AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE;
    }
    ...
}

Cuando el servicio solicita la activación de Exploración táctil, el usuario debe permitir que se active la función, si es que aún no está activa. Cuando esta función está activa, tu servicio recibe notificaciones de gestos de accesibilidad a través del método de devolución de llamada onGesture() del servicio y puede responder actuando en nombre del usuario.

Gestos continuos

Los dispositivos que ejecutan Android 8.0 (nivel de API 26) admiten gestos continuos o gestos programáticos que contienen más de un objeto Path.

Cuando especificas una secuencia de trazos, puedes indicar que pertenecen al mismo gesto programático. Para ello, usa el argumento final willContinue en el constructor GestureDescription.StrokeDescription, como se muestra en el siguiente fragmento de código:

Kotlin

// Simulates an L-shaped drag path: 200 pixels right, then 200 pixels down.
private fun doRightThenDownDrag() {
    val dragRightPath = Path().apply {
        moveTo(200f, 200f)
        lineTo(400f, 200f)
    }
    val dragRightDuration = 500L // 0.5 second

    // The starting point of the second path must match
    // the ending point of the first path.
    val dragDownPath = Path().apply {
        moveTo(400f, 200f)
        lineTo(400f, 400f)
    }
    val dragDownDuration = 500L
    val rightThenDownDrag = GestureDescription.StrokeDescription(
            dragRightPath,
            0L,
            dragRightDuration,
            true
    ).apply {
        continueStroke(dragDownPath, dragRightDuration, dragDownDuration, false)
    }
}

Java

// Simulates an L-shaped drag path: 200 pixels right, then 200 pixels down.
private void doRightThenDownDrag() {
    Path dragRightPath = new Path();
    dragRightPath.moveTo(200, 200);
    dragRightPath.lineTo(400, 200);
    long dragRightDuration = 500L; // 0.5 second

    // The starting point of the second path must match
    // the ending point of the first path.
    Path dragDownPath = new Path();
    dragDownPath.moveTo(400, 200);
    dragDownPath.lineTo(400, 400);
    long dragDownDuration = 500L;
    GestureDescription.StrokeDescription rightThenDownDrag =
            new GestureDescription.StrokeDescription(dragRightPath, 0L,
            dragRightDuration, true);
    rightThenDownDrag.continueStroke(dragDownPath, dragRightDuration,
            dragDownDuration, false);
}

Para obtener más información, consulta el video de la sesión Novedades de la accesibilidad de Android de Google I/O 2017 (desde el minuto 15:47).

Usa acciones de accesibilidad

Los servicios de accesibilidad pueden actuar en nombre de los usuarios para simplificar las interacciones con las apps y ser más productivos. La capacidad de los servicios de accesibilidad de realizar acciones se agregó en 2011 y se expandió significativamente en 2012.

Para actuar en nombre de los usuarios, tu servicio de accesibilidad debe registrarse a fin de recibir eventos de las apps y solicitar permiso para ver el contenido de ellas configurando android:canRetrieveWindowContent como true en el archivo de configuración del servicio. Cuando tu servicio recibe eventos, puede recuperar el objeto AccessibilityNodeInfo del evento mediante getSource(). Con el objeto AccessibilityNodeInfo, tu servicio puede explorar la jerarquía de vistas para determinar qué acción tomar y, luego, actuar en nombre del usuario con performAction().

Kotlin

class MyAccessibilityService : AccessibilityService() {

    override fun onAccessibilityEvent(event: AccessibilityEvent) {
        // Get the source node of the event.
        event.source?.apply {

            // Use the event and node information to determine what action to
            // take.

            // Act on behalf of the user.
            performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)

            // Recycle the nodeInfo object.
            recycle()
        }
    }
    ...
}

Java

public class MyAccessibilityService extends AccessibilityService {

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        // Get the source node of the event.
        AccessibilityNodeInfo nodeInfo = event.getSource();

        // Use the event and node information to determine what action to take.

        // Act on behalf of the user.
        nodeInfo.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);

        // Recycle the nodeInfo object.
        nodeInfo.recycle();
    }
    ...
}

El método performAction() permite que tu servicio realice acciones dentro de una app. Si tu servicio necesita realizar una acción general, como navegar a la pantalla principal, presionar el botón Atrás o abrir la pantalla de notificaciones o la lista de apps recientes, usa el método performGlobalAction().

Usa tipos de enfoque

En 2012, Android introdujo un enfoque en la interfaz de usuario llamado enfoque de accesibilidad. Los servicios de accesibilidad pueden usar este enfoque para seleccionar cualquier elemento visible de la interfaz de usuario y realizar acciones al respecto. Este tipo de enfoque es diferente del foco de entrada, que determina qué elemento de la interfaz de usuario en pantalla recibe una entrada cuando un usuario escribe caracteres o presiona Intro en un teclado o el botón central de un pad direccional.

Es posible que un elemento de una interfaz de usuario tenga el enfoque de entrada, mientras que otro elemento tiene el enfoque de accesibilidad. El propósito del enfoque de accesibilidad es proporcionar servicios de accesibilidad con un método para interactuar con los elementos visibles en una pantalla, independientemente de si el elemento es enfocable en la entrada desde una perspectiva del sistema. Para asegurarte de que el servicio de accesibilidad interactúe correctamente con los elementos de entrada de las apps, sigue los lineamientos para probar la accesibilidad de una app y probar tu servicio mientras usas una app típica.

Un servicio de accesibilidad puede determinar qué elemento de la interfaz de usuario tiene enfoque de entrada o de accesibilidad mediante el método AccessibilityNodeInfo.findFocus(). También puedes buscar elementos que se pueden seleccionar con enfoque de entrada mediante el método focusSearch(). Por último, tu servicio de accesibilidad puede establecer el enfoque de accesibilidad mediante el método performAction(AccessibilityNodeInfo.ACTION_SET_ACCESSIBILITY_FOCUS).

Recopila información

Los servicios de accesibilidad tienen métodos estándar para recopilar y representar unidades clave de información proporcionada por el usuario, como detalles de eventos, texto y números.

Obtén información sobre el cambio de ventanas

Android 9 (nivel de API 28) y las versiones posteriores permiten que las apps hagan un seguimiento de las actualizaciones de ventanas cuando una app vuelve a dibujar varias ventanas de manera simultánea. Cuando se produzca un evento TYPE_WINDOWS_CHANGED, usa la API de getWindowChanges() para determinar cómo cambian las ventanas. Durante una actualización multiventana, cada una produce su propio conjunto de eventos. El método getSource() muestra la vista raíz de la ventana asociada con cada evento.

Si una app define los títulos del panel de accesibilidad para sus objetos View, tu servicio puede reconocer cuándo se actualiza la IU de la app. Cuando ocurra un evento TYPE_WINDOW_STATE_CHANGED, usa los tipos que muestra getContentChangeTypes() para determinar cómo cambia la ventana. Por ejemplo, el framework puede detectar en qué momento un panel tiene un título nuevo o desaparece.

Obtén detalles del evento

Android proporciona información a los servicios de accesibilidad sobre la interacción de la interfaz de usuario a través de objetos AccessibilityEvent. En versiones anteriores de Android, la información disponible en un evento de accesibilidad, si bien proporcionaba detalles significativos sobre el control de la interfaz de usuario que seleccionaban los usuarios, ofrecía información contextual limitada. En muchos casos, la información contextual que falta puede ser fundamental para comprender el significado del control seleccionado.

Un ejemplo de una interfaz en la que el contexto es fundamental es un calendario o un planificador diario. Si el usuario selecciona un horario disponible de 4:00 p.m. en una lista diaria de lunes a viernes y el servicio de accesibilidad anuncia "4:00 p.m.", pero no menciona el nombre del día de la semana, el día del mes o el nombre del mes, el resultado es confuso. En este caso, el contexto de un control de la interfaz de usuario es fundamental para un usuario que desea programar una reunión.

Desde 2011, Android amplía considerablemente la cantidad de información que un servicio de accesibilidad puede obtener sobre la interacción de una interfaz de usuario redactando eventos de accesibilidad basados en la jerarquía de vistas. Una jerarquía de vistas es el conjunto de componentes de la interfaz de usuario que contiene el componente (sus elementos superiores) y los elementos de la interfaz de usuario que ese componente podría contener (sus elementos secundarios). De esta manera, Android puede proporcionar más detalles sobre los eventos de accesibilidad, lo que permite que los servicios de accesibilidad proporcionen comentarios más útiles a los usuarios.

Un servicio de accesibilidad obtiene información sobre un evento de la interfaz de usuario a través de un AccessibilityEvent que el sistema pasa al método de devolución de llamada onAccessibilityEvent() del servicio. Este objeto proporciona detalles sobre el evento, incluido el tipo de objeto sobre el que se realiza la acción, su texto descriptivo y otros detalles.

  • AccessibilityEvent.getRecordCount() y getRecord(int): Estos métodos te permiten recuperar el conjunto de objetos AccessibilityRecord que contribuyen al AccessibilityEvent que te pasó el sistema. Este nivel de detalle proporciona más contexto para el evento que activa tu servicio de accesibilidad.

  • AccessibilityRecord.getSource(): Este método muestra un objeto AccessibilityNodeInfo. Este objeto te permite solicitar la jerarquía de diseño de vistas (elementos superiores y secundarios) del componente que origina el evento de accesibilidad. Esta función permite que un servicio de accesibilidad investigue todo el contexto de un evento, incluidos el contenido y el estado de las vistas contenedoras o secundarias.

La plataforma de Android permite que un AccessibilityService consulte la jerarquía de vistas y recopile información sobre el componente de IU que genera un evento, así como sus elementos superiores y secundarios. Para ello, establece la siguiente línea en tu configuración XML:

android:canRetrieveWindowContent="true"

Luego, obtén un objeto AccessibilityNodeInfo con getSource(). Esta llamada solo muestra un objeto si la ventana en la que se origina el evento sigue siendo la ventana activa. De lo contrario, muestra un valor nulo, por lo que debes comportarte en consecuencia.

En el siguiente ejemplo, el código hace lo siguiente cuando se recibe un evento:

  1. Captura inmediatamente el elemento superior de la vista en la que se origina el evento.
  2. En esa vista, busca una etiqueta y una casilla de verificación como vistas secundarias.
  3. Si las encuentra, crea una string para informar al usuario, que indica la etiqueta y si está marcada.

Si en algún momento se muestra un valor nulo mientras recorres la jerarquía de vistas, se abandona el método en silencio.

Kotlin

// Alternative onAccessibilityEvent that uses AccessibilityNodeInfo.

override fun onAccessibilityEvent(event: AccessibilityEvent) {

    val source: AccessibilityNodeInfo = event.source ?: return

    // Grab the parent of the view that fires the event.
    val rowNode: AccessibilityNodeInfo = getListItemNodeInfo(source) ?: return

    // Using this parent, get references to both child nodes, the label, and the
    // checkbox.
    val taskLabel: CharSequence = rowNode.getChild(0)?.text ?: run {
        rowNode.recycle()
        return
    }

    val isComplete: Boolean = rowNode.getChild(1)?.isChecked ?: run {
        rowNode.recycle()
        return
    }

    // Determine what the task is and whether it's complete based on the text
    // inside the label, and the state of the checkbox.
    if (rowNode.childCount < 2 || !rowNode.getChild(1).isCheckable) {
        rowNode.recycle()
        return
    }

    val completeStr: String = if (isComplete) {
        getString(R.string.checked)
    } else {
        getString(R.string.not_checked)
    }
    val reportStr = "$taskLabel$completeStr"
    speakToUser(reportStr)
}

Java

// Alternative onAccessibilityEvent that uses AccessibilityNodeInfo.

@Override
public void onAccessibilityEvent(AccessibilityEvent event) {

    AccessibilityNodeInfo source = event.getSource();
    if (source == null) {
        return;
    }

    // Grab the parent of the view that fires the event.
    AccessibilityNodeInfo rowNode = getListItemNodeInfo(source);
    if (rowNode == null) {
        return;
    }

    // Using this parent, get references to both child nodes, the label, and the
    // checkbox.
    AccessibilityNodeInfo labelNode = rowNode.getChild(0);
    if (labelNode == null) {
        rowNode.recycle();
        return;
    }

    AccessibilityNodeInfo completeNode = rowNode.getChild(1);
    if (completeNode == null) {
        rowNode.recycle();
        return;
    }

    // Determine what the task is and whether it's complete based on the text
    // inside the label, and the state of the checkbox.
    if (rowNode.getChildCount() < 2 || !rowNode.getChild(1).isCheckable()) {
        rowNode.recycle();
        return;
    }

    CharSequence taskLabel = labelNode.getText();
    final boolean isComplete = completeNode.isChecked();
    String completeStr = null;

    if (isComplete) {
        completeStr = getString(R.string.checked);
    } else {
        completeStr = getString(R.string.not_checked);
    }
    String reportStr = taskLabel + completeStr;
    speakToUser(reportStr);
}

Ahora tienes un servicio de accesibilidad completo y en funcionamiento. Intenta configurar cómo interactúa con el usuario. Para ello, agrega el motor de texto a voz de Android o usa Vibrator para proporcionar respuesta táctil.

Procesa texto

Los dispositivos con Android 8.0 (API nivel 26) y versiones posteriores incluyen varias funciones de procesamiento de texto que permiten que los servicios de accesibilidad identifiquen y operen con mayor facilidad en unidades de texto específicas que aparecen en la pantalla.

Cuadros de información

Android 9 (nivel de API 28) incluye varias funciones que te permiten acceder a la información sobre la herramienta en la IU de una app. Usa getTooltipText() para leer el texto de la información sobre la herramienta y usa ACTION_SHOW_TOOLTIP y ACTION_HIDE_TOOLTIP para indicar a las instancias de View que oculten o muestren su información.

Texto de la sugerencia

A partir de 2017, Android incluye varios métodos para interactuar con el texto de la sugerencia de un objeto basado en texto:

  • Los métodos isShowingHintText() y setShowingHintText() indican y establecen, respectivamente, si el contenido de texto actual del nodo representa el texto de la sugerencia del nodo.
  • getHintText() proporciona acceso al texto de la sugerencia. Incluso si un objeto no muestra texto de la sugerencia, la llamada a getHintText() se realiza de forma correcta.

Ubicaciones de los caracteres de texto en la pantalla

En los dispositivos que ejecutan Android 8.0 (nivel de API 26) y versiones posteriores, los servicios de accesibilidad pueden determinar las coordenadas de pantalla del cuadro delimitador de cada carácter visible dentro de un widget TextView. Para encontrar estas coordenadas, los servicios llaman a refreshWithExtraData() y pasan EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY como primer argumento y un objeto Bundle como segundo. A medida que se ejecuta el método, el sistema completa el argumento Bundle con un array parcelable de objetos Rect. Cada objeto Rect representa el cuadro delimitador de un carácter específico.

Valores de rango unilaterales estandarizados

Algunos objetos AccessibilityNodeInfo usan una instancia de AccessibilityNodeInfo.RangeInfo para indicar que un elemento de la IU puede aceptar un rango de valores. Cuando crees un rango usando RangeInfo.obtain() o cuando recuperes los valores extremos del rango usando getMin() y getMax(), ten en cuenta que los dispositivos con Android 8.0 (nivel de API 26) y versiones posteriores representan rangos unilaterales de manera estandarizada:

Responde a los eventos de accesibilidad

Ahora que tu servicio está configurado para ejecutarse y escuchar eventos, escribe código de modo que sepa qué hacer cuando llegue un AccessibilityEvent. Comienza anulando el método onAccessibilityEvent(AccessibilityEvent). En ese método, usa getEventType() para determinar el tipo de evento y getContentDescription() para extraer el texto de la etiqueta asociada con la vista que activa el evento:

Kotlin

override fun onAccessibilityEvent(event: AccessibilityEvent) {
    var eventText: String = when (event.eventType) {
        AccessibilityEvent.TYPE_VIEW_CLICKED -> "Clicked: "
        AccessibilityEvent.TYPE_VIEW_FOCUSED -> "Focused: "
        else -> ""
    }

    eventText += event.contentDescription

    // Do something nifty with this text, like speak the composed string back to
    // the user.
    speakToUser(eventText)
    ...
}

Java

@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
    final int eventType = event.getEventType();
    String eventText = null;
    switch(eventType) {
        case AccessibilityEvent.TYPE_VIEW_CLICKED:
            eventText = "Clicked: ";
            break;
        case AccessibilityEvent.TYPE_VIEW_FOCUSED:
            eventText = "Focused: ";
            break;
    }

    eventText = eventText + event.getContentDescription();

    // Do something nifty with this text, like speak the composed string back to
    // the user.
    speakToUser(eventText);
    ...
}

Recursos adicionales

Para obtener más información, consulta los siguientes recursos:

Guías

Codelabs