Создайте службу обеспечения доступности.

Служба специальных возможностей — это приложение, которое улучшает пользовательский интерфейс, помогая пользователям с ограниченными возможностями или тем, кто временно не может полноценно взаимодействовать с устройством. Эти службы работают в фоновом режиме и взаимодействуют с системой, проверяя содержимое экрана и взаимодействуя с приложениями от имени пользователя. Примерами являются программы чтения с экрана (например, TalkBack), инструменты Switch Access и системы голосового управления.

В этом руководстве рассматриваются основы создания службы специальных возможностей для Android.

Жизненный цикл службы обеспечения доступности

Для создания службы специальных возможностей необходимо расширить класс AccessibilityService и объявить службу в манифесте вашего приложения.

Создайте класс сервиса.

Создайте класс, наследующий AccessibilityService . Необходимо переопределить следующие методы:

  • onAccessibilityEvent : Вызывается, когда система обнаруживает событие, соответствующее конфигурации вашей службы (например, изменение фокуса или нажатие кнопки). Здесь ваша служба интерпретирует пользовательский интерфейс.
  • onInterrupt : Вызывается, когда система прерывает обратную связь вашей службы (например, чтобы остановить вывод речи, когда пользователь быстро перемещает фокус).
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
    }
}

Укажите это в манифесте.

Зарегистрируйте свою службу в файле AndroidManifest.xml . Необходимо строго соблюдать разрешение BIND_ACCESSIBILITY_SERVICE , чтобы только система могла подключаться к вашей службе.

Чтобы убедиться в работоспособности кнопки настроек, объявите объект 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>

Настройте службу

Создайте конфигурационный файл в res/xml/accessibility_service_config.xml . Этот файл определяет, какие события обрабатывает ваша служба и какую обратную связь она предоставляет. Убедитесь, что вы указали ссылку на ServiceSettingsActivity , объявленный в вашем манифесте:

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

Конфигурационный файл содержит следующие ключевые атрибуты:

  • android:accessibilityEventTypes : События, которые вы хотите получать. Используйте typeAllMask для универсальной службы.
  • android:canRetrieveWindowContent : Должен быть true если вашему сервису необходимо проверять иерархию пользовательского интерфейса (например, для чтения текста с экрана).
  • android:canPerformGestures должен быть установлен true если вы планируете программно отправлять жесты (например, свайпы или касания).
  • android:accessibilityFlags : Объедините флаги для включения функций. flagRequestFingerprintGestures необходим для жестов отпечатков пальцев. flagRequestAccessibilityButton необходим для кнопки специальных возможностей программного обеспечения.

Полный список параметров конфигурации см. в разделе AccessibilityServiceInfo .

Конфигурация во время выполнения

Хотя XML-конфигурация является статической, вы также можете динамически изменять конфигурацию службы во время выполнения. Это полезно для переключения функций в зависимости от предпочтений пользователя.

Переопределите onServiceConnected() , чтобы применять обновления во время выполнения с помощью 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)
}

Интерпретация содержимого пользовательского интерфейса

Когда срабатывает onAccessibilityEvent() , система предоставляет AccessibilityEvent . Это событие служит точкой входа в дерево доступности , иерархическое представление содержимого экрана.

Ваш сервис взаимодействует преимущественно с объектами AccessibilityNodeInfo , которые представляют элементы пользовательского интерфейса, такие как кнопки, списки и текст. Данные об этих элементах пользовательского интерфейса нормализуются и преобразуются в AccessibilityNodeInfo .

В следующем примере показано, как получить источник события и пройтись по дереву доступности, чтобы найти необходимую информацию.

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
}

Действовать от имени пользователей

Службы обеспечения доступности могут выполнять действия от имени пользователя, такие как нажатие кнопок или прокрутка списков.

Для выполнения действия вызовите метод performAction() для объекта AccessibilityNodeInfo .

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

Для глобальных действий, затрагивающих всю систему (например, нажатие кнопки «Назад» или открытие панели уведомлений), используйте performGlobalAction() .

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

Управляйте фокусом

В Android существует два различных типа фокусировки: фокусировка ввода (куда направляется ввод с клавиатуры) и фокусировка специальных возможностей (что проверяет служба специальных возможностей).

Следующий фрагмент кода показывает, как найти элемент, который в данный момент находится в режиме доступности:

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

Следующий фрагмент кода показывает, как переместить фокус доступности на определенный элемент:

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

При создании службы доступности следует учитывать состояние фокуса пользователя и избегать перехвата фокуса, если это явно не вызвано действием пользователя.

Выполняйте жесты

Ваш сервис может отправлять на экран пользовательские жесты, такие как свайпы, касания или мультитач-взаимодействия. Для этого объявите в конфигурации android:canPerformGestures="true" , чтобы использовать API dispatchGesture() .

Простые жесты

Для выполнения простых жестов начните с создания объекта Path , представляющего движение, связанное с данным жестом. Затем оберните Path в GestureDescription , чтобы описать штрих. Наконец, вызовите dispatchGesture для отправки жеста.

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

Продолжение жестов

Для сложных взаимодействий (например, рисования фигуры в форме буквы L или выполнения точного многошагового перетаскивания) вы можете объединять штрихи, используя параметр 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)
}

Управление звуком

При создании службы специальных возможностей (особенно программы чтения с экрана) используйте аудиопоток STREAM_ACCESSIBILITY . Это позволит пользователям регулировать громкость службы независимо от громкости системных мультимедиа.

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

Обязательно включите флаг FLAG_ENABLE_ACCESSIBILITY_VOLUME в свою конфигурацию, либо в XML-файле, либо с помощью setServiceInfo во время выполнения.

Расширенные функции

Жесты отпечатков пальцев

На устройствах под управлением Android 10 (уровень API 29) или выше ваш сервис может считывать движения пальцев по датчику отпечатков пальцев. Это полезно для предоставления альтернативных элементов управления навигацией.

Добавьте следующую логику в метод 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)
    }
}

Кнопка доступности

На устройствах, использующих программные навигационные клавиши, пользователи могут вызвать вашу службу с помощью кнопки специальных возможностей на панели навигации.

Для использования этой функции добавьте флаг FLAG_REQUEST_ACCESSIBILITY_BUTTON в конфигурацию вашей службы. Затем добавьте логику регистрации в метод 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
            }
        }
    )
}

Многоязычная программа преобразования текста в речь

Сервис, озвучивающий текст, может автоматически переключать языки, если исходный текст помечен тегом LocaleSpan . Это позволяет вашему сервису правильно произносить текст на разных языках без ручного переключения.

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
)

Когда ваша служба обрабатывает AccessibilityNodeInfo , проверьте свойство text объектов LocaleSpan , чтобы определить правильный язык преобразования текста в речь.

Дополнительные ресурсы

Для получения более подробной информации ознакомьтесь со следующими ресурсами:

Гиды

Кодлабс

Просмотры контента