独自のアクセシビリティ サービスを作成する

ユーザー補助サービスは、障がいのあるユーザーや一時的にデバイスを完全に操作できない可能性があるユーザーを支援するために、ユーザー インターフェースを強化するアプリです。たとえば、車を運転している、幼い子供の世話をしている、または非常に騒がしいパーティーに参加するユーザーは、追加のまたは別のインターフェース フィードバックが必要になる可能性があります。

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 システムでユーザー補助サービスとして扱われるために、アプリ マニフェストに固有の宣言を含める必要があります。このセクションでは、ユーザー補助サービスの必須の設定と任意の設定について説明します。

ユーザー補助サービスの宣言

アプリをユーザー補助サービスとして扱うには、マニフェストの application 要素内に、activity 要素ではなく service 要素を含めます。さらに、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> 要素は、アプリのリソース ディレクトリ(<project_dir>/res/xml/accessibility_service_config.xml>)に作成する 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 のリファレンス ドキュメントをご覧ください。

ユーザー補助サービスを設定する

ユーザー補助サービスの構成変数を設定して、実行する方法とタイミングをシステムに伝える場合は、次の点を考慮してください。

  • 応答するアクティビティのタイプを選択してください。
  • サービスをすべてのアプリに対してアクティブにする必要があるか、特定のパッケージ名のみに対してアクティブにする必要があるか。
  • どのようなフィードバック タイプを使用するかを伝えます。

これらの変数を設定するには、2 つの方法があります。下位互換性のあるオプションは、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);

}

2 つ目の方法は、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 を使用する場合は、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(): (省略可)ユーザー補助サービスに接続したときに、システムがこのメソッドを呼び出します。オーディオ マネージャーやデバイス バイブレーションなどのユーザー フィードバック システム サービスへの接続など、サービスの 1 回限りの設定手順を行うには、このメソッドを使用します。ランタイムにサービスの構成を設定する場合や、1 回限りの調整を行う場合は、ここで setServiceInfo() を呼び出すと便利です。

  • onAccessibilityEvent(): (必須)ユーザー補助サービスがフィードバックしているアプリでユーザーがボタンをタップしたり、ユーザー インターフェース コントロールにフォーカスを当てたりするなど、ユーザー補助サービスで指定されたイベント フィルタリング パラメータに一致する AccessibilityEvent を検出したときに、このメソッドがコールバックされます。システムがこのメソッドを呼び出すと、関連する AccessibilityEvent が渡されます。サービスは、これを解釈してユーザーにフィードバックを提供するために使用できます。このメソッドは、サービスのライフサイクル全体で何度も呼び出すことができます。

  • onInterrupt(): (必須)サービスが提供するフィードバックをシステムが中断する必要がある場合に、システムがこのメソッドを呼び出します。これは通常、別のコントロールにフォーカスを移動するなどのユーザー アクションに応じて行われます。このメソッドは、サービスのライフサイクル全体で何度も呼び出すことができます。

  • onUnbind(): (省略可)システムがユーザー補助サービスをシャットダウンしようとすると、システムがこのメソッドを呼び出します。このメソッドを使用して、1 回限りのシャットダウン手順を行うことができます。これには、オーディオ マネージャーやデバイス バイブレータなどのユーザー フィードバック システム サービスの割り当て解除が含まれます。

これらのコールバック メソッドは、ユーザー補助サービスの基本構造を提供します。Android システムによって AccessibilityEvent オブジェクトの形式で提供されるデータの処理方法を決定し、ユーザーにフィードバックを提供できます。ユーザー補助イベントから情報を取得する方法について詳しくは、イベントの詳細を取得するをご覧ください。

ユーザー補助イベントに登録する

ユーザー補助サービス設定パラメータで最も重要な機能の一つは、サービスで処理できるユーザー補助イベントのタイプを指定できることです。この情報を指定すると、ユーザー補助サービスが相互に連携し、特定のアプリの特定のイベントタイプだけを柔軟に処理できるようになります。イベント フィルタリングには次の条件を含めることができます。

  • パッケージ名: サービスで処理するユーザー補助イベントを含むアプリのパッケージ名を指定します。このパラメータを省略すると、どのアプリのユーザー補助イベントでもユーザー補助サービスが利用可能とみなされます。このパラメータは、ユーザー補助サービスの構成ファイルで android:packageNames 属性をカンマ区切りリストで設定するか、AccessibilityServiceInfo.packageNames メンバーを使用して設定できます。

  • イベントタイプ: サービスで処理するユーザー補助イベントのタイプを指定します。このパラメータは、ユーザー補助サービスの構成ファイルで android:accessibilityEventTypes 属性を | 文字で区切ったリストとして設定できます(例: accessibilityEventTypes="typeViewClicked|typeViewFocused")。または、AccessibilityServiceInfo.eventTypes メンバーを使用して設定することもできます。

ユーザー補助サービスをセットアップするときは、サービスで処理できるイベントを慎重に検討し、それらのイベントにのみ登録します。ユーザーは一度に複数のユーザー補助サービスを有効にできるため、サービスで処理できないイベントを使用しないようにしてください。他のサービスがユーザー エクスペリエンスを向上させるために、このようなイベントを処理する場合があることに留意してください。

ユーザー補助機能の音量

Android 8.0(API レベル 26)以降を搭載したデバイスには STREAM_ACCESSIBILITY 音量カテゴリがあり、これにより、ユーザー補助サービスの音声出力の音量を、デバイス上の他の音とは別に制御することができます。

ユーザー補助サービスでは、FLAG_ENABLE_ACCESSIBILITY_VOLUME オプションを設定することで、このストリーム タイプを使用できます。その後、デバイスの AudioManager インスタンスで adjustStreamVolume() メソッドを呼び出して、デバイスのユーザー補助の音量を変更できます。

次のコード スニペットは、ユーザー補助サービスで 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);
        }
    }
}

詳しくは、Google I/O 2017 の Android のユーザー補助の新機能に関するセッションの動画の 6:35 からご覧ください。

ユーザー補助のショートカット

Android 8.0(API レベル 26)以降を搭載したデバイスでは、どの画面からでも音量大と音量小の両方のボタンを同時に長押しすることで、任意のユーザー補助サービスを有効または無効にすることができます。このショートカットを使用すると、TalkBack はデフォルトで有効または無効になりますが、ユーザーはこのボタンを構成して、デバイスにインストールされているサービスを有効または無効にすることができます。

ユーザーがユーザー補助ショートカットから特定のユーザー補助サービスにアクセスするには、そのサービスが実行時に機能をリクエストする必要があります。

詳細については、Google I/O 2017 の Android のユーザー補助の新機能に関するセッションの動画の 13:25 からご覧ください。

ユーザー補助機能ボタン

ソフトウェア レンダリングのナビゲーション エリアを使用し、Android 8.0(API レベル 26)以降を搭載しているデバイスでは、ナビゲーション バーの右側にユーザー補助機能ボタンが表示されます。ユーザーがこのボタンを押すと、現在画面に表示されているコンテンツに応じて、有効になっているユーザー補助機能やサービスのいずれかを呼び出せます。

ユーザーがユーザー補助機能ボタンを使用して特定のユーザー補助サービスを呼び出せるようにするには、AccessibilityServiceInfo オブジェクトの android:accessibilityFlags 属性に 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);
    }
}

詳細については、Google I/O 2017 の Android のユーザー補助の新機能に関するセッションの動画の 16:28 からご覧ください。

指紋認証センサーでの操作

Android 8.0(API レベル 26)以降を搭載したデバイスのユーザー補助サービスは、デバイスの指紋認証センサーに沿った方向スワイプ(上、下、左、右)に応答できます。これらの操作に関するコールバックを受け取るようにサービスを構成するには、次の手順を完了します。

  1. USE_BIOMETRIC 権限と CAPABILITY_CAN_REQUEST_FINGERPRINT_GESTURES 機能を宣言します。
  2. android:accessibilityFlags 属性内で FLAG_REQUEST_FINGERPRINT_GESTURES フラグを設定します。
  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);
        }
    }
}

詳しくは、Google I/O 2017 の Android のユーザー補助の新機能に関するセッションの動画(9:03 以降)をご覧ください。

多言語でのテキスト読み上げ

Android 8.0(API レベル 26)以降、Android のテキスト読み上げ(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;
}

詳細については、Google I/O 2017 の Android のユーザー補助の新機能に関するセッションの動画(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 オブジェクトを含むプログラム ジェスチャーをサポートしています。

一連のストロークを指定するときに、GestureDescription.StrokeDescription コンストラクタの最後の引数 willContinue を使用して、同じプログラマティック操作に属するように指定できます。次のコード スニペットをご覧ください。

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

詳細については、Google I/O 2017 の Android のユーザー補助の新機能に関するセッションの動画の 15:47 からご覧ください。

ユーザー補助アクションを使用する

ユーザー補助サービスは、ユーザーに代わってアプリの操作を簡素化し、生産性を向上させることができます。ユーザー補助サービスがアクションを実行する機能は 2011 年に追加され、2012 年には大幅に拡張されました。

ユーザーに代わってユーザー補助サービスを使用するには、登録してアプリからイベントを受信し、サービス構成ファイルandroid:canRetrieveWindowContenttrue に設定して、アプリのコンテンツを表示する権限をリクエストする必要があります。サービスはイベントを受信すると、getSource() を使用してイベントから AccessibilityNodeInfo オブジェクトを取得できます。AccessibilityNodeInfo オブジェクトによってサービスはビュー階層を探索し、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 は「ユーザー補助フォーカス」と呼ばれるユーザー インターフェースのフォーカスを導入しました。ユーザー補助サービスは、このフォーカスを使用して、表示されているユーザー インターフェース要素を選択し、操作できます。このフォーカス タイプは入力フォーカスとは異なります。入力フォーカスは、ユーザーが文字を入力したとき、キーボードの Enter キーを押すか、D-pad の中央ボタンを押したときに、画面上のどのユーザー インターフェース要素が入力を受け取るかを決定します。

ユーザー インターフェースの 1 つの要素に入力フォーカスがあり、別の要素にユーザー補助フォーカスがあってもかまいません。ユーザー補助フォーカスの目的は、その要素がシステムの観点から入力フォーカス可能であるかどうかにかかわらず、ユーザー補助サービスに、画面上の要素を操作する手段を提供することです。ユーザー補助サービスがアプリの入力要素と正しく連携するように、アプリのユーザー補助機能のテストに関するガイドラインに沿って、一般的なアプリを使用しながらサービスをテストします。

ユーザー補助サービスは、AccessibilityNodeInfo.findFocus() メソッドを使用して、入力フォーカスまたはユーザー補助フォーカスがあるユーザー インターフェース要素を判別できます。focusSearch() メソッドを使用して、入力フォーカスで選択できる要素を検索することもできます。最後に、ユーザー補助サービスでは、performAction(AccessibilityNodeInfo.ACTION_SET_ACCESSIBILITY_FOCUS) メソッドを使用してユーザー補助のフォーカスを設定できます。

情報を入手する

ユーザー補助サービスには、イベントの詳細、テキスト、数値など、ユーザー提供情報の主要な単位を収集して表す標準的なメソッドがあります。

ウィンドウの変更の詳細を取得する

Android 9(API レベル 28)以降では、アプリが複数のウィンドウを同時に再描画したとき、アプリでウィンドウの更新を追跡できます。TYPE_WINDOWS_CHANGED イベントが発生したら、getWindowChanges() API を使用してウィンドウの変更方法を決定します。マルチウィンドウの更新中は、各ウィンドウが独自のイベントセットを生成します。getSource() メソッドは、各イベントに関連付けられたウィンドウのルートビューを返します。

アプリが View オブジェクトのユーザー補助ペインのタイトルを定義している場合、サービスはアプリの UI がいつ更新されたかを認識できます。TYPE_WINDOW_STATE_CHANGED イベントが発生したら、getContentChangeTypes() が返すタイプを使用して、ウィンドウの変更方法を決定します。たとえば、フレームワークはペインに新しいタイトルが追加されたタイミングや、ペインが消えたことを検出できます。

イベントの詳細を取得する

Android は、AccessibilityEvent オブジェクトを通じて、ユーザー インターフェース操作に関する情報をユーザー補助サービスに提供します。以前のバージョンの Android では、ユーザーが選択したユーザー インターフェース コントロールについての重要な詳細情報を提供する一方で、ユーザー補助イベントで利用できる情報は、コンテキスト情報に限られていました。多くの場合、このようなコンテキスト情報がないことは、選択したコントロールの意味を理解するうえで重要です。

コンテキストが重要となるインターフェースの例として、カレンダーや日付の予定表があります。ユーザーが月曜日から金曜日のリストから午後 4 時の時間帯を選択し、ユーザー補助サービスが「午後 4 時」と読み上げたのに平日の名前、日付、月の名前が読み上げられない場合、結果として得られるフィードバックが混乱します。この場合、会議をスケジュールするユーザーにとっては、ユーザー インターフェース コントロールのコンテキストが重要となります。

2011 年以降、Android はビュー階層に基づいてユーザー補助イベントを作成することで、ユーザー補助サービスがユーザー インターフェースの操作に関して取得できる情報量を大幅に増やしました。ビュー階層は、コンポーネント(親)を含むユーザー インターフェース コンポーネントと、そのコンポーネントに含まれる可能性のあるユーザー インターフェース要素(子)のセットです。このようにして、Android はユーザー補助イベントに関するより詳細な情報を提供し、ユーザー補助サービスがユーザーにさらに有用なフィードバックを提供できるようにします。

ユーザー補助サービスは、システムからサービスの onAccessibilityEvent() コールバック メソッドに渡される AccessibilityEvent を介して、ユーザー インターフェース イベントに関する情報を取得します。このオブジェクトは、操作されたオブジェクトのタイプ、説明テキスト、その他の詳細情報など、イベントに関する詳細情報を提供します。

  • AccessibilityEvent.getRecordCount()getRecord(int): これらのメソッドを使用すると、システムから渡される AccessibilityEvent に寄与する AccessibilityRecord オブジェクトのセットを取得できます。この詳細レベルほど、ユーザー補助サービスをトリガーするイベントの詳細なコンテキスト情報が得られます。

  • AccessibilityRecord.getSource(): このメソッドは AccessibilityNodeInfo オブジェクトを返します。このオブジェクトを使用すると、ユーザー補助イベントを発生させたコンポーネントのビュー レイアウト階層(親と子)をリクエストできます。この機能を使用すると、ユーザー補助サービスは、ビューや子ビューのコンテンツや状態など、イベントの完全なコンテキストを調査できます。

Android プラットフォームには、AccessibilityService がビュー階層をクエリする機能があり、イベントを生成する UI コンポーネントとその親と子に関する情報を収集します。これを行うには、XML 構成で次の行を設定します。

android:canRetrieveWindowContent="true"

それが完了したら、getSource() を使用して AccessibilityNodeInfo オブジェクトを取得します。この呼び出しは、イベントが発生したウィンドウがまだアクティブなウィンドウである場合にのみ、オブジェクトを返します。含まれていない場合は null を返すため、適切な動作を行います。

以下の例では、イベントの受信時にコードは次の処理を行います。

  1. イベントが発生したビューの親をすぐに取得します。
  2. そのビューで、子ビューとしてラベルとチェックボックスを探します。
  3. 見つかった場合は、ラベルとチェックボックスがオンかどうかを示す文字列を作成してユーザーに報告します。

ビュー階層の走査中に null 値が返されると、メソッドは静かに諦めます。

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)には、アプリの UI のツールチップにアクセスするための機能がいくつか導入されています。ツールチップのテキストを読み取るには getTooltipText() を使用し、ツールチップを表示または非表示にするように View のインスタンスに指示するには、ACTION_SHOW_TOOLTIPACTION_HIDE_TOOLTIP を使用します。

ヒントのテキスト

2017 年以降、Android にはテキストベースのオブジェクトのヒントテキストを操作するためのメソッドがいくつか含まれています。

  • isShowingHintText() メソッドと setShowingHintText() メソッドは、ノードの現在のテキスト コンテンツがノードのヒントテキストかどうかをそれぞれ示し、設定します。
  • getHintText() は、ヒントテキスト自体へのアクセスを提供します。オブジェクトにヒントテキストが表示されない場合でも、getHintText() の呼び出しは成功します。

画面上のテキスト文字の場所

Android 8.0(API レベル 26)以降を搭載したデバイスでは、ユーザー補助サービスにより、TextView ウィジェット内の表示される文字の境界ボックスの画面座標を特定できます。サービスは、refreshWithExtraData() を呼び出して、最初の引数として EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY、2 番目の引数として Bundle オブジェクトを渡すことで、これらの座標を検出します。このメソッドを実行すると、システムによって Rect オブジェクトの Parcelable 配列が Bundle 引数に入力されます。各 Rect オブジェクトは特定の文字の境界ボックスを表します。

標準化された片側範囲値

一部の AccessibilityNodeInfo オブジェクトでは、AccessibilityNodeInfo.RangeInfo のインスタンスを使用して、UI 要素が値の範囲を取ることができることを示します。RangeInfo.obtain() を使用して範囲を作成するとき、または getMin()getMax() を使用して範囲の極値を取得する場合、Android 8.0(API レベル 26)以降を搭載しているデバイスは、標準化された方法で片側範囲を表すことに留意してください。

ユーザー補助イベントに応答する

これでイベントを実行し、リッスンするようにサービスがセットアップされたので、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);
    ...
}

参考情報

詳しくは、次のリソースをご覧ください。

ガイド

Codelab