Cómo crear tu propio servicio de accesibilidad (vistas)

Conceptos y la implementación de Jetpack Compose

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. Por ejemplo, es posible que los usuarios que estén conduciendo, cuidando a un niño pequeño o en 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 sobre cómo crear un servicio de accesibilidad.

Un servicio de accesibilidad se puede ofrecer como paquete con una app normal o crear 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 tienes pensado asociar una app, puedes quitar la clase Activity de inicio de tu fuente.

Permisos y declaraciones de manifiesto

Las apps que proporcionan servicios de accesibilidad deben incluir declaraciones específicas en sus manifiestos para que el sistema de 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 en tu manifiesto. Además, dentro del elemento service, incluye un filtro de intent 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 el servicio controla y la 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 con una instancia de esta clase y setServiceInfo() en el tiempo 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 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 del 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"
/>

Si deseas obtener más información sobre los atributos de 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 dinámicamente en el tiempo de ejecución, consulta la documentación de referencia de AccessibilityServiceInfo.

Configura tu servicio de accesibilidad

Ten en cuenta lo siguiente cuando establezcas las variables de configuración de tu servicio de accesibilidad 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 establecer estas variables. La opción de retrocompatibilidad consiste en establecerlas en el código con 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 mediante 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 tu declaración de servicio que dirija al archivo en formato XML. Si almacenas tu archivo XML en res/xml/serviceconfig.xml, la etiqueta nueva se verá del siguiente modo:

<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 que se inicia el servicio (onServiceConnected()), mientras está en ejecución (onAccessibilityEvent(), onInterrupt()) y cuando se apaga (onUnbind()).

  • onServiceConnected(): (opcional) El sistema llama a este método cuando se conecta a tu servicio de accesibilidad. Usa este método para completar los pasos de configuración únicos para tu servicio, como conectarte a los servicios del sistema de comentarios del usuario, por ejemplo, el administrador de audio o el vibrador del dispositivo. Si quieres establecer la configuración de tu 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 vuelve a llamar a este método cuando detecta un AccessibilityEvent que coincide con los parámetros de filtrado de eventos especificados por tu servicio de accesibilidad, por ejemplo, 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 posteriormente. 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, como respuesta a la acción de un usuario, por ejemplo, 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(): (opcional) El sistema llama a este método cuando está a punto de apagar el servicio de accesibilidad. Usa este método para realizar cualquier procedimiento de cierre único, como anular la asignació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 de AccessibilityEvent y proporcionar comentarios a los usuarios. Para más obtener información sobre cómo conseguir datos del 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. Si especificas esta información, los servicios de accesibilidad pueden cooperar entre sí y tienes 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 quieras que controle tu servicio. Si se omite este parámetro, se considera que tu servicio de accesibilidad está disponible para brindar servicio a los eventos de accesibilidad de cualquier app. Puedes establecer 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 quieres 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 configurarlo 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. Debido a 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);
        }
    }
}

Si deseas 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).

Atajo 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 apretadas las dos teclas de volumen al mismo tiempo. Aunque esta combinación de teclas habilita e inhabilita Talkback de manera predeterminada, los usuarios pueden configurar el botón para habilitar e inhabilitar cualquier servicio instalado en su dispositivo.

Para que los usuarios puedan acceder a un servicio de accesibilidad específico desde la combinación de teclas de accesibilidad, el servicio debe solicitar la función en el tiempo de ejecución.

Si deseas 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) y 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 demuestra 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);
    }
}

Si deseas 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 digitales

Los servicios de accesibilidad en dispositivos con Android 8.0 (nivel de API 26) y versiones posteriores pueden responder a deslizamientos direccionales (arriba, abajo, izquierda y derecha) a lo largo del sensor de huellas dactilares de un dispositivo. Para 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 con sensor de huellas dactilares, tu servicio no puede usar el sensor cuando se está utilizando con fines de autenticación. Si deseas conocer la disponibilidad del sensor, llama al método isGestureDetectionAvailable() y, luego, 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);
        }
    }
}

Si deseas obtener más información, consulta el video de la sesión Novedades de la accesibilidad de Android de Google I/O 2017, a partir del 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 decir en voz alta frases en varios idiomas dentro de un mismo bloque de texto. Para habilitar esta capacidad de cambiar de idioma automáticamente en un servicio de accesibilidad, envuelve todas las cadenas 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).

Actúa 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, la variedad de acciones se expandió 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 permite que un servicio de accesibilidad seleccione todos los elementos visibles.

Estas capacidades permiten que los desarrolladores de servicios de accesibilidad creen modos de navegación alternativos, como la navegación por gestos, y brindarles a los usuarios con discapacidades mejor control de sus dispositivos Android.

Detecta gestos

Los servicios de accesibilidad pueden detectar gestos específicos y realizar acciones en nombre de un usuario para responder a ellos. Esta función requiere que tu servicio de accesibilidad solicite la activación de la función Exploración táctil. Para solicitar esta activación, tu servicio puede establecer 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;
    }
    ...
}

Después de que tu servicio solicite 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 los gestos de accesibilidad a través del método de devolución de llamada onGesture() y puede responder actuando en nombre del usuario.

Gestos continuos

Los dispositivos con Android 8.0 (nivel de API 26) y versiones posteriores admiten los gestos continuos o los gestos programáticos con 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);
}

Si deseas obtener más información, consulta el video de la sesión Novedades en 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 para realizar acciones se agregó en 2011 y se expandió considerablemente en 2012.

Para actuar en nombre de los usuarios, tu servicio de accesibilidad debe registrarse para recibir eventos de las apps y solicitar permiso para ver el contenido de las apps. Para ello, establece android:canRetrieveWindowContent en true en el archivo de configuración del servicio. Cuando tu servicio recibe eventos, puede recuperar el objeto AccessibilityNodeInfo del evento con getSource(). Con el objeto AccessibilityNodeInfo, tu servicio puede explorar la jerarquía de vistas para determinar qué acción realizar 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 global, 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 de 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 enfoque de entrada, que determina qué elemento de la interfaz de usuario en la pantalla recibe una entrada cuando un usuario escribe caracteres, presiona Intro en un teclado o presiona el botón central de un pad direccional.

Es posible que un elemento en una interfaz de usuario tenga enfoque de entrada y que otro elemento tenga enfoque de accesibilidad. El propósito del enfoque de accesibilidad es proporcionar servicios de accesibilidad con un método de interacción con elementos visibles en una pantalla, independientemente de que el elemento se pueda enfocar en la entrada desde una perspectiva del sistema. Para asegurarte de que tu 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 prueba 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 enfoque de accesibilidad con el método AccessibilityNodeInfo.findFocus(). También puedes usar el método focusSearch() para buscar elementos que se pueden seleccionar con el enfoque de entrada. Por último, tu servicio de accesibilidad puede establecer el enfoque de accesibilidad con 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 al mismo tiempo. Cuando se produce un evento de TYPE_WINDOWS_CHANGED, usa la API de getWindowChanges() para determinar cómo cambian las ventanas. Durante una actualización multiventana, cada ventana genera su propio conjunto de eventos. El método getSource() devuelve la vista raíz de la ventana asociada con cada evento.

Si una app define títulos del panel de accesibilidad para sus objetos View, tu servicio puede reconocer cuándo se actualiza la IU de la app. Cuando se produce un evento de TYPE_WINDOW_STATE_CHANGED, usa los tipos que devuelve 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 en qué momento un panel desapareció.

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, aunque proporcionaba detalles significativos sobre el control de la interfaz de usuario seleccionado por los usuarios, ofrecía información contextual limitada. En muchos casos, la información contextual que falta podría ser fundamental para entender el significado del control seleccionado.

Un ejemplo de una interfaz en la que el contexto es fundamental es un calendario o un plan del día. Si el usuario selecciona una franja horaria 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 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. Para ello, compone 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 información más detallada sobre los eventos de accesibilidad, lo que permite que los servicios de accesibilidad ofrezcan 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 información de contexto sobre el evento que activa tu servicio de accesibilidad.

  • AccessibilityRecord.getSource(): Este método devuelve un objeto AccessibilityNodeInfo. Este objeto te permite solicitar la jerarquía de diseño de la vista (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, incluido 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 su elemento superior y sus elementos secundarios. Para ello, establece la siguiente línea en tu configuración de XML:

android:canRetrieveWindowContent="true"

Cuando hayas terminado, usa getSource() para obtener un objeto AccessibilityNodeInfo. Esta llamada solo devuelve un objeto si la ventana en la que se origina el evento sigue activa. De lo contrario, mostrará un valor nulo y se comportará en consecuencia.

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

  1. Capta inmediatamente el elemento superior de la vista en la que se originó el evento.
  2. En esa vista, busca una etiqueta y una casilla de verificación como vistas secundarias.
  3. Si las encuentra, crea una cadena para informárselo al usuario, indicando la etiqueta y si estaba 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 un 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.

Información sobre la herramienta

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 utiliza ACTION_SHOW_TOOLTIP y ACTION_HIDE_TOOLTIP para indicar instancias de View a fin de mostrar u ocultar la información sobre la herramienta.

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 definen, 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 correctamente.

Ubicaciones de los caracteres de texto en la pantalla

En los dispositivos con 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 argumento. A medida que se ejecuta el método, el sistema completa el argumento Bundle con un arreglo con parcelas 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 con RangeInfo.obtain() o cuando recuperes los valores extremos del rango con getMin() y getMax(), ten en cuenta que, en los dispositivos con Android 8.0 (nivel de API 26) y versiones posteriores, los rangos unilaterales se representan de manera estandarizada:

Responde a los eventos de accesibilidad

Ahora que tu servicio está configurado y listo para detectar eventos, escribe código para que sepa qué hacer cuando llegue un AccessibilityEvent. Para comenzar, anula el método onAccessibilityEvent(AccessibilityEvent). En ese método, usa getEventType() para determinar el tipo de evento y getContentDescription() para extraer cualquier etiqueta de texto 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