Únete a ⁠ #Android11: The Beta Launch Show el 3 de junio.

Crea tu propio servicio de accesibilidad

Un servicio de accesibilidad es una aplicación que proporciona mejoras de la interfaz de usuario para asistir a los usuarios que tienen 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.

Nota: Tu app solo debe usar servicios de accesibilidad a nivel de la plataforma con el objetivo de ayudar a los usuarios con discapacidades a interactuar con tu app.

La capacidad de crear e implementar servicios de accesibilidad se introdujo en Android 1.6 (API nivel 4) y mejoró significativamente en Android 4.0 (API nivel 14). La biblioteca de compatibilidad de Android también se actualizó con el lanzamiento de Android 4.0 a fin de que estas funciones de accesibilidad mejoradas fueran compatibles con Android 1.6. A los desarrolladores que deseen crear servicios de accesibilidad ampliamente compatibles, se les recomienda usar la biblioteca de compatibilidad y desarrollar para las funciones de accesibilidad más avanzadas que se introdujeron en Android 4.0.

Crea tu servicio de accesibilidad

Un servicio de accesibilidad se puede ofrecer como paquete con una aplicación normal o creada como un proyecto de Android independiente. Los pasos para crear el servicio son los mismos en cualquier caso. 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 creaste un proyecto nuevo para este servicio y no tienes pensado asociar una app, puedes quitar la clase de actividad de inicio de tu fuente.

Permisos y declaraciones de manifiesto

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

Declaración del servicio de accesibilidad

Para que tu aplicación se considere un servicio de accesibilidad, debes incluir un elemento service (en lugar de un elemento activity) dentro del elemento application en tu manifiesto. Además, dentro del elemento service, también debes incluir un filtro de intent del servicio de accesibilidad. En cuanto a la compatibilidad con Android 4.1 y versiones posteriores, el manifiesto también debe proteger el servicio agregando el permiso de BIND_ACCESSIBILITY_SERVICE a fin de 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>
    

Estas declaraciones se requieren para todos los servicios de accesibilidad implementados en Android 1.6 (API nivel 4) o versiones posteriores.

Configuración del servicio de accesibilidad

Los servicios de accesibilidad también deben proporcionar una configuración que especifique los tipos de eventos de accesibilidad que el servicio maneja 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 mediante 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.

A partir de Android 4.0, 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 de metadatos hace referencia a un archivo XML que creas en el directorio de recursos de tu aplicación (<project_dir>/res/xml/accessibility_service_config.xml). En el siguiente código, se muestra contenido de ejemplo para el 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, sigue estos vínculos a la 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

Establece las variables de configuración de tu servicio de accesibilidad a fin de indicarle al sistema cómo y cuándo quieres ejecutarlo. ¿A qué tipos de eventos te gustaría responder? ¿El servicio debería estar activo para todas las aplicaciones o solo para nombres de paquetes específicos? ¿Qué tipos de comentarios usa?

Tienes dos opciones sobre cómo establecer estas variables. La opción de retrocompatibilidad consiste en establecerlas en el código mediante setServiceInfo(android.accessibilityservice.AccessibilityServiceInfo). Para ello, anula el método de onServiceConnected() y configura tu servicio allí.

Kotlin

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

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

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

            // Default services are invoked only if no package-specific ones are present
            // for the type of AccessibilityEvent generated. This service *is*
            // application-specific, so the flag isn't necessary. If this was a
            // general-purpose service, it would be worth considering 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
        // won't be passed to this service.
        info.eventTypes = AccessibilityEvent.TYPE_VIEW_CLICKED |
                AccessibilityEvent.TYPE_VIEW_FOCUSED;

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

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

        // Default services are invoked only if no package-specific ones are present
        // for the type of AccessibilityEvent generated. This service *is*
        // application-specific, so the flag isn't necessary. If this was a
        // general-purpose service, it would be worth considering 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 mediante XML. Las mismas opciones de configuración que las anteriores, definidas mediante XML, se verían del siguiente modo:

    <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 optas por XML, asegúrate de mencionarlo en tu manifiesto. Para ello, agrega una etiqueta <meta-data> a tu declaración de servicio, que dirija al archivo XML. Si almacenaste 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 el que el sistema Android los llama, desde que se inicia el servicio (onServiceConnected()), mientras está en ejecución (onAccessibilityEvent(), onInterrupt()) y cuando se cierra (onUnbind()).

  • onServiceConnected() (opcional): El sistema llama a este método cuando se conecta correctamente a tu servicio de accesibilidad. Usa este método para completar cualquier paso de configuración único 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 el evento que filtra los parámetros especificado por tu servicio de accesibilidad. Por ejemplo, cuando el usuario hace clic en un botón o se enfoca en un control de la interfaz de usuario en una aplicación para la que tu servicio de accesibilidad proporciona comentarios. Cuando esto sucede, el sistema llama a este método y pasa el AccessibilityEvent asociado, que el sistema 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): Se llama a este método cuando el sistema 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): Se llama a este método cuando el sistema está a punto de cerrar el servicio de accesibilidad. Usa este método para realizar cualquier procedimiento de cierre único, como desasignar 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. Debes 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 manejar tu servicio. Si especificas esta información, los servicios de accesibilidad pueden cooperar entre sí y, como desarrollador, tienes la flexibilidad de manejar solo tipos de eventos específicos de aplicaciones específicas. El filtrado de eventos puede incluir los siguientes criterios:

  • Nombres de paquetes: Especifica los nombres de paquetes de las aplicaciones cuyos eventos de accesibilidad quieras que maneje tu servicio. Si omites este parámetro, tu servicio de accesibilidad se considera disponible para brindar servicio a los eventos de accesibilidad de cualquier aplicación. Este parámetro se puede establecer en los archivos de configuración del servicio de accesibilidad con el atributo android:packageNames como una lista separada por comas o mediante el miembro de AccessibilityServiceInfo.packageNames.
  • Tipos de eventos: Especifica los tipos de eventos de accesibilidad que quieres que maneje tu servicio. Este parámetro se puede establecer 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"), o puedes establecerlo con el miembro AccessibilityServiceInfo.eventTypes.

Cuando configures tu servicio de accesibilidad, considera con atención qué eventos puede manejar 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 manejar. Recuerda que otros servicios pueden manejar esos eventos a fin de mejorar la experiencia de un usuario.

Volumen de accesibilidad

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

Los servicios de accesibilidad pueden configurar la opción de 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).

Acceso directo de accesibilidad

En los dispositivos con Android 8.0 (API nivel 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, un usuario puede configurar el botón para habilitar e inhabilitar cualquier servicio instalado en su dispositivo, incluido el tuyo.

A fin de 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 cuando se inicia, 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 con Android 8.0 (API nivel 26) o versiones posteriores que usan un área de navegación procesada por software, el lado derecho de la barra de navegación incluye un botón de accesibilidad. Cuando un usuario presiona este botón, puede 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 mediante 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 mediante registerAccessibilityButtonCallback().

Nota: Esta función solo está disponible en dispositivos con un área de navegación procesada por software. Los servicios siempre deben usar isAccessibilityButtonAvailable() y responder a los cambios en función de la disponibilidad del botón de accesibilidad mediante la implementación de onAvailabilityChanged(). De esta manera, los usuarios siempre pueden acceder a la funcionalidad del servicio, incluso si el botón de accesibilidad no es compatible o deja de estar disponible.

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 (API nivel 26) o versiones posteriores pueden responder a un mecanismo de entrada alternativo y deslizamientos direccionales (arriba, abajo, izquierda, derecha) junto con el sensor de huellas digitales de un dispositivo. A fin de configurar un servicio para que reciba devoluciones de llamada sobre estas interacciones, completa la siguiente secuencia de pasos:

  1. Declara el permiso de USE_FINGERPRINT y la capacidad de CAPABILITY_CAN_REQUEST_FINGERPRINT_GESTURES.
  2. Establece la marca FLAG_REQUEST_FINGERPRINT_GESTURES dentro del atributo android:accessibilityFlags.
  3. Usa registerFingerprintGestureCallback() a fin de registrarte para recibir devoluciones de llamada.

Nota: Debes permitir que los usuarios inhabiliten la compatibilidad que un servicio de accesibilidad tiene con los gestos del sensor de huellas digitales. Aunque varios servicios de accesibilidad pueden detectar gestos del sensor de huellas digitales de manera simultánea, esto provoca que los servicios entren en conflicto unos con otros.

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 digitales, 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"
        ... />
    

MyFingerprintGestureService.java

Kotlin

    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

    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 (desde el minuto 9:03).

Texto a voz multilingüe

A partir de Android 8.0 (API nivel 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 de manera automática en un servicio de accesibilidad, envuelve 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).

Toma medidas para los usuarios

A partir de Android 4.0 (API nivel 14), 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 Android 4.1 (API nivel 16) se expandió la variedad de acciones y ahora se incluyen las 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 de inicio, presionar el botón "Atrás" y abrir la pantalla de notificaciones y la lista de aplicaciones recientes. Android 4.1 también incluye un tipo de enfoque nuevo, el enfoque de accesibilidad, que permite que un servicio de accesibilidad seleccione todos los elementos visibles.

Estas capacidades nuevas 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, que se agregó en Android 4.1 (API nivel 16), 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 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;
        }
        ...
    }
    

Una vez que tu servicio haya solicitado 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 realizando acciones en nombre del usuario.

Gestos continuos

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

Cuando especificas las secuencias de trazos, debes 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 realizar acciones en nombre de los usuarios para lograr que la interacción con las aplicaciones sea más simple y productiva. La capacidad de los servicios de accesibilidad de realizar acciones se agregó en Android 4.0 (API nivel 14) y se expandió considerablemente con Android 4.1 (API nivel 16).

A fin de realizar acciones en nombre de los usuarios, tu servicio de accesibilidad debe registrarse para recibir eventos de algunas o muchas aplicaciones y solicitar permiso para ver el contenido de las aplicaciones. Para ello, establece el android:canRetrieveWindowContent en 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 a fin de determinar qué acción realizar en nombre del usuario mediante 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

                // take action 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

            // take action 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 aplicación. Si tu servicio necesita realizar una acción global, como navegar a la pantalla principal, presiona el botón "Atrás", abre la pantalla de notificaciones o la lista de aplicaciones recientes, y luego usa el método performGlobalAction().

Usa tipos de enfoque

Android 4.1 (API nivel 16) introduce un tipo nuevo de enfoque de la interfaz de usuario, llamado enfoque de accesibilidad. Los servicios de accesibilidad pueden usar este tipo de enfoque para seleccionar los elementos visibles de la interfaz de usuario y realizar acciones al respecto. Este tipo de enfoque es diferente del enfoque de entrada, que es más conocido y determina qué elemento de la interfaz de usuario en la pantalla recibe una entrada cuando un usuario escribe caracteres o presiona Intro en un teclado o el botón central de un control de pad direccional.

El enfoque de accesibilidad es independiente y está separado por completo del enfoque de entrada. De hecho, es posible que un elemento en una interfaz de usuario tenga un enfoque de entrada y que otro elemento tenga un enfoque de accesibilidad. El propósito del enfoque de accesibilidad es proporcionar servicios de accesibilidad mediante un método de interacción con cualquier elemento visible en una pantalla, independientemente de que el elemento se pueda enfocar en la entrada desde una perspectiva del sistema. Puedes probar los gestos de accesibilidad para ver el enfoque de accesibilidad en acción. Para obtener más información sobre cómo probar esta función, consulta Cómo probar la navegación por gestos.

Nota: Los servicios de accesibilidad que usan el enfoque de accesibilidad son responsables de sincronizar el enfoque de entrada actual cuando un elemento admite este tipo de enfoque. Los servicios que no sincronizan el enfoque de entrada con el enfoque de accesibilidad corren el riesgo de ocasionar problemas en las aplicaciones que esperan que el enfoque de entrada esté en una ubicación específica cuando se realizan determinadas acciones.

Un servicio de accesibilidad puede determinar qué elemento de la interfaz de usuario tiene enfoque de entrada o enfoque de accesibilidad mediante 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 mediante el método performAction(AccessibilityNodeInfo.ACTION_SET_ACCESSIBILITY_FOCUS).

Recopila información

Los servicios de accesibilidad también 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 detalles del evento

El sistema Android proporciona información a los servicios de accesibilidad sobre la interacción de la interfaz de usuario a través de objetos AccessibilityEvent. Antes de Android 4.0, la información disponible en un evento de accesibilidad, aunque proporcionaba una cantidad considerable de datos sobre el control de la interfaz de usuario que seleccionaba el usuario, 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.

Android 4.0 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, el sistema Android puede proporcionar información mucho 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 información sobre el evento, como el tipo de objeto para el que se realiza la acción, su texto descriptivo y otros detalles. A partir de Android 4.0 (y con compatibilidad en las actualizaciones anteriores a través del objeto AccessibilityEventCompat en la biblioteca de compatibilidad), puedes obtener información adicional sobre el evento mediante las siguientes llamadas:

  • AccessibilityEvent.getRecordCount() y getRecord(int): Estos métodos te permiten recuperar el conjunto de objetos AccessibilityRecord que contribuyeron al AccessibilityEvent que te pasó el sistema. Este nivel de detalle proporciona más información de contexto sobre el evento que activó tu servicio de accesibilidad.
  • AccessibilityEvent.getSource(): Este método muestra un objeto AccessibilityNodeInfo. Este objeto te permite solicitar la jerarquía de diseño de las vistas (elementos superiores y secundarios) del componente que originó el evento de accesibilidad. Esta función permite que un servicio de accesibilidad investigue todo el contexto de un evento, como el contenido y el estado de las vistas contenedoras o secundarias.

    Importante: La capacidad de investigar la jerarquía de vistas desde un AccessibilityEvent podría exponer información privada sobre el usuario a tu servicio de accesibilidad. Por este motivo, tu servicio debería solicitar este nivel de acceso a través del archivo XML de configuración del servicio, incluir el atributo canRetrieveWindowContent y establecerlo en true. Si no incluyes esta configuración en tu archivo xml de configuración del servicio, las llamadas a getSource() fallan.

    Nota: En Android 4.1 (API nivel 16) y versiones posteriores, el método getSource(), además de AccessibilityNodeInfo.getChild() y getParent(), solo muestran objetos de vista que se consideran importantes para la accesibilidad (vistas que dibujan contenido o responden las acciones del usuario). Si tu servicio requiere todas las vistas, puede solicitarlas configurando en FLAG_INCLUDE_NOT_IMPORTANT_VIEWS el miembro flags de la instancia AccessibilityServiceInfo del servicio.

Obtén información sobre el cambio de ventanas

Android 9 (API nivel 28) y las versiones superiores 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 cambiaron las ventanas. Durante una actualización de varias ventanas, cada ventana genera su propio conjunto de eventos. El método getSource() muestra la vista de raíz de la ventana asociada con cada evento.

Si una app definió títulos del panel de accesibilidad para sus objetos View, tu servicio puede reconocer cuándo se actualiza la IU de la app. Si ocurre un evento de TYPE_WINDOW_STATE_CHANGED, utiliza los tipos que muestra getContentChangeTypes() a fin de determinar cómo cambió la ventana. Por ejemplo, el marco de trabajo puede detectar en qué momento un panel tiene un título nuevo o en qué momento un panel despareció.

Recopila detalles sobre el nodo de accesibilidad

Este paso es opcional, pero muy útil. La plataforma de Android permite que un AccessibilityService consulte la jerarquía de vistas y recopile información sobre el componente de IU que generó un evento, además de sus elementos superiores y secundarios. Para ello, asegúrate de establecer 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 muestra un objeto si la ventana en la que se originó el evento sigue activa. De lo contrario, mostrará un valor nulo y se comportará en consecuencia. El siguiente ejemplo es un fragmento de código que, cuando recibe un evento, hace lo siguiente:

  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, para informárselo al usuario, crea una string que indique la etiqueta y si estaba marcada o no.
  4. 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 fired 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 or not it's complete, based on
        // the text inside the label, and the state of the check-box.
        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 fired 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 or not it's complete, based on
        // the text inside the label, and the state of the check-box.
        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.

Información sobre la herramienta

Android 9 (API nivel 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

Android 8.0 (API nivel 26) 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.
  • Para acceder al texto de la sugerencia, usa getHintText(). Incluso si un objeto no muestra texto de la sugerencia actualmente, se realiza una llamada a getHintText() de manera correcta.

Ubicaciones de los caracteres de texto en la pantalla

En los dispositivos con Android 8.0 (API nivel 26) y versiones posteriores, los servicios de accesibilidad pueden determinar las coordenadas de pantalla del cuadro de límite 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 de límite 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 IU puede aceptar un rango de valores. Cuando crees un rango mediante RangeInfo.obtain() o cuando recuperes los valores extremos del rango mediante getMin() y getMax(), ten en cuenta que, en los dispositivos con Android 8.0 (API nivel 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 activó 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, utiliza los siguientes recursos:

Codelabs