Créer un service d'accessibilité

Un service d'accessibilité est une application qui améliore l'interface utilisateur afin d'aider les personnes ayant un handicap ou qui peuvent temporairement être dans l'incapacité d'interagir avec un appareil. Ces services s'exécutent en arrière-plan et communiquent avec le système pour inspecter le contenu de l'écran et interagir avec les applications au nom de l'utilisateur. Par exemple, les lecteurs d'écran (comme TalkBack), les outils Switch Access et les systèmes de commande vocale.

Ce guide présente les principes de base de la création d'un service d'accessibilité Android.

Cycle de vie du service d'accessibilité

Pour créer un service d'accessibilité, vous devez étendre la AccessibilityService classe et déclarer le service dans le fichier manifeste de votre application.

Créer la classe de service

Créez une classe qui étend AccessibilityService. Vous devez remplacer les méthodes suivantes :

  • onAccessibilityEvent: appelée lorsque le système détecte un événement correspondant à la configuration de votre service (par exemple, un changement de focus ou un clic sur un bouton). C'est là que votre service interprète l'interface utilisateur.
  • onInterrupt: appelée lorsque le système interrompt la rétroaction de votre service (par exemple, pour arrêter la sortie vocale lorsque l'utilisateur déplace rapidement le focus).
package com.example.android.apis.accessibility

import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.AccessibilityServiceInfo
import android.accessibilityservice.FingerprintGestureController
import android.accessibilityservice.AccessibilityButtonController
import android.accessibilityservice.GestureDescription
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.graphics.Path
import android.os.Build
import android.media.AudioManager
import android.content.Context

class MyAccessibilityService : AccessibilityService() {

    override fun onAccessibilityEvent(event: AccessibilityEvent) {
        // Interpret the event and provide feedback to the user
    }

    override fun onInterrupt() {
        // Interrupt any ongoing feedback
    }

    override fun onServiceConnected() {
        // Perform initialization here
    }
}

Déclarer dans le fichier manifeste

Enregistrez votre service dans le fichier AndroidManifest.xml. Vous devez appliquer strictement l' autorisation BIND_ACCESSIBILITY_SERVICE afin que seul le système puisse être lié à votre service.

Pour vous assurer que le bouton des paramètres fonctionne, déclarez ServiceSettingsActivity.

<application>
  <service android:name=".accessibility.MyAccessibilityService"
      android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
      android:exported="true"
      android:label="@string/accessibility_service_label">
      <intent-filter>
          <action android:name="android.accessibilityservice.AccessibilityService" />
      </intent-filter>
      <meta-data
          android:name="android.accessibilityservice"
          android:resource="@xml/accessibility_service_config" />
  </service>

  <activity android:name=".accessibility.ServiceSettingsActivity"
      android:exported="true"
      android:label="@string/accessibility_service_settings_label" />
</application>

Configurer le service

Créez un fichier de configuration dans res/xml/accessibility_service_config.xml. Ce fichier définit les événements gérés par votre service et les commentaires qu'il fournit. Veillez à référencer le ServiceSettingsActivity que vous avez déclaré dans votre fichier manifeste :

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

Le fichier de configuration inclut les attributs clés suivants :

  • android:accessibilityEventTypes: événements que vous souhaitez recevoir. Utilisez typeAllMask pour un service à usage général.
  • android:canRetrieveWindowContent: doit être true si votre service doit inspecter la hiérarchie de l'interface utilisateur (par exemple, pour lire du texte à l'écran).
  • android:canPerformGestures: doit être true si vous prévoyez de distribuer des gestes (comme des balayages ou des appuis) par programmation.
  • android:accessibilityFlags: combinez des flags pour activer des fonctionnalités. flagRequestFingerprintGestures est requis pour les gestes d'empreinte digitale. flagRequestAccessibilityButton est requis pour le bouton d'accessibilité du logiciel.

Pour obtenir la liste complète des options de configuration, consultez AccessibilityServiceInfo.

Configuration de l'environnement d'exécution

Bien que la configuration XML soit statique, vous pouvez également modifier la configuration de votre service de manière dynamique au moment de l'exécution. Cela est utile pour activer ou désactiver des fonctionnalités en fonction des préférences de l'utilisateur.

Remplacez onServiceConnected() pour appliquer les mises à jour d'exécution à l'aide de setServiceInfo() :

override fun onServiceConnected() {
    val info = AccessibilityServiceInfo()

    // Set the type of events that this service wants to listen to.
    info.eventTypes = AccessibilityEvent.TYPE_VIEW_CLICKED or AccessibilityEvent.TYPE_VIEW_FOCUSED

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

    // Set flags at runtime.
    info.flags = AccessibilityServiceInfo.FLAG_DEFAULT or
            AccessibilityServiceInfo.FLAG_REQUEST_FINGERPRINT_GESTURES

    this.setServiceInfo(info)
}

Interpréter le contenu de l'interface utilisateur

Lorsque onAccessibilityEvent() se déclenche, le système fournit un AccessibilityEvent. Cet événement sert de point d'entrée à l' arborescence d'accessibilité, une représentation hiérarchique du contenu de l'écran.

Votre service interagit principalement avec AccessibilityNodeInfo objets, qui représentent des éléments d'interface utilisateur tels que des boutons, des listes et du texte. Les données concernant ces éléments d'interface utilisateur sont normalisées dans AccessibilityNodeInfo.

L'exemple suivant montre comment récupérer la source d'un événement et parcourir l'arborescence d'accessibilité pour trouver des informations.

override fun onAccessibilityEvent(event: AccessibilityEvent) {
    // Get the source node of the event
    val sourceNode: AccessibilityNodeInfo? = event.source

    if (sourceNode == null) return

    // Inspect properties
    if (sourceNode.isCheckable) {
        val state = if (sourceNode.isChecked) "Checked" else "Unchecked"
        val label = sourceNode.text ?: sourceNode.contentDescription
        
        // Provide feedback (for example, speak to the user)
        speakToUser("$label is $state")
    }

    // Always recycle nodes to prevent memory leaks
    sourceNode.recycle()
}

private fun speakToUser(text: String) {
    // Your text-to-speech implementation goes here
}

Agir pour le compte des utilisateurs

Les services d'accessibilité peuvent effectuer des actions, telles que cliquer sur des boutons ou faire défiler des listes, au nom de l'utilisateur.

Pour effectuer une action, appelez performAction() sur un objet AccessibilityNodeInfo.

fun performClick(node: AccessibilityNodeInfo) {
    if (node.isClickable) {
        node.performAction(AccessibilityNodeInfo.ACTION_CLICK)
    }
}

Pour les actions globales qui affectent l'ensemble du système (comme appuyer sur le bouton "Retour" ou ouvrir le volet de notifications), utilisez performGlobalAction().

// Navigate back
fun navigateBack() {
    performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK)
}

Gérer le focus

Android comporte deux types de focus distincts : le focus d'entrée (où les entrées au clavier sont envoyées) et le focus d'accessibilité (ce que le service d'accessibilité inspecte).

L'extrait suivant montre comment trouver l'élément qui a actuellement le focus d'accessibilité :

// Find the node that currently has accessibility focus
// Note: rootInActiveWindow can be null if the window is not available
val root = rootInActiveWindow
if (root != null) {
    val focusedNode = root.findFocus(AccessibilityNodeInfo.FOCUS_ACCESSIBILITY)

    // Do something with focusedNode

    // Always recycle nodes
    focusedNode?.recycle()
    // rootInActiveWindow doesn't need to be recycled, but obtained nodes do.
}

L'extrait suivant montre comment déplacer le focus d'accessibilité vers un élément spécifique :

// Request that the system give focus to a given node
fun focusNode(node: AccessibilityNodeInfo) {
    node.performAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS)
}

Lorsque vous créez un service d'accessibilité, respectez l'état de focus de l'utilisateur et évitez de le voler, sauf s'il est explicitement déclenché par une action de l'utilisateur.

Effectuer des gestes

Votre service peut envoyer des gestes personnalisés à l'écran, tels que des balayages, des appuis ou des interactions multitouch. Pour ce faire, déclarez android:canPerformGestures="true" dans votre configuration afin de pouvoir utiliser l'API dispatchGesture().

Gestes simples

Pour effectuer des gestes simples, commencez par créer un objet Path pour représenter le mouvement associé à un geste donné. Ensuite, encapsulez le Path dans un GestureDescription pour décrire le trait. Enfin, appelez dispatchGesture pour envoyer le geste.

fun swipeRight() {
    // Create a path for the swipe (from x=100 to x=500)
    val swipePath = Path()
    swipePath.moveTo(100f, 500f)
    swipePath.lineTo(500f, 500f)

    // Build the stroke description (0ms delay, 500ms duration)
    val stroke = GestureDescription.StrokeDescription(swipePath, 0, 500)

    // Build the gesture description
    val gestureBuilder = GestureDescription.Builder()
    gestureBuilder.addStroke(stroke)

    // Dispatch the gesture
    dispatchGesture(gestureBuilder.build(), object : AccessibilityService.GestureResultCallback() {
        override fun onCompleted(gestureDescription: GestureDescription?) {
            super.onCompleted(gestureDescription)
            // Gesture finished successfully
        }
    }, null)
}

Gestes continus

Pour les interactions complexes (comme dessiner une forme en L ou effectuer un glissement précis en plusieurs étapes), vous pouvez enchaîner les traits à l'aide du paramètre willContinue.

fun performLShapedGesture() {
    val path1 = Path().apply {
        moveTo(200f, 200f)
        lineTo(400f, 200f)
    }

    val path2 = Path().apply {
        moveTo(400f, 200f)
        lineTo(400f, 400f)
    }

    // First stroke: willContinue = true
    val stroke1 = GestureDescription.StrokeDescription(path1, 0, 500, true)

    // Second stroke: continues immediately after stroke1
    val stroke2 = stroke1.continueStroke(path2, 0, 500, false)

    val builder = GestureDescription.Builder()
    builder.addStroke(stroke1)
    builder.addStroke(stroke2)

    dispatchGesture(builder.build(), null, null)
}

Gestion audio

Lorsque vous créez un service d'accessibilité (en particulier un lecteur d'écran), utilisez le flux audio STREAM_ACCESSIBILITY. Cela permet aux utilisateurs de contrôler le volume du service indépendamment du volume multimédia du système.

fun increaseAccessibilityVolume() {
    val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
    audioManager.adjustStreamVolume(
        AudioManager.STREAM_ACCESSIBILITY,
        AudioManager.ADJUST_RAISE,
        0
    )
}

Veillez à inclure le flag FLAG_ENABLE_ACCESSIBILITY_VOLUME dans votre configuration, au format XML ou via setServiceInfo au moment de l'exécution.

Fonctionnalités avancées

Gestes d'empreinte digitale

Sur les appareils exécutant Android 10 (niveau d'API 29) ou version ultérieure, votre service peut capturer les balayages directionnels sur le lecteur d'empreinte digitale. Cela est utile pour fournir d'autres commandes de navigation.

Ajoutez la logique suivante à votre méthode onServiceConnected() :

// Import: android.os.Build
// Import: android.accessibilityservice.FingerprintGestureController

private var gestureController: FingerprintGestureController? = null

override fun onServiceConnected() {
    // Check if the device is running Android 10 (Q) or higher
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        gestureController = fingerprintGestureController

        val callback = object : FingerprintGestureController.FingerprintGestureCallback() {
            override fun onGestureDetected(gesture: Int) {
                when (gesture) {
                    FingerprintGestureController.FINGERPRINT_GESTURE_SWIPE_DOWN -> {
                        // Handle swipe down
                    }
                    FingerprintGestureController.FINGERPRINT_GESTURE_SWIPE_UP -> {
                        // Handle swipe up
                    }
                }
            }
        }

        gestureController?.registerFingerprintGestureCallback(callback, null)
    }
}

Bouton Accessibilité

Sur les appareils utilisant des touches de navigation logicielles, les utilisateurs peuvent appeler votre service à l'aide d'un bouton d'accessibilité dans la barre de navigation.

Pour utiliser cette fonctionnalité, ajoutez le flag FLAG_REQUEST_ACCESSIBILITY_BUTTON à la configuration de votre service. Ajoutez ensuite la logique d'enregistrement à votre méthode onServiceConnected().

// Import: android.accessibilityservice.AccessibilityButtonController

override fun onServiceConnected() {
    // ... existing initialization code ...

    val controller = accessibilityButtonController

    controller.registerAccessibilityButtonCallback(
        object : AccessibilityButtonController.AccessibilityButtonCallback() {
            override fun onClicked(controller: AccessibilityButtonController) {
                // Respond to button tap
            }
        }
    )
}

Synthèse vocale multilingue

Un service qui lit du texte à voix haute peut changer automatiquement de langue si le texte source est balisé avec LocaleSpan. Votre service peut ainsi prononcer correctement le contenu multilingue sans avoir à changer manuellement de langue.

import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.style.LocaleSpan
import java.util.Locale

// Wrap text in LocaleSpan to indicate language
val spannable = SpannableStringBuilder("Bonjour")
spannable.setSpan(
    LocaleSpan(Locale.FRANCE),
    0,
    spannable.length,
    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)

Lorsque votre service traite AccessibilityNodeInfo, inspectez la propriété text pour les objets LocaleSpan afin de déterminer la langue de synthèse vocale appropriée.

Ressources supplémentaires

Pour en savoir plus, consultez les ressources suivantes :

Guides

Ateliers de programmation

Afficher le contenu