Сервис специальных возможностей – это приложение, которое улучшает пользовательский интерфейс, чтобы помогать людям с ограниченными возможностями или тем, кто временно не может полноценно взаимодействовать с устройством. Эти сервисы работают в фоновом режиме и взаимодействуют с системой, чтобы проверять контент на экране и взаимодействовать с приложениями от имени пользователя. Примеры: программы чтения с экрана (например, 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, чтобы определить правильный язык для преобразования текста в речь.
Дополнительные ресурсы
Дополнительную информацию можно найти в следующих ресурсах:
Руководства
- Как создавать приложения со специальными возможностями
- Специальные возможности в Jetpack Compose
- Краткое руководство по специальным возможностям в Compose