Creare un servizio di accessibilità

Un servizio di accessibilità è un'app che migliora l'interfaccia utente per assistere gli utenti con disabilità o che potrebbero temporaneamente non essere in grado di interagire completamente con un dispositivo. Questi servizi vengono eseguiti in background e comunicano con il sistema per esaminare i contenuti dello schermo e interagire con le app per conto dell'utente. Alcuni esempi includono screen reader (come TalkBack), strumenti di Switch Access e sistemi di controllo vocale.

Questa guida illustra le nozioni di base per la creazione di un servizio di accessibilità Android.

Ciclo di vita del servizio di accessibilità

Per creare un servizio di accessibilità, devi estendere la classe AccessibilityService e dichiarare il servizio nel manifest della tua app.

Crea la classe di servizio

Crea una classe che estende AccessibilityService. Devi eseguire l'override dei seguenti metodi:

  • onAccessibilityEvent: chiamato quando il sistema rileva un evento che corrisponde alla configurazione del servizio (ad esempio, cambio di messa a fuoco o clic di un pulsante). È qui che il tuo servizio interpreta l'interfaccia utente.
  • onInterrupt: chiamato quando il sistema interrompe il feedback del servizio (ad esempio, per interrompere la sintesi vocale quando l'utente sposta rapidamente lo stato attivo).
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
    }
}

Dichiarazione nel manifest

Registra il tuo servizio nel file AndroidManifest.xml. Devi applicare rigorosamente l'autorizzazione BIND_ACCESSIBILITY_SERVICE in modo che solo il sistema possa associarsi al tuo servizio.

Per assicurarti che il pulsante delle impostazioni funzioni, dichiara 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>

Configurare il servizio

Crea un file di configurazione in res/xml/accessibility_service_config.xml. Questo file definisce gli eventi gestiti dal servizio e il feedback che fornisce. Assicurati di fare riferimento all'ServiceSettingsActivity che hai dichiarato nel manifest:

<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" />

Il file di configurazione include i seguenti attributi chiave:

  • android:accessibilityEventTypes: gli eventi che vuoi ricevere. Utilizza typeAllMask per un servizio generico.
  • android:canRetrieveWindowContent: deve essere true se il tuo servizio deve ispezionare la gerarchia della UI (ad esempio, per leggere il testo sullo schermo).
  • android:canPerformGestures: deve essere true se intendi inviare gesti (come scorrimenti o tocchi) in modo programmatico.
  • android:accessibilityFlags: combina i flag per attivare le funzionalità. flagRequestFingerprintGestures è necessario per i gesti con l'impronta. flagRequestAccessibilityButton è necessario per il pulsante di accessibilità del software.

Per un elenco completo delle opzioni di configurazione, vedi AccessibilityServiceInfo.

Configurazione di runtime

Sebbene la configurazione XML sia statica, puoi anche modificare dinamicamente la configurazione del servizio in fase di runtime. Ciò è utile per attivare/disattivare le funzionalità in base alle preferenze dell'utente.

Esegui l'override di onServiceConnected() per applicare gli aggiornamenti del runtime utilizzando 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)
}

Interpretare i contenuti dell'interfaccia utente

Quando viene attivato onAccessibilityEvent(), il sistema fornisce un AccessibilityEvent. Questo evento funge da punto di ingresso all'albero dell'accessibilità, una rappresentazione gerarchica dei contenuti dello schermo.

Il tuo servizio interagisce principalmente con AccessibilityNodeInfo oggetti, che rappresentano elementi della UI come pulsanti, elenchi e testo. I dati relativi a questi elementi della UI vengono normalizzati in AccessibilityNodeInfo.

Il seguente esempio mostra come recuperare l'origine di un evento e attraversare l'albero di accessibilità per trovare informazioni.

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
}

Agire per conto degli utenti

I servizi di accessibilità possono eseguire azioni, ad esempio fare clic su pulsanti o scorrere elenchi, per conto dell'utente.

Per eseguire un'azione, chiama performAction() su un oggetto AccessibilityNodeInfo.

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

Per le azioni globali che interessano l'intero sistema (come premere il pulsante Indietro o aprire l'area notifiche), utilizza performGlobalAction().

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

Gestisci il focus

Android ha due tipi distinti di messa a fuoco: messa a fuoco dell'input (dove va l'input da tastiera) e messa a fuoco dell'accessibilità (ciò che il servizio di accessibilità sta ispezionando).

Il seguente snippet mostra come trovare l'elemento che attualmente ha il focus di 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.
}

Il seguente snippet mostra come spostare lo stato attivo dell'accessibilità su un elemento specifico:

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

Quando crei un servizio di accessibilità, rispetta lo stato attivo dell'utente ed evita di rubare lo stato attivo, a meno che non venga attivato esplicitamente da un'azione dell'utente.

Eseguire gesti

Il tuo servizio può inviare gesti personalizzati allo schermo, come scorrimenti, tocchi o interazioni multitocco. Per farlo, dichiara android:canPerformGestures="true" nella configurazione in modo da poter utilizzare l'API dispatchGesture().

Gesti semplici

Per eseguire gesti semplici, inizia creando un oggetto Path per rappresentare il movimento associato a un determinato gesto. Poi, racchiudi Path in un GestureDescription per descrivere il tratto. Infine, chiama dispatchGesture per inviare il gesto.

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

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

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

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

Gesti continui

Per interazioni complesse (come disegnare una L o eseguire un trascinamento preciso in più passaggi), puoi concatenare i tratti utilizzando il parametro 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)
}

Gestione audio

Quando crei un servizio di accessibilità (in particolare uno screen reader), utilizza lo stream audio STREAM_ACCESSIBILITY. In questo modo gli utenti possono controllare il volume del servizio indipendentemente dal volume multimediale del sistema.

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

Assicurati di includere il flag FLAG_ENABLE_ACCESSIBILITY_VOLUME nella configurazione, in XML o tramite setServiceInfo in fase di runtime.

Funzionalità avanzate

Gesti con sensore di impronte

Sui dispositivi con Android 10 (livello API 29) o versioni successive, il tuo servizio può acquisire scorrimenti direzionali sul sensore di impronte digitali. Ciò è utile per fornire controlli di navigazione alternativi.

Aggiungi la seguente logica al metodo 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)
    }
}

Pulsante Accessibilità

Sui dispositivi che utilizzano i tasti di navigazione software, gli utenti possono richiamare il tuo servizio tramite un pulsante Accessibilità nella barra di navigazione.

Per utilizzare questa funzionalità, aggiungi il flag FLAG_REQUEST_ACCESSIBILITY_BUTTON alla configurazione del servizio. Quindi, aggiungi la logica di registrazione al metodo 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
            }
        }
    )
}

Sintesi vocale multilingue

Un servizio che legge il testo ad alta voce può cambiare automaticamente lingua se il testo di origine è contrassegnato con LocaleSpan. In questo modo, il servizio può pronunciare correttamente i contenuti in più lingue senza dover cambiare manualmente la lingua.

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
)

Quando il tuo servizio elabora AccessibilityNodeInfo, esamina la proprietà text per gli oggetti LocaleSpan per determinare la lingua di sintesi vocale corretta.

Risorse aggiuntive

Per saperne di più, consulta le seguenti risorse:

Guide

Codelab

Visualizza contenuti