Criar um serviço de acessibilidade

Um serviço de acessibilidade é um app que melhora a interface do usuário para ajudar pessoas com deficiência ou que podem estar temporariamente impossibilitadas de interagir com um dispositivo. Esses serviços são executados em segundo plano e se comunicam com o sistema para inspecionar o conteúdo da tela e interagir com os apps em nome do usuário. Por exemplo, leitores de tela (como o TalkBack), ferramentas de acesso com interruptor e sistemas de controle de voz.

Este guia aborda os conceitos básicos da criação de um serviço de acessibilidade do Android.

Ciclo de vida do serviço de acessibilidade

Para criar um serviço de acessibilidade, é necessário estender a classe AccessibilityService e declarar o serviço no manifesto do app.

Criar a classe de serviço

Crie uma classe que estenda o AccessibilityService. Substitua os seguintes métodos:

  • onAccessibilityEvent: é chamado quando o sistema detecta um evento que corresponde à configuração do seu serviço (por exemplo, mudança de foco ou clique em um botão). É aqui que seu serviço interpreta a interface do usuário.
  • onInterrupt: chamado quando o sistema interrompe o feedback do seu serviço (por exemplo, para interromper a saída de voz quando o usuário move o foco rapidamente).
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
    }
}

Declarar no manifesto

Registre o serviço no arquivo AndroidManifest.xml. É necessário aplicar estritamente a permissão BIND_ACCESSIBILITY_SERVICE para que somente o sistema possa se vincular ao serviço.

Para garantir que o botão de configurações funcione, declare o 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>

Configurar o serviço

Crie um arquivo de configuração em res/xml/accessibility_service_config.xml. Esse arquivo define quais eventos seu serviço processa e qual feedback ele fornece. Não se esqueça de referenciar o ServiceSettingsActivity que você declarou no manifesto:

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

O arquivo de configuração inclui os seguintes atributos principais:

  • android:accessibilityEventTypes: os eventos que você quer receber. Use typeAllMask para um serviço de uso geral.
  • android:canRetrieveWindowContent: precisa ser true se o serviço precisar inspecionar a hierarquia da interface (por exemplo, para ler texto da tela).
  • android:canPerformGestures: precisa ser true se você pretende enviar gestos (como deslizar ou tocar) de forma programática.
  • android:accessibilityFlags: combine flags para ativar recursos. O flagRequestFingerprintGestures é necessário para gestos de impressão digital. O flagRequestAccessibilityButton é necessário para o botão de acessibilidade do software.

Para uma lista completa de opções de configuração, consulte AccessibilityServiceInfo.

Configuração do ambiente de execução

Embora a configuração XML seja estática, também é possível modificar a configuração do serviço dinamicamente no tempo de execução. Isso é útil para ativar/desativar recursos com base nas preferências do usuário.

Substitua onServiceConnected() para aplicar atualizações de ambiente de execução usando 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)
}

Interpretar conteúdo da interface

Quando onAccessibilityEvent() é acionado, o sistema fornece um AccessibilityEvent. Esse evento funciona como o ponto de entrada para a árvore de acessibilidade, uma representação hierárquica do conteúdo da tela.

Seu serviço interage principalmente com objetos AccessibilityNodeInfo que representam elementos da interface, como botões, listas e texto. Os dados sobre esses elementos de interface são normalizados em AccessibilityNodeInfo.

O exemplo a seguir mostra como recuperar a origem de um evento e percorrer a árvore de acessibilidade para encontrar informações.

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 em nome dos usuários

Os serviços de acessibilidade podem realizar ações em nome do usuário, como clicar em botões ou rolar listas.

Para realizar uma ação, chame performAction() em um objeto AccessibilityNodeInfo.

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

Para ações globais que afetam todo o sistema (como pressionar o botão "Voltar" ou abrir a bandeja de notificações), use performGlobalAction().

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

Gerenciar foco

O Android tem dois tipos distintos de foco: o foco de entrada (onde a entrada do teclado é direcionada) e o foco de acessibilidade (o que o serviço de acessibilidade está inspecionando).

O snippet a seguir mostra como encontrar o elemento que tem o foco de acessibilidade no momento:

// 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.
}

O snippet a seguir mostra como mover o foco de acessibilidade para um elemento específico:

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

Ao criar um serviço de acessibilidade, respeite o estado de foco do usuário e evite roubar o foco, a menos que seja explicitamente acionado por uma ação do usuário.

Fazer gestos

Seu serviço pode enviar gestos personalizados para a tela, como deslizar, tocar ou interações multitoque. Para isso, declare android:canPerformGestures="true" na configuração para usar a API dispatchGesture().

Gestos simples

Para realizar gestos simples, comece criando um objeto Path para representar o movimento associado a um determinado gesto. Em seguida, envolva o Path em um GestureDescription para descrever o traço. Por fim, chame dispatchGesture para enviar o 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)
}

Gestos contínuos

Para interações complexas (como desenhar um formato de L ou realizar um arrasto preciso de várias etapas), é possível encadear traços usando o parâmetro 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)
}

Gerenciamento de áudio

Ao criar um serviço de acessibilidade (principalmente um leitor de tela), use o stream de áudio STREAM_ACCESSIBILITY. Isso permite que os usuários controlem o volume do serviço independente do volume de mídia do sistema.

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

Inclua a flag FLAG_ENABLE_ACCESSIBILITY_VOLUME na sua configuração, em XML ou usando setServiceInfo no tempo de execução.

Recursos avançados

Gestos de impressão digital

Em dispositivos com o Android 10 (nível 29 da API) ou mais recente, seu serviço pode capturar deslizamentos direcionais no sensor de impressão digital. Isso é útil para fornecer controles de navegação alternativos.

Adicione a seguinte lógica ao método 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)
    }
}

Botão de acessibilidade

Em dispositivos que usam teclas de navegação de software, os usuários podem invocar seu serviço usando um botão de acessibilidade na barra de navegação.

Para usar esse recurso, adicione a flag FLAG_REQUEST_ACCESSIBILITY_BUTTON às configurações de serviço. Em seguida, adicione a lógica de registro ao método 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
            }
        }
    )
}

Conversão de texto em voz multilíngue

Um serviço que lê texto em voz alta pode mudar de idioma automaticamente se o texto de origem estiver marcado com LocaleSpan. Isso permite que o serviço pronuncie corretamente conteúdo em vários idiomas sem troca manual.

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 o serviço processa AccessibilityNodeInfo, inspecione a propriedade text para objetos LocaleSpan e determine o idioma correto de conversão de texto em voz.

Outros recursos

Para saber mais, consulte os seguintes recursos:

Guias

Codelabs

Visualiza conteúdo