Criar seu próprio serviço de acessibilidade

Um serviço de acessibilidade é um app que melhora a interface do usuário para auxiliar usuários com deficiências ou que podem não conseguir interagir totalmente com um dispositivo temporariamente. Por exemplo, usuários que estão dirigindo, cuidando de uma criança ou participando de uma festa muito barulhenta podem precisar de feedback extra ou alternativo da interface.

O Android oferece serviços de acessibilidade padrão, incluindo o TalkBack, e os desenvolvedores podem criar e distribuir os próprios serviços. Este documento explica os conceitos básicos da criação de um serviço de acessibilidade.

Um serviço de acessibilidade pode ser empacotado com um app normal ou criado como um projeto Android independente. As etapas para criar o serviço são as mesmas em qualquer situação.

Criar um serviço de acessibilidade

No projeto, crie uma classe que estenda 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() {
    }

...
}

Se você criar um novo projeto para essa Service e não pretende ter um app associado a ele, remova a classe inicial Activity da origem.

Declarações e permissões do manifesto

Os apps que oferecem serviços de acessibilidade precisam incluir declarações específicas nos manifestos do app para serem tratados como esse tipo de serviço pelo sistema Android. Esta seção explica as configurações obrigatórias e opcionais para serviços de acessibilidade.

Declaração de serviço de acessibilidade

Para que o app seja tratado como um serviço de acessibilidade, inclua um elemento service, em vez do elemento activity, no elemento application do manifesto. Além disso, no elemento service, inclua um filtro de intent de serviço de acessibilidade. O manifesto também precisa proteger o serviço, adicionando a permissão BIND_ACCESSIBILITY_SERVICE para garantir que somente o sistema possa se vincular a ele. Veja um exemplo:

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

Configuração do serviço de acessibilidade

Os serviços de acessibilidade precisam fornecer uma configuração que especifique os tipos de eventos de acessibilidade que o serviço processa e outras informações sobre o serviço. A configuração de um serviço de acessibilidade está contida na classe AccessibilityServiceInfo. Seu serviço pode criar e definir uma configuração usando uma instância dessa classe e setServiceInfo() no momento da execução. No entanto, nem todas as opções de configuração estão disponíveis com esse método.

Você pode incluir um elemento <meta-data> no manifesto com uma referência a um arquivo de configuração, que permite definir todas as opções do serviço de acessibilidade, conforme mostrado no exemplo abaixo.

<service android:name=".MyAccessibilityService">
  ...
  <meta-data
    android:name="android.accessibilityservice"
    android:resource="@xml/accessibility_service_config" />
</service>

Esse elemento <meta-data> se refere a um arquivo XML criado no diretório de recursos do app: <project_dir>/res/xml/accessibility_service_config.xml>. O código a seguir mostra um exemplo do conteúdo do arquivo de configuração de serviço:

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

Para mais informações sobre os atributos XML que podem ser usados no arquivo de configuração do serviço de acessibilidade, consulte a seguinte documentação de referência:

Para mais informações sobre quais configurações podem ser definidas dinamicamente durante a execução, consulte a documentação de referência AccessibilityServiceInfo.

Configurar o serviço de acessibilidade

Considere o seguinte ao definir as variáveis de configuração do serviço de acessibilidade para informar ao sistema como e quando executar:

  • A quais tipos de evento você quer que ele responda?
  • O serviço precisa estar ativo para todos os apps ou apenas para nomes de pacotes específicos?
  • Que tipos diferentes de feedback ele usa?

Você tem duas opções para configurar essas variáveis. A opção compatível com versões anteriores é defini-los no código usando setServiceInfo(android.accessibilityservice.AccessibilityServiceInfo). Para fazer isso, substitua o método onServiceConnected() e configure seu serviço lá, conforme mostrado no exemplo a seguir.

Kotlin

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

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

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

        // Default services are invoked only if no package-specific services are
        // present for the type of AccessibilityEvent generated. This service is
        // app-specific, so the flag isn't necessary. For a general-purpose
        // service, consider setting the DEFAULT flag.

        // flags = AccessibilityServiceInfo.DEFAULT;

        notificationTimeout = 100
    }

    this.serviceInfo = info

}

Java

@Override
public void onServiceConnected() {
    // Set the type of events that this service wants to listen to. Others
    // aren't passed to this service.
    info.eventTypes = AccessibilityEvent.TYPE_VIEW_CLICKED |
            AccessibilityEvent.TYPE_VIEW_FOCUSED;

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

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

    // Default services are invoked only if no package-specific services are
    // present for the type of AccessibilityEvent generated. This service is
    // app-specific, so the flag isn't necessary. For a general-purpose service,
    // consider setting the DEFAULT flag.

    // info.flags = AccessibilityServiceInfo.DEFAULT;

    info.notificationTimeout = 100;

    this.setServiceInfo(info);

}

A segunda opção é configurar o serviço usando um arquivo XML. Algumas opções de configuração, como canRetrieveWindowContent, estão disponíveis apenas se você configurar seu serviço usando XML. As opções de configuração do exemplo anterior ficam assim quando definidas com XML:

<accessibility-service
     android:accessibilityEventTypes="typeViewClicked|typeViewFocused"
     android:packageNames="com.example.android.myFirstApp, com.example.android.mySecondApp"
     android:accessibilityFeedbackType="feedbackSpoken"
     android:notificationTimeout="100"
     android:settingsActivity="com.example.android.apis.accessibility.TestBackActivity"
     android:canRetrieveWindowContent="true"
/>

Se você usa XML, faça referência a ele no manifesto adicionando uma tag <meta-data> à declaração de serviço apontando para o arquivo XML. Se você armazenar o arquivo XML em res/xml/serviceconfig.xml, a nova tag vai ficar assim:

<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 do serviço de acessibilidade

Um serviço de acessibilidade precisa estender a classe AccessibilityService e substituir os seguintes métodos dessa classe. Esses métodos são apresentados na ordem em que o sistema Android os chama: desde quando o serviço é iniciado (onServiceConnected()), até enquanto está em execução (onAccessibilityEvent(), onInterrupt()), até quando ele é encerrado (onUnbind()).

  • onServiceConnected(): (opcional) o sistema chama esse método quando se conecta ao serviço de acessibilidade. Use esse método para seguir etapas únicas de configuração do seu serviço, incluindo a conexão com os serviços do sistema de feedback do usuário, como o gerenciador de áudio ou a vibração do dispositivo. Se você quiser definir a configuração do serviço durante a execução ou fazer ajustes únicos, esse é um local conveniente para chamar setServiceInfo().

  • onAccessibilityEvent(): (obrigatório) o sistema chama esse método de volta ao detectar uma AccessibilityEvent que corresponde aos parâmetros de filtragem de eventos especificados pelo serviço de acessibilidade, como quando o usuário toca em um botão ou foca em um controle da interface do usuário em um app para o qual esse serviço está fornecendo feedback. Quando o sistema chama esse método, ele transmite o AccessibilityEvent associado, que o serviço pode interpretar e usar para fornecer feedback ao usuário. Esse método pode ser chamado várias vezes durante o ciclo de vida do serviço.

  • onInterrupt(): (obrigatório) o sistema chama esse método quando o sistema quer interromper o feedback que seu serviço está fornecendo, geralmente em resposta a uma ação do usuário, como mover o foco para um controle diferente. Esse método pode ser chamado várias vezes durante o ciclo de vida do serviço.

  • onUnbind(): (opcional) o sistema chama esse método quando está prestes a encerrar o serviço de acessibilidade. Use esse método para realizar qualquer procedimento de desligamento único, incluindo a desalocação dos serviços do sistema de feedback do usuário, como o gerenciador de áudio ou a vibração do dispositivo.

Esses métodos de callback fornecem a estrutura básica do serviço de acessibilidade. Você pode decidir como processar dados fornecidos pelo sistema Android na forma de objetos AccessibilityEvent e fornecer feedback ao usuário. Para mais informações sobre como acessar informações de um evento de acessibilidade, consulte Ver detalhes do evento.

Registrar-se nos eventos de acessibilidade

Uma das funções mais importantes dos parâmetros de configuração do serviço de acessibilidade é permitir que você especifique os tipos de eventos de acessibilidade que seu serviço pode processar. A especificação dessas informações permite que os serviços de acessibilidade cooperem entre si e oferece flexibilidade para processar apenas tipos de evento específicos de apps específicos. A filtragem de eventos pode incluir os seguintes critérios:

  • Nomes de pacotes:especificam os nomes dos pacotes dos apps com eventos de acessibilidade que você quer que o serviço processe. Se esse parâmetro for omitido, o serviço de acessibilidade será considerado disponível para eventos de acessibilidade de serviços de qualquer app. Você pode definir esse parâmetro nos arquivos de configuração do serviço de acessibilidade com o atributo android:packageNames como uma lista separada por vírgulas ou usar o membro AccessibilityServiceInfo.packageNames.

  • Tipos de evento:especificam os tipos de eventos de acessibilidade que você quer que o serviço processe. É possível definir esse parâmetro nos arquivos de configuração do serviço de acessibilidade com o atributo android:accessibilityEventTypes como uma lista separada pelo caractere |. Por exemplo: accessibilityEventTypes="typeViewClicked|typeViewFocused". Ou é possível configurá-lo usando o membro AccessibilityServiceInfo.eventTypes.

Ao configurar o serviço de acessibilidade, considere cuidadosamente os eventos que ele pode processar e registre-se apenas para esses eventos. Como os usuários podem ativar mais de um serviço de acessibilidade ao mesmo tempo, seu serviço não pode consumir eventos que não pode processar. Lembre-se de que outros serviços podem processar esses eventos para melhorar a experiência do usuário.

Volume da acessibilidade

Dispositivos com o Android 8.0 (nível 26 da API) e versões mais recentes incluem a categoria de volume STREAM_ACCESSIBILITY, que permite controlar o volume da saída de áudio do serviço de acessibilidade, independentemente de outros sons no dispositivo.

Os serviços de acessibilidade podem usar esse tipo de stream configurando a opção FLAG_ENABLE_ACCESSIBILITY_VOLUME. É possível mudar o volume do áudio de acessibilidade do dispositivo chamando o método adjustStreamVolume() na instância de AudioManager do dispositivo.

O snippet de código a seguir demonstra como um serviço de acessibilidade pode usar a categoria de volume 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);
        }
    }
}

Para saber mais, assista o vídeo da sessão Novidades de acessibilidade do Android (em inglês) do Google I/O 2017, a partir de 6:35.

Atalho de acessibilidade

Em dispositivos com o Android 8.0 (nível 26 da API) ou versões mais recentes, os usuários podem ativar e desativar o serviço de acessibilidade que preferirem em qualquer tela, basta tocar e manter as duas teclas de volume pressionadas ao mesmo tempo. Embora esse atalho ative e desative o TalkBack por padrão, os usuários podem configurar o botão para ativar e desativar qualquer serviço instalado no dispositivo.

Para que os usuários acessem um serviço específico pelo atalho, o serviço precisa solicitar o recurso durante a execução.

Para saber mais, assista ao vídeo da sessão Novidades na acessibilidade do Android (em inglês) do Google I/O 2017, a partir de 13:25.

Botão de acessibilidade

Em dispositivos que usam uma área de navegação renderizada por software e executam o Android 8.0 (API de nível 26) ou mais recente, o lado direito da barra de navegação inclui um botão de acessibilidade. Quando os usuários pressionam esse botão, eles podem invocar um dos vários recursos e serviços de acessibilidade ativados, dependendo do conteúdo mostrado na tela no momento.

Para permitir que os usuários invoquem determinado serviço usando o botão de acessibilidade, o serviço precisa adicionar a flag FLAG_REQUEST_ACCESSIBILITY_BUTTON ao atributo android:accessibilityFlags de um objeto AccessibilityServiceInfo. O serviço pode registrar callbacks usando registerAccessibilityButtonCallback().

O snippet de código a seguir demonstra como configurar um serviço de acessibilidade para responder ao usuário que pressiona o botão de acessibilidade.

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);
    }
}

Para saber mais, assista o vídeo da sessão Novidades de acessibilidade do Android (em inglês) do Google I/O 2017, a partir de 16:28.

Gestos de impressão digital

Os serviços de acessibilidade em dispositivos com o Android 8.0 (nível 26 da API) ou versões mais recentes podem responder a deslizes direcionais (para cima, para baixo, para a esquerda e para a direita) no sensor de impressão digital de um dispositivo. Para configurar um serviço para receber callbacks sobre essas interações, siga esta sequência:

  1. Declare a permissão USE_BIOMETRIC e o recurso CAPABILITY_CAN_REQUEST_FINGERPRINT_GESTURES.
  2. Defina a flag FLAG_REQUEST_FINGERPRINT_GESTURES no atributo android:accessibilityFlags.
  3. Registre-se para callbacks usando registerFingerprintGestureCallback().

Lembre-se de que nem todos os dispositivos têm sensores de impressão digital. Para identificar se um dispositivo oferece suporte ao sensor, use o método isHardwareDetected(). Mesmo em um dispositivo que tenha um sensor de impressão digital, seu serviço não poderá usar o sensor quando ele estiver sendo usado para fins de autenticação. Para identificar quando o sensor está disponível, chame o método isGestureDetectionAvailable() e implemente o callback onGestureDetectionAvailabilityChanged().

O snippet de código a seguir mostra um exemplo de como usar gestos de impressão digital para navegar em um tabuleiro de jogo virtual:

// AndroidManifest.xml
<manifest ... >
    <uses-permission android:name="android.permission.USE_FINGERPRINT" />
    ...
    <application>
        <service android:name="com.example.MyFingerprintGestureService" ... >
            <meta-data
                android:name="android.accessibilityservice"
                android:resource="@xml/myfingerprintgestureservice" />
        </service>
    </application>
</manifest>
// myfingerprintgestureservice.xml
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    ...
    android:accessibilityFlags=" ... |flagRequestFingerprintGestures"
    android:canRequestFingerprintGestures="true"
    ... />

Kotlin

// MyFingerprintGestureService.kt
import android.accessibilityservice.FingerprintGestureController.*

class MyFingerprintGestureService : AccessibilityService() {

    private var gestureController: FingerprintGestureController? = null
    private var fingerprintGestureCallback:
            FingerprintGestureController.FingerprintGestureCallback? = null
    private var mIsGestureDetectionAvailable: Boolean = false

    override fun onCreate() {
        gestureController = fingerprintGestureController
        mIsGestureDetectionAvailable = gestureController?.isGestureDetectionAvailable ?: false
    }

    override fun onServiceConnected() {
        if (mFingerprintGestureCallback != null || !mIsGestureDetectionAvailable) return

        fingerprintGestureCallback =
                object : FingerprintGestureController.FingerprintGestureCallback() {
                    override fun onGestureDetected(gesture: Int) {
                        when (gesture) {
                            FINGERPRINT_GESTURE_SWIPE_DOWN -> moveGameCursorDown()
                            FINGERPRINT_GESTURE_SWIPE_LEFT -> moveGameCursorLeft()
                            FINGERPRINT_GESTURE_SWIPE_RIGHT -> moveGameCursorRight()
                            FINGERPRINT_GESTURE_SWIPE_UP -> moveGameCursorUp()
                            else -> Log.e(MY_APP_TAG, "Error: Unknown gesture type detected!")
                        }
                    }

                    override fun onGestureDetectionAvailabilityChanged(available: Boolean) {
                        mIsGestureDetectionAvailable = available
                    }
                }

        fingerprintGestureCallback?.also {
            gestureController?.registerFingerprintGestureCallback(it, null)
        }
    }
}

Java

// MyFingerprintGestureService.java
import static android.accessibilityservice.FingerprintGestureController.*;

public class MyFingerprintGestureService extends AccessibilityService {
    private FingerprintGestureController gestureController;
    private FingerprintGestureController
            .FingerprintGestureCallback fingerprintGestureCallback;
    private boolean mIsGestureDetectionAvailable;

    @Override
    public void onCreate() {
        gestureController = getFingerprintGestureController();
        mIsGestureDetectionAvailable =
                gestureController.isGestureDetectionAvailable();
    }

    @Override
    protected void onServiceConnected() {
        if (fingerprintGestureCallback != null
                || !mIsGestureDetectionAvailable) {
            return;
        }

        fingerprintGestureCallback =
               new FingerprintGestureController.FingerprintGestureCallback() {
            @Override
            public void onGestureDetected(int gesture) {
                switch (gesture) {
                    case FINGERPRINT_GESTURE_SWIPE_DOWN:
                        moveGameCursorDown();
                        break;
                    case FINGERPRINT_GESTURE_SWIPE_LEFT:
                        moveGameCursorLeft();
                        break;
                    case FINGERPRINT_GESTURE_SWIPE_RIGHT:
                        moveGameCursorRight();
                        break;
                    case FINGERPRINT_GESTURE_SWIPE_UP:
                        moveGameCursorUp();
                        break;
                    default:
                        Log.e(MY_APP_TAG,
                                  "Error: Unknown gesture type detected!");
                        break;
                }
            }

            @Override
            public void onGestureDetectionAvailabilityChanged(boolean available) {
                mIsGestureDetectionAvailable = available;
            }
        };

        if (fingerprintGestureCallback != null) {
            gestureController.registerFingerprintGestureCallback(
                    fingerprintGestureCallback, null);
        }
    }
}

Para saber mais, assista o vídeo da sessão Novidades de acessibilidade do Android (em inglês) no Google I/O 2017, a partir de 9:03.

Conversão de texto em voz multilíngue

No Android 8.0 (nível 26 da API) e versões mais recentes, o serviço de conversão de texto em voz (TTS) do Android pode identificar e falar frases em vários idiomas em um único bloco de texto. Para ativar esse recurso automático de troca de idioma em um serviço de acessibilidade, envolva todas as strings em objetos LocaleSpan, conforme mostrado neste snippet 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 saber mais, assista o vídeo da sessão Novidades de acessibilidade do Android (em inglês) do Google I/O 2017, a partir das 10:59.

Agir em nome dos usuários

Desde 2011, os serviços de acessibilidade podem agir em nome dos usuários, incluindo a alteração do foco de entrada e a seleção (ativa) de elementos da interface do usuário. Em 2012, o intervalo de ações foi ampliado para incluir listas de rolagem e interação com campos de texto. Os serviços de acessibilidade também podem realizar ações globais, como navegar até a tela inicial, pressionar o botão "Voltar" e abrir a tela de notificações e a lista de apps recentes. Desde 2012, o Android inclui o foco em acessibilidade, o que torna todos os elementos visíveis selecionáveis por um serviço de acessibilidade.

Esses recursos permitem que os desenvolvedores de serviços de acessibilidade criem modos de navegação alternativos, como a navegação por gestos, e ofereçam aos usuários com deficiência um controle aprimorado dos dispositivos Android.

Ouvir gestos

Os serviços de acessibilidade podem detectar gestos específicos e responder agindo em nome de um usuário. Esse recurso exige que o serviço de acessibilidade solicite a ativação do recurso "Explorar por toque". O serviço pode solicitar essa ativação configurando o membro flags da instância AccessibilityServiceInfo do serviço como FLAG_REQUEST_TOUCH_EXPLORATION_MODE, conforme mostrado no exemplo a seguir.

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

Depois que o serviço solicitar a ativação do Explorar por toque, o usuário precisará permitir que o recurso seja ativado, se ele ainda não estiver ativo. Quando esse recurso está ativo, o serviço recebe notificações de gestos de acessibilidade pelo método de callback onGesture() e pode responder agindo em nome do usuário.

Gestos contínuos

Os dispositivos que executam o Android 8.0 (nível 26 da API) oferecem suporte a gestos contínuos ou gestos programáticos que contêm mais de um objeto Path.

Ao especificar uma sequência de traços, é possível especificar que eles pertencem ao mesmo gesto programático usando o argumento final willContinue no construtor GestureDescription.StrokeDescription, conforme mostrado no snippet de código a seguir:

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);
}

Para saber mais, assista o vídeo da sessão Novidades de acessibilidade do Android (em inglês) do Google I/O 2017, a partir de 15:47.

Usar ações de acessibilidade

Os serviços de acessibilidade podem agir em nome dos usuários para simplificar as interações com apps e serem mais produtivos. A capacidade dos serviços de acessibilidade de executar ações foi adicionada em 2011 e ampliada significativamente em 2012.

Para agir em nome dos usuários, o serviço de acessibilidade precisa se registrar para receber eventos de apps e solicitar permissão para ver o conteúdo de apps definindo android:canRetrieveWindowContent como true no arquivo de configuração de serviço. Quando os eventos são recebidos pelo serviço, ele pode recuperar o objeto AccessibilityNodeInfo usando getSource(). Com o objeto AccessibilityNodeInfo, seu serviço pode explorar a hierarquia de visualização para determinar qual ação realizar e, em seguida, agir para o usuário usando performAction().

Kotlin

class MyAccessibilityService : AccessibilityService() {

    override fun onAccessibilityEvent(event: AccessibilityEvent) {
        // Get the source node of the event.
        event.source?.apply {

            // Use the event and node information to determine what action to
            // take.

            // Act on behalf of the user.
            performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)

            // Recycle the nodeInfo object.
            recycle()
        }
    }
    ...
}

Java

public class MyAccessibilityService extends AccessibilityService {

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        // Get the source node of the event.
        AccessibilityNodeInfo nodeInfo = event.getSource();

        // Use the event and node information to determine what action to take.

        // Act on behalf of the user.
        nodeInfo.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);

        // Recycle the nodeInfo object.
        nodeInfo.recycle();
    }
    ...
}

O método performAction() permite que o serviço realize ações em um app. Se o serviço precisar executar uma ação global, como navegar até a tela inicial, tocar no botão "Voltar" ou abrir a tela de notificações ou a lista de apps recentes, use o método performGlobalAction().

Usar tipos de foco

Em 2012, o Android lançou um foco na interface do usuário chamado foco em acessibilidade. Os serviços de acessibilidade podem usar esse foco para selecionar qualquer elemento visível da interface do usuário e realizar ações nele. Esse tipo de foco é diferente do foco de entrada, que determina qual elemento da interface do usuário na tela recebe entrada quando o usuário digita caracteres, pressiona Enter em um teclado ou o botão central de um botão direcional.

É possível que um elemento em uma interface do usuário tenha foco de entrada enquanto outro tem foco de acessibilidade. O objetivo do foco da acessibilidade é fornecer aos serviços um método de interação com elementos visíveis em uma tela, independente de o elemento ser focalizável de entrada do ponto de vista do sistema. Para garantir que o serviço de acessibilidade interaja corretamente com os elementos de entrada dos apps, siga as diretrizes para testar a acessibilidade de um app e testar o serviço durante o uso de um app comum.

Um serviço de acessibilidade pode determinar qual elemento da interface do usuário tem foco de entrada ou de acessibilidade usando o método AccessibilityNodeInfo.findFocus(). Também é possível pesquisar elementos que podem ser selecionados com foco de entrada usando o método focusSearch(). Por fim, o serviço de acessibilidade pode definir o foco usando o método performAction(AccessibilityNodeInfo.ACTION_SET_ACCESSIBILITY_FOCUS).

Coletar informações

Os serviços de acessibilidade têm métodos padrão de coleta e representação de unidades-chave de informações fornecidas pelo usuário, como detalhes do evento, texto e números.

Ver detalhes de mudanças de janelas

O Android 9 (nível 28 da API) e versões mais recentes permitem que os apps acompanhem as atualizações de janelas quando um app redesenha várias janelas simultaneamente. Quando um evento TYPE_WINDOWS_CHANGED ocorrer, use a API getWindowChanges() para determinar como as janelas mudam. Durante uma atualização de várias janelas, cada janela produz o próprio conjunto de eventos. O método getSource() retorna a visualização raiz da janela associada a cada evento.

Se um app definir títulos do painel de acessibilidade para os objetos View, o serviço poderá reconhecer quando a IU do app for atualizada. Quando ocorrer um evento TYPE_WINDOW_STATE_CHANGED, use os tipos retornados por getContentChangeTypes() para determinar como a janela muda. Por exemplo, o framework pode detectar quando um painel tem um novo título ou quando ele desaparece.

Ver detalhes de evento

O Android fornece informações para serviços de acessibilidade sobre a interação da interface do usuário por meio de objetos AccessibilityEvent. Nas versões anteriores do Android, as informações disponíveis em um evento de acessibilidade, embora ofereciam detalhes significativos sobre o controle da interface do usuário selecionado pelos usuários, ofereciam informações contextuais limitadas. Em muitos casos, essas informações de contexto ausentes podem ser fundamentais para entender o significado do controle selecionado.

Um exemplo de interface em que o contexto é essencial é um calendário ou planejamento diário. Se o usuário selecionar um horário às 16h em uma lista de segunda a sexta-feira e o serviço de acessibilidade anunciar "16h", mas não anunciar o nome do dia da semana, o dia do mês ou o nome do mês, o feedback resultante será confuso. Nesse caso, o contexto de um controle de interface é fundamental para um usuário que queira agendar uma reunião.

Desde 2011, o Android estende significativamente a quantidade de informações que um serviço de acessibilidade pode acessar sobre uma interação da interface do usuário compondo eventos de acessibilidade com base na hierarquia de visualização. Uma hierarquia de visualização é o conjunto de componentes da interface do usuário que contém o componente (os pais) e os elementos da interface do usuário que podem estar contidos por esse componente (os filhos). Dessa forma, o Android pode fornecer mais detalhes sobre os eventos de acessibilidade, permitindo que os serviços ofereçam feedback mais útil aos usuários.

Um serviço de acessibilidade recebe informações sobre um evento da interface do usuário por meio de um AccessibilityEvent transmitido pelo sistema para o método de callback onAccessibilityEvent() do serviço. Esse objeto apresenta detalhes sobre o evento, incluindo o tipo de objeto usado, o texto descritivo dele e outros detalhes.

  • AccessibilityEvent.getRecordCount() e getRecord(int): esses métodos permitem extrair o conjunto de objetos AccessibilityRecord que contribuem para o AccessibilityEvent transmitido a você pelo sistema. Esse nível de detalhamento fornece mais contexto para o evento que aciona seu serviço de acessibilidade.

  • AccessibilityRecord.getSource(): esse método retorna um objeto AccessibilityNodeInfo. Esse objeto permite solicitar a hierarquia de layout de visualização (pais e filhos) do componente que origem do evento de acessibilidade. Esse recurso permite que um serviço de acessibilidade investigue o contexto completo de um evento, incluindo o conteúdo e o estado de todas as visualizações filhas ou contidas.

A plataforma Android oferece a capacidade de um AccessibilityService consultar a hierarquia de visualização, coletando informações sobre o componente de IU que gera um evento, bem como o pai e os filhos dele. Para fazer isso, defina a seguinte linha na sua configuração XML:

android:canRetrieveWindowContent="true"

Depois disso, gere um objeto AccessibilityNodeInfo usando getSource(). Essa chamada só retornará um objeto se a janela de origem do evento ainda for a janela ativa. Caso contrário, ele retorna nulo, portanto, comporte-se de acordo.

No exemplo a seguir, o código faz o seguinte quando um evento é recebido:

  1. Pega imediatamente o pai da visualização em que o evento se origina.
  2. Nessa visualização, um rótulo e uma caixa de seleção são procurados como visualizações filhas.
  3. Se forem encontrados, será criada uma string para informar ao usuário, indicando o rótulo e se ele foi verificado.

Se, a qualquer momento, um valor nulo for retornado ao percorrer a hierarquia de visualização, o método desistirá silenciosamente.

Kotlin

// Alternative onAccessibilityEvent that uses AccessibilityNodeInfo.

override fun onAccessibilityEvent(event: AccessibilityEvent) {

    val source: AccessibilityNodeInfo = event.source ?: return

    // Grab the parent of the view that fires the event.
    val rowNode: AccessibilityNodeInfo = getListItemNodeInfo(source) ?: return

    // Using this parent, get references to both child nodes, the label, and the
    // checkbox.
    val taskLabel: CharSequence = rowNode.getChild(0)?.text ?: run {
        rowNode.recycle()
        return
    }

    val isComplete: Boolean = rowNode.getChild(1)?.isChecked ?: run {
        rowNode.recycle()
        return
    }

    // Determine what the task is and whether it's complete based on the text
    // inside the label, and the state of the checkbox.
    if (rowNode.childCount < 2 || !rowNode.getChild(1).isCheckable) {
        rowNode.recycle()
        return
    }

    val completeStr: String = if (isComplete) {
        getString(R.string.checked)
    } else {
        getString(R.string.not_checked)
    }
    val reportStr = "$taskLabel$completeStr"
    speakToUser(reportStr)
}

Java

// Alternative onAccessibilityEvent that uses AccessibilityNodeInfo.

@Override
public void onAccessibilityEvent(AccessibilityEvent event) {

    AccessibilityNodeInfo source = event.getSource();
    if (source == null) {
        return;
    }

    // Grab the parent of the view that fires the event.
    AccessibilityNodeInfo rowNode = getListItemNodeInfo(source);
    if (rowNode == null) {
        return;
    }

    // Using this parent, get references to both child nodes, the label, and the
    // checkbox.
    AccessibilityNodeInfo labelNode = rowNode.getChild(0);
    if (labelNode == null) {
        rowNode.recycle();
        return;
    }

    AccessibilityNodeInfo completeNode = rowNode.getChild(1);
    if (completeNode == null) {
        rowNode.recycle();
        return;
    }

    // Determine what the task is and whether it's complete based on the text
    // inside the label, and the state of the checkbox.
    if (rowNode.getChildCount() < 2 || !rowNode.getChild(1).isCheckable()) {
        rowNode.recycle();
        return;
    }

    CharSequence taskLabel = labelNode.getText();
    final boolean isComplete = completeNode.isChecked();
    String completeStr = null;

    if (isComplete) {
        completeStr = getString(R.string.checked);
    } else {
        completeStr = getString(R.string.not_checked);
    }
    String reportStr = taskLabel + completeStr;
    speakToUser(reportStr);
}

Agora, você tem um serviço de acessibilidade completo e funcional. Tente configurar como ele interage com o usuário adicionando o mecanismo de conversão de texto em voz do Android ou usando um Vibrator para fornecer retorno tátil.

Processar texto

Os dispositivos com o Android 8.0 (API de nível 26) ou versões mais recentes incluem vários recursos de processamento de texto que facilitam a identificação e a operação dos serviços de acessibilidade em unidades de texto específicas exibidas na tela.

Dicas

O Android 9 (nível 28 da API) introduz vários recursos que dão acesso a dicas na IU de um app. Use getTooltipText() para ler o texto de uma dica e use ACTION_SHOW_TOOLTIP e ACTION_HIDE_TOOLTIP para instruir as instâncias de View a mostrar ou ocultar as dicas.

Texto de dica

Desde 2017, o Android inclui vários métodos para interagir com o texto de dica de um objeto baseado em texto:

  • Os métodos isShowingHintText() e setShowingHintText() indicam e definem, respectivamente, se o conteúdo de texto atual do nó representa o texto de dica do nó.
  • getHintText() fornece acesso ao próprio texto de dica. Mesmo que um objeto não esteja exibindo texto de dica, a chamada de getHintText() funciona.

Locais de caracteres de texto na tela

Em dispositivos com o Android 8.0 (nível 26 da API) e versões mais recentes, os serviços de acessibilidade podem determinar as coordenadas da tela para cada caixa delimitadora de caractere visível em um widget TextView. Os serviços encontram essas coordenadas chamando refreshWithExtraData(), transmitindo EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY como o primeiro argumento e um objeto Bundle como o segundo argumento. À medida que o método é executado, o sistema preenche o argumento Bundle com uma matriz parcelable de objetos Rect. Cada objeto Rect representa a caixa delimitadora de um caractere específico.

Valores padronizados de intervalo unilateral

Alguns objetos AccessibilityNodeInfo usam uma instância de AccessibilityNodeInfo.RangeInfo para indicar que um elemento da interface pode assumir um intervalo de valores. Ao criar um intervalo usando RangeInfo.obtain() ou ao recuperar os valores extremos do intervalo usando getMin() e getMax(), lembre-se de que os dispositivos com o Android 8.0 (API de nível 26) e versões mais recentes representam intervalos unilaterais de maneira padronizada:

Responder a eventos de acessibilidade

Agora que o serviço está configurado para executar e detectar eventos, escreva o código para que ele saiba o que fazer quando uma AccessibilityEvent chegar. Comece substituindo o método onAccessibilityEvent(AccessibilityEvent). Nesse método, use getEventType() para determinar o tipo de evento e getContentDescription() para extrair qualquer texto de rótulo associado à visualização que dispara o 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);
    ...
}

Outros recursos

Para saber mais, acesse os seguintes recursos:

Guias

Codelabs