Как создать собственный сервис специальных возможностей (Views)

Основные понятия и реализация Jetpack Compose

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

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

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

Как создать сервис специальных возможностей

В проекте создайте класс, расширяющий 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() {
    }

...
}

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

Декларации и разрешения в манифесте

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

Декларация для сервисов специальных возможностей

Чтобы ваше приложение считалось сервисом специальных возможностей, включите в манифест элемент service, а не activity, в элементе application. Кроме того, в элемент service нужно включить фильтр интентов сервиса специальных возможностей. Манифест также должен защищать сервис, добавляя разрешение BIND_ACCESSIBILITY_SERVICE, чтобы к нему могла подключаться только система. Рассмотрим пример.

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

Настройка сервиса специальных возможностей

В конфигурации сервиса специальных возможностей должны быть указаны типы событий, которые он обрабатывает, и дополнительная информация о нем. Конфигурация сервиса специальных возможностей содержится в классе AccessibilityServiceInfo. Ваш сервис может создавать и задавать конфигурацию, используя экземпляр этого класса и setServiceInfo() во время выполнения. Однако при этом доступны не все параметры конфигурации.

Вы можете добавить в манифест элемент <meta-data> со ссылкой на файл конфигурации, чтобы задать все параметры для сервиса специальных возможностей, как показано в примере ниже.

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

Этот элемент <meta-data> ссылается на XML-файл, который вы создаете в каталоге ресурсов приложения: <project_dir>/res/xml/accessibility_service_config.xml>. Ниже приведен пример содержимого файла конфигурации сервиса.

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

Дополнительную информацию об атрибутах XML, которые можно использовать в файле конфигурации сервиса специальных возможностей, вы найдете в следующих справочных материалах:

Дополнительную информацию о том, какие настройки можно динамически задавать во время выполнения, вы найдете в справочной документации по AccessibilityServiceInfo.

Как настроить сервис специальных возможностей

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

  • На какие типы событий он должен реагировать?
  • Сервис должен быть активен для всех приложений или только для определенных пакетов?
  • Какие типы обратной связи он использует?

Задать эти переменные можно двумя способами. Для обратной совместимости можно задать их в коде с помощью setServiceInfo(android.accessibilityservice.AccessibilityServiceInfo). Для этого переопределите метод onServiceConnected() и настройте сервис, как показано в примере ниже.

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

}

Второй вариант – настроить сервис с помощью XML-файла. Некоторые параметры конфигурации, например canRetrieveWindowContent, доступны только при настройке сервиса с помощью XML. Вот как выглядят параметры конфигурации из предыдущего примера, если они определены с помощью 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"
/>

Если вы используете XML, добавьте в манифест тег <meta-data>, указывающий на XML-файл. Если вы храните XML-файл в res/xml/serviceconfig.xml, новый тег выглядит так:

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

Методы сервиса специальных возможностей

Сервис специальных возможностей должен расширять класс AccessibilityService и переопределять следующие методы из этого класса: Эти методы представлены в том порядке, в котором их вызывает система Android: от запуска сервиса (onServiceConnected()) до его работы (onAccessibilityEvent(), onInterrupt()) и завершения (onUnbind()).

  • onServiceConnected() (необязательно) – система вызывает этот метод при подключении к вашему сервису специальных возможностей. Используйте этот метод для однократной настройки сервиса, в том числе для подключения к системным сервисам обратной связи, таким как аудиоменеджер или вибрация устройства. Если вы хотите задать конфигурацию сервиса во время выполнения или внести однократные корректировки, это удобное место для вызова setServiceInfo().

  • onAccessibilityEvent() (обязательный) – система вызывает этот метод, когда обнаруживает событие AccessibilityEvent, соответствующее параметрам фильтрации событий, заданным вашим сервисом специальных возможностей. Например, когда пользователь нажимает кнопку или фокусируется на элементе управления пользовательского интерфейса в приложении, для которого ваш сервис специальных возможностей предоставляет обратную связь. При вызове этого метода система передает связанный объект AccessibilityEvent, который сервис может интерпретировать и использовать для предоставления обратной связи пользователю. Этот метод может вызываться многократно в течение жизненного цикла сервиса.

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

  • onUnbind() (необязательно) – система вызывает этот метод, когда собирается отключить сервис специальных возможностей. Используйте этот метод для выполнения любых одноразовых процедур завершения работы, включая деаллокацию системных служб обратной связи с пользователем, таких как аудиоменеджер или вибратор устройства.

Эти методы обратного вызова обеспечивают базовую структуру сервиса специальных возможностей. Вы можете решить, как обрабатывать данные, предоставляемые системой Android в виде объектов AccessibilityEvent, и отправлять отзывы пользователям. Подробнее о том, как получить информацию о событии специальных возможностей…

Регистрация на мероприятия, посвященные специальным возможностям

Одна из самых важных функций параметров конфигурации сервиса специальных возможностей – указать, какие типы событий специальных возможностей может обрабатывать ваш сервис. Указав эту информацию, вы позволите специальным возможностям взаимодействовать друг с другом и сможете обрабатывать только определенные типы событий из определенных приложений. Фильтрация событий может включать следующие критерии:

  • Названия пакетов. Укажите названия пакетов приложений, события специальных возможностей которых должен обрабатывать ваш сервис. Если этот параметр не указан, сервис специальных возможностей будет обрабатывать события специальных возможностей для любого приложения. Вы можете задать этот параметр в файлах конфигурации сервиса специальных возможностей с помощью атрибута android:packageNames в виде списка, разделенного запятыми, или использовать элемент AccessibilityServiceInfo.packageNames.

  • Типы событий. Укажите, какие типы событий специальных возможностей должен обрабатывать ваш сервис. Этот параметр можно задать в файлах конфигурации специальных возможностей с помощью атрибута android:accessibilityEventTypes в виде списка, разделенного символом |, например accessibilityEventTypes="typeViewClicked|typeViewFocused". или с помощью элемента AccessibilityServiceInfo.eventTypes.

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

Громкость подсказок

На устройствах с Android 8.0 (уровень API 26) и более поздних версий есть категория громкости STREAM_ACCESSIBILITY, которая позволяет управлять громкостью аудиовыхода сервиса специальных возможностей независимо от других звуков на устройстве.

Сервисы специальных возможностей могут использовать этот тип потока, если задан параметр FLAG_ENABLE_ACCESSIBILITY_VOLUME. Затем вы можете изменить громкость специальных возможностей, вызвав метод adjustStreamVolume() для экземпляра AudioManager устройства.

В следующем фрагменте кода показано, как сервис специальных возможностей может использовать категорию громкости 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);
        }
    }
}

Дополнительную информацию можно найти в видео Что нового в специальных возможностях Android с конференции Google I/O 2017 (начиная с 6:35).

Быстрое включение

На устройствах с Android 8.0 (уровень API 26) и более поздними версиями пользователи могут включать и отключать нужный сервис специальных возможностей на любом экране, одновременно нажимая и удерживая обе кнопки регулировки громкости. По умолчанию эта кнопка включает и отключает TalkBack, но пользователи могут настроить ее для включения и отключения любого сервиса, установленного на устройстве.

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

Дополнительную информацию можно найти в видео Что нового в специальных возможностях Android с конференции Google I/O 2017 (начиная с 13:25).

Кнопка специальных возможностей

На устройствах с программно отрисованной областью навигации и ОС Android 8.0 (уровень API 26) или более поздней версии в правой части панели навигации есть кнопка специальных возможностей. Нажав на нее, пользователь может вызвать одну из нескольких включенных специальных возможностей в зависимости от контента, который в данный момент показывается на экране.

Чтобы пользователи могли вызывать сервис специальных возможностей с помощью кнопки специальных возможностей, в атрибут android:accessibilityFlags объекта AccessibilityServiceInfo сервиса нужно добавить флаг FLAG_REQUEST_ACCESSIBILITY_BUTTON. Затем сервис может зарегистрировать обратные вызовы с помощью registerAccessibilityButtonCallback().

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

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

Дополнительную информацию можно найти в видео Что нового в специальных возможностях Android с конференции Google I/O 2017 (начиная с 16:28).

Регистрировать жесты на сканере отпечатков пальцев

Службы специальных возможностей на устройствах с Android 8.0 (уровень API 26) и более поздними версиями могут реагировать на жесты пролистывания (вверх, вниз, влево и вправо) по сканеру отпечатков пальцев. Чтобы настроить сервис для получения обратных вызовов об этих взаимодействиях, выполните следующие действия:

  1. Объявите разрешение USE_BIOMETRIC и функцию CAPABILITY_CAN_REQUEST_FINGERPRINT_GESTURES.
  2. Установите флаг FLAG_REQUEST_FINGERPRINT_GESTURES в атрибуте android:accessibilityFlags.
  3. Зарегистрируйте обратные вызовы с помощью registerFingerprintGestureCallback().

Обратите внимание, что не на всех устройствах есть сканер отпечатков пальцев. Чтобы определить, поддерживает ли устройство датчик, используйте метод isHardwareDetected(). Даже если на устройстве есть сканер отпечатков пальцев, ваш сервис не сможет использовать его, когда он задействован для аутентификации. Чтобы определить, когда датчик доступен, вызовите метод isGestureDetectionAvailable() и реализуйте обратный вызов onGestureDetectionAvailabilityChanged().

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

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

Дополнительную информацию можно найти в видео Что нового в специальных возможностях Android с конференции Google I/O 2017 (начиная с 9:03).

Многоязычный синтез речи

Начиная с Android 8.0 (уровень API 26), сервис синтеза речи (TTS) может распознавать и озвучивать фразы на разных языках в одном блоке текста. Чтобы включить автоматическое переключение языков в сервисе специальных возможностей, оберните все строки в объекты LocaleSpan, как показано в следующем фрагменте кода:

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

Дополнительную информацию можно найти в видео Что нового в специальных возможностях Android с конференции Google I/O 2017 (начиная с 10:59).

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

С 2011 года специальные возможности могут действовать от имени пользователей, в том числе менять фокус ввода и выбирать (активировать) элементы пользовательского интерфейса. В 2012 году список действий пополнился прокруткой списков и взаимодействием с текстовыми полями. Сервисы специальных возможностей также могут выполнять глобальные действия, например переходить на главный экран, нажимать кнопку "Назад", открывать экран уведомлений и список недавно использованных приложений. С 2012 года в Android есть фокус специальных возможностей, который позволяет выбирать все видимые элементы с помощью сервиса специальных возможностей.

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

Прослушивание жестов

Сервисы специальных возможностей могут отслеживать определенные жесты и реагировать на них от имени пользователя. Для работы этой функции необходимо, чтобы сервис специальных возможностей запрашивал активацию функции "Изучение касанием". Для этого в экземпляре AccessibilityServiceInfo сервиса нужно задать для элемента flags значение FLAG_REQUEST_TOUCH_EXPLORATION_MODE, как показано в примере ниже.

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

После того как ваш сервис запросит активацию функции "Изучение касанием", пользователь должен разрешить ее включение, если она ещё не активна. Когда эта функция активна, ваш сервис получает уведомления о жестах специальных возможностей через метод обратного вызова onGesture() и может реагировать на них, выполняя действия от имени пользователя.

Продолжительные жесты

Устройства с Android 8.0 (уровень API 26) и более поздними версиями поддерживают продолжающиеся жесты, то есть программные жесты, содержащие более одного объекта Path.

При указании последовательности штрихов вы можете указать, что они принадлежат к одному и тому же программному жесту, используя последний аргумент willContinue в конструкторе GestureDescription.StrokeDescription, как показано в следующем фрагменте кода:

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

Дополнительную информацию можно найти в видео What's new in Android accessibility (Что нового в специальных возможностях Android) с конференции Google I/O 2017 (начиная с 15:47).

Как использовать действия специальных возможностей

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

Чтобы действовать от имени пользователей, ваш сервис специальных возможностей должен зарегистрироваться для получения событий от приложений и запросить разрешение на просмотр контента приложений, установив для android:canRetrieveWindowContent значение true в файле конфигурации сервиса. Когда ваш сервис получает события, он может извлечь из них объект AccessibilityNodeInfo с помощью getSource(). С помощью объекта AccessibilityNodeInfo ваш сервис может изучить иерархию View, чтобы определить, какое действие нужно выполнить, а затем выполнить его от имени пользователя, используя 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();
    }
    ...
}

Метод performAction() позволяет сервису выполнять действия в приложении. Если сервису нужно выполнить глобальное действие, например перейти на главный экран, нажать кнопку "Назад", открыть экран уведомлений или список недавно использованных приложений, используйте метод performGlobalAction().

Как использовать типы режима концентрации внимания

В 2012 году в Android появился фокус специальных возможностей пользовательского интерфейса. Службы специальных возможностей могут использовать этот фокус, чтобы выбрать любой видимый элемент пользовательского интерфейса и выполнить с ним действие. Этот тип фокуса отличается от фокуса ввода, который определяет, какой элемент пользовательского интерфейса на экране получает входные данные, когда пользователь вводит символы, нажимает Ввод на клавиатуре или нажимает центральную кнопку на D-pad.

Возможна ситуация, когда фокус ввода находится на одном элементе пользовательского интерфейса, а фокус специальных возможностей – на другом. Цель фокуса специальных возможностей – предоставить сервисам специальных возможностей способ взаимодействия с видимыми элементами на экране, независимо от того, можно ли сфокусироваться на элементе с точки зрения системы. Чтобы убедиться, что ваш сервис специальных возможностей правильно взаимодействует с элементами ввода в приложениях, следуйте рекомендациям по тестированию доступности приложения.

Сервис специальных возможностей может определить, какой элемент пользовательского интерфейса находится в фокусе ввода или специальных возможностей, с помощью метода AccessibilityNodeInfo.findFocus(). Вы также можете искать элементы, которые можно выбрать с помощью фокуса ввода, используя метод focusSearch(). Наконец, сервис специальных возможностей может установить фокус специальных возможностей с помощью метода performAction(AccessibilityNodeInfo.ACTION_SET_ACCESSIBILITY_FOCUS).

Соберите информацию о нежелательных объявлениях

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

Получить сведения об изменении окна

В Android 9 (уровень API 28) и более поздних версиях приложения могут отслеживать обновления окон, когда приложение перерисовывает несколько окон одновременно. Когда происходит событие TYPE_WINDOWS_CHANGED, используйте API getWindowChanges(), чтобы определить, как меняются окна. При обновлении в многооконном режиме каждое окно генерирует собственный набор событий. Метод getSource() возвращает корневое представление окна, связанного с каждым событием.

Если в приложении заданы названия панелей специальных возможностей для объектов View, ваш сервис может распознавать, когда обновляется интерфейс приложения. Когда происходит событие TYPE_WINDOW_STATE_CHANGED, используйте типы, возвращаемые getContentChangeTypes(), чтобы определить, как меняется окно. Например, фреймворк может определить, когда у панели появляется новый заголовок или когда панель исчезает.

Получить сведения о мероприятии

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

Примером интерфейса, в котором контекст имеет решающее значение, является календарь или ежедневник. Если пользователь выбирает временной интервал 16:00 в списке дней с понедельника по пятницу, а сервис специальных возможностей объявляет "16:00", но не объявляет название дня недели, день месяца или название месяца, полученный отзыв сбивает с толку. В этом случае контекст элемента управления пользовательским интерфейсом имеет решающее значение для пользователя, который хочет запланировать встречу.

С 2011 года в Android значительно расширен объем информации, которую сервис специальных возможностей может получить о взаимодействии с пользовательским интерфейсом. Это стало возможным благодаря созданию событий специальных возможностей на основе иерархии View. Иерархия View – это набор компонентов пользовательского интерфейса, которые содержат компонент (его родительские элементы) и элементы пользовательского интерфейса, которые могут содержаться в этом компоненте (его дочерние элементы). Это позволяет Android предоставлять более подробную информацию о событиях специальных возможностей, а сервисам специальных возможностей – более полезную обратную связь для пользователей.

Сервис специальных возможностей получает информацию о событии пользовательского интерфейса через объект AccessibilityEvent, который система передает методу обратного вызова onAccessibilityEvent(). Этот объект содержит подробные сведения о событии, включая тип объекта, с которым выполняется действие, его описание и другие данные.

  • AccessibilityEvent.getRecordCount() и getRecord(int). Эти методы позволяют получить набор объектов AccessibilityRecord, которые вносят вклад в AccessibilityEvent, переданный вам системой. Это позволяет лучше понять контекст события, которое активирует сервис специальных возможностей.

  • AccessibilityRecord.getSource() – этот метод возвращает объект AccessibilityNodeInfo. Этот объект позволяет запрашивать иерархию макета представления (родительские и дочерние элементы) компонента, который инициирует событие специальных возможностей. Эта функция позволяет сервису специальных возможностей изучить полный контекст события, включая контент и состояние всех окружающих или дочерних представлений.

Платформа Android позволяет AccessibilityService запрашивать иерархию View, собирая информацию о компоненте пользовательского интерфейса, который генерирует событие, а также о его родительских и дочерних элементах. Для этого добавьте в конфигурацию XML следующую строку:

android:canRetrieveWindowContent="true"

После этого получите объект AccessibilityNodeInfo с помощью getSource(). Этот вызов возвращает объект, только если окно, в котором возникло событие, по-прежнему активно. В противном случае возвращается значение null.

В примере ниже код выполняет следующие действия при получении события:

  1. Немедленно захватывает родительский элемент представления, в котором возникает событие.
  2. В этом представлении ищет ярлык и флажок как дочерние представления.
  3. Если они найдены, создается строка, которая сообщает пользователю, какой ярлык был проверен и был ли он установлен.

Если в какой-либо момент при обходе иерархии View возвращается нулевое значение, метод прекращает работу.

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

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

Обработка текста

В Android 8.0 (уровень API 26) и более поздних версиях есть несколько функций обработки текста, которые помогают службам специальных возможностей распознавать и обрабатывать определенные текстовые элементы на экране.

Всплывающие подсказки

В Android 9 (уровень API 28) появилось несколько возможностей, позволяющих получить доступ к подсказкам в пользовательском интерфейсе приложения. Используйте getTooltipText(), чтобы прочитать текст подсказки, и ACTION_SHOW_TOOLTIP и ACTION_HIDE_TOOLTIP, чтобы показать или скрыть подсказки для экземпляров View.

Подсказка

Начиная с 2017 года в Android есть несколько методов для взаимодействия с текстом подсказки текстового объекта:

  • Методы isShowingHintText() и setShowingHintText() указывают и задают соответственно, представляет ли текущий текстовый контент узла текст подсказки узла.
  • getHintText() предоставляет доступ к тексту подсказки. Даже если объект не показывает текст подсказки, вызов getHintText() выполняется успешно.

Расположение символов текста на экране.

На устройствах с Android 8.0 (уровень API 26) и более поздних версий службы специальных возможностей могут определять координаты граничной рамки каждого видимого символа в виджете TextView. Сервисы находят эти координаты, вызывая refreshWithExtraData(), передавая EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY в качестве первого аргумента и объект Bundle в качестве второго аргумента. При выполнении метода система заполняет аргумент Bundle массивом объектов Rect, которые можно упаковать. Каждый объект Rect представляет собой граничную рамку определенного символа.

Стандартизированные значения одностороннего диапазона

Некоторые объекты AccessibilityNodeInfo используют экземпляр AccessibilityNodeInfo.RangeInfo, чтобы указать, что элемент интерфейса может принимать диапазон значений. При создании диапазона с помощью RangeInfo.obtain() или получении его крайних значений с помощью getMin() и getMax() помните, что устройства с Android 8.0 (уровень API 26) и более поздними версиями представляют односторонние диапазоны стандартным образом:

  • Если минимальное значение не задано, то Float.NEGATIVE_INFINITY представляет минимальное значение.
  • Для диапазонов без максимального значения Float.POSITIVE_INFINITY представляет максимальное значение.

Реагирование на события специальных возможностей

Теперь, когда сервис настроен на запуск и прослушивание событий, напишите код, чтобы он знал, что делать при поступлении AccessibilityEvent. Сначала переопределите метод onAccessibilityEvent(AccessibilityEvent). В этом методе используйте getEventType(), чтобы определить тип события, и getContentDescription(), чтобы извлечь текст ярлыка, связанный с просмотром, который активирует событие.

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

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

Дополнительную информацию можно найти в следующих ресурсах:

Руководства

Практические работы