独自のユーザー補助サービスを作成する

ユーザー補助サービスは、障がいがあるユーザーや一時的にデバイスを十分に操作できないユーザーを支援するために、ユーザー インターフェースの拡張機能を提供するアプリです。たとえば、運転中のユーザーや、幼児の世話をしているユーザー、非常に騒がしいパーティに参加しているユーザーに、インターフェースから追加または代わりとなるフィードバックが必要になることがあります。

Android では、TalkBack などの標準のユーザー補助サービスが提供されるほか、デベロッパーも独自のサービスを作成して配信することができます。このドキュメントでは、ユーザー補助サービスの構築の基本事項について説明します。

注: アプリがプラットフォーム レベルのユーザー補助サービスを使用するのは、障がいのあるユーザーがアプリを操作するのをサポートする目的のみにする必要があります。

デベロッパーによるユーザー補助サービスのビルドとデプロイは、Android 1.6(API レベル 4)から可能になり、Android 4.0(API レベル 14)でその機能が大幅に改善されました。Android サポート ライブラリも Android 4.0 のリリース時に更新され、ユーザー補助の拡張された機能のサポートが Android 1.6 にまでさかのぼって提供されています。幅広い互換性を持つユーザー補助サービスの提供を目指すデベロッパーは、サポート ライブラリを使用することをおすすめします。さらに Android 4.0 で導入された高度なユーザー補助機能向けの開発もおすすめします。

ユーザー補助サービスを作成する

ユーザー補助サービスは、通常のアプリにバンドルするか、スタンドアロンの 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() {
    }

...
}

このサービスの新しいプロジェクトを作成済みで、サービスにアプリを関連付ける予定がない場合は、ソースからスターター アクティビティのクラスを削除できます。

マニフェストの宣言と権限

ユーザー補助サービスを提供するアプリでは、Android システムでユーザー補助サービスとして扱われるための特定の宣言を、そのアプリのマニフェストに含める必要があります。このセクションでは、ユーザー補助サービスで必須の設定と省略可能な設定について説明します。

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

ユーザー補助サービスとして扱われるためには、マニフェストの application 要素内に activity 要素ではなく service 要素を含める必要があります。また、service 要素内には、ユーザー補助サービスのインテント フィルタを含める必要もあります。Android 4.1 以上との互換性を保つには、システムだけがサービスをバインドできるように、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>

これらの宣言は、Android 1.6(API レベル 4)以上にデプロイされるすべてのユーザー補助サービスに必須です。

ユーザー補助サービスの設定

ユーザー補助サービスでは、扱うユーザー補助イベントのタイプと、サービスに関する追加情報を指定する設定も行う必要があります。ユーザー補助サービスの設定は、AccessibilityServiceInfo クラスに含まれます。サービスの構築と設定を行うには、ランタイムにこのクラスのインスタンスと setServiceInfo() を使用します。 ただし、このメソッドでは設定できないオプションもあります。

Android 4.0 以降では、マニフェストに <meta-data> 要素を含めて、設定ファイルへの参照を指定できます。設定ファイルには、次の例に示すように、ユーザー補助サービスのどのオプションも設定できます。

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

このメタデータ要素は、アプリのリソース ディレクトリ(<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
        // won't be passed to this service.
        eventTypes = AccessibilityEvent.TYPE_VIEW_CLICKED or AccessibilityEvent.TYPE_VIEW_FOCUSED

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

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

        // Default services are invoked only if no package-specific ones are present
        // for the type of AccessibilityEvent generated. This service *is*
        // application-specific, so the flag isn't necessary. If this was a
        // general-purpose service, it would be worth considering 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
    // won't be passed to this service.
    info.eventTypes = AccessibilityEvent.TYPE_VIEW_CLICKED |
            AccessibilityEvent.TYPE_VIEW_FOCUSED;

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

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

    // Default services are invoked only if no package-specific ones are present
    // for the type of AccessibilityEvent generated. This service *is*
    // application-specific, so the flag isn't necessary. If this was a
    // general-purpose service, it would be worth considering setting the
    // DEFAULT flag.

    // info.flags = AccessibilityServiceInfo.DEFAULT;

    info.notificationTimeout = 100;

    this.setServiceInfo(info);

}

もう 1 つは、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 ファイルを参照してください。それには、サービスの宣言に XML ファイルを指す <meta-data> タグを指定します。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 が検出されたときに、このメソッドが呼び出されます。たとえば、ユーザーがボタンをクリックしたときや、ユーザー補助サービスがフィードバックを提供しているアプリ内のユーザー インターフェース(UI)コントロールがフォーカスされたときです。この場合、システムはこのメソッドを呼び出して、関連する AccessibilityEvent を渡します。サービスはそれを解釈し、ユーザーにフィードバックを提供するのに使用します。このメソッドは、サービスのライフサイクル全体で何度も呼び出されることがあります。
  • onInterrupt() -(必須)サービスが提供するフィードバックをシステムが中断しようとしたときに、このメソッドが呼び出されます。通常は、別のコントロールにフォーカスを動かすような、ユーザー アクションに応じるときです。このメソッドは、サービスのライフサイクル全体で何度も呼び出されることがあります。
  • onUnbind() -(省略可)システムがユーザー補助サービスをシャットダウンしようとするときにこのメソッドが呼び出されます。このメソッドを使って、1 回限りのシャットダウン手順を行い、オーディオ マネージャーやデバイス バイブレータのような、ユーザー フィードバック システム サービスの割り当て解除などを行います。

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

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

ユーザー補助サービスの設定パラメータの最も重要な機能の 1 つは、そのサービスで処理できるユーザー補助イベントのタイプを指定できることです。この情報を指定することで、ユーザー補助サービス間の連携が可能になり、デベロッパーは各アプリから特定のイベントタイプのみを柔軟に処理できます。そのためのイベント フィルタには次の条件を指定できます。

  • パッケージ名 - そのサービスで扱うユーザー補助イベントが属すアプリのパッケージ名を指定します。このパラメータを省略すると、そのユーザー補助サービスはすべてのアプリのユーザー補助イベントにサービスを提供できるとみなされます。このパラメータは、ユーザー補助サービスの設定ファイル内に 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);
        }
    }
}

詳細については、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)以上を搭載するデバイスでは、ナビゲーション バーの右側に「ユーザー補助機能ボタン」があります。ユーザーがこのボタンを押すと、画面に現在表示されているコンテンツに応じて、有効になっているユーザー補助の機能やサービスのいずれかを起動できます。

ユーザー補助機能ボタンを使用してユーザーが特定のサービスを起動できるようにするには、そのサービスで FLAG_REQUEST_ACCESSIBILITY_BUTTON フラグを AccessibilityServiceInfo オブジェクトの android:accessibilityFlags 属性に追加する必要があります。これにより、そのサービスは registerAccessibilityButtonCallback() を使用するコールバックを登録できます。

注: この機能は、ソフトウェア レンダリングによるナビゲーション領域を提供するデバイスでのみ使用できます。サービスは onAvailabilityChanged() を実装し、ユーザー補助機能ボタンが利用可能かどうかに基づいて、常時 isAccessibilityButtonAvailable() を使って変更に対応する必要があります。それにより、ユーザー補助機能ボタンがサポートされていない場合や、利用できなくなった場合でも、ユーザーはいつでもサービスの機能にアクセスできます。

次のコード スニペットでは、ユーザー補助機能ボタンを押したユーザーに応じるように、ユーザー補助サービスを設定する方法を示します。

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_FINGERPRINT の権限と 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"
    ... />

MyFingerprintGestureService.java

Kotlin

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

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 分 3 秒からご覧ください。

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

Android 8.0(API レベル 26)のテキスト読み上げ(TTS)サービスでは、1 つのテキスト ブロック内の各フレーズで複数の言語を識別して読み上げることができます。 ユーザー補助サービスでこの自動言語切り替え機能を有効にするには、次のコード スニペットに示すように、すべての文字列を 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 秒からご覧ください。

ユーザーの代わりにアクションを行う

Android 4.0(API レベル 14)以降、ユーザー補助サービスがユーザーの代わりに、入力フォーカスの変更や、UI 要素の選択(有効化)などのアクションを行えます。Android 4.1(API レベル 16)では、アクションの範囲が拡大され、リストのスクロールや、テキスト欄での操作が含まれるようになりました。ユーザー補助サービスでは、ホーム画面に移動する、戻るボタンを押す、通知画面や最近使ったアプリのリストを開くなどの、グローバル アクションも可能です。Android 4.1 には新しいタイプのフォーカスである「ユーザー補助フォーカス」も導入され、すべての視覚要素がユーザー補助サービスから選択できるようになりました。

これらの新しい機能により、ユーザー補助機能のデベロッパーはジェスチャー ナビゲーションのような代替ナビゲーション モードを作成して、障がいのあるユーザーに Android デバイスの改善されたコントロールを提供できます。

ジェスチャーをリッスンする

ユーザー補助サービスは、特定のジェスチャーをリッスンして応答し、ユーザーの代わりにアクションを行うことができます。この機能は、Android 4.1(API レベル 16)で追加されており、ユーザー補助サービスはタッチガイド機能の有効化をリクエストする必要があります。サービスからこの有効化をリクエストするには、次の例に示すように、サービスの 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);
}

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

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

ユーザー補助サービスは、ユーザーの代わりにアクションを行うことで、アプリの操作をよりシンプルにして生産性を高めることができます。ユーザー補助サービスがアクションを実行する機能は、Android 4.0(API レベル 14)で導入され、Android 4.1(API レベル 16)で大幅に拡張されました。

ユーザーに代わってアクションを行うには、ユーザー補助サービスが、数個のまたは多数のアプリからイベントを受け取るよう登録して、アプリのコンテンツを表示する権限をリクエストする必要があります。それには、サービス設定ファイル内で 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

            // take action 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

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

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

performAction() メソッドを使用すると、アプリ内でサービスがアクションを行えます。ホーム画面に移動する、戻るボタンを押す、通知画面や最近使ったアプリのリストを開くなどのグローバル アクションを行う必要がある場合は、performGlobalAction() メソッドを使用します。

フォーカス タイプを使用する

Android 4.1(API レベル 16)では、「ユーザー補助フォーカス」と呼ばれる、新しいタイプの UI フォーカスが導入されています。ユーザー補助サービスでは、このタイプのフォーカスを使用して、表示されている UI 要素を選択して操作できます。このフォーカス タイプは、以前からある「入力フォーカス」とは異なります。入力フォーカスは、ユーザーがキーボード上の文字を入力して Enter キーを押すか、D-pad コントロールの中央ボタンを押したときに、画面上のどの UI 要素が入力を受け取るかを決定します。

ユーザー補助フォーカスは、入力フォーカスとは完全に別個の独立したものです。実際、UI 上の要素のいずれかに入力フォーカスがあるときに、それとは別の要素にユーザー補助フォーカスを当てることができます。ユーザー補助フォーカスの目的は、システム上の入力フォーカスがどの要素にあるかにかかわらず、ユーザー補助サービスが画面に表示されている任意の要素を操作できるようにすることです。ユーザー補助ジェスチャーをテストすると、ユーザー補助フォーカスの実際の動作を確認できます。この機能のテストについて詳しくは、ジェスチャー ナビゲーションのテストに関する説明をご覧ください。

注: ユーザー補助フォーカスを使用するユーザー補助サービスは、フォーカスされた要素が入力フォーカスに対応している場合に、現在の入力フォーカスを同期させる必要があります。入力フォーカスとユーザー補助フォーカスを同期させないと、特定のアクションが行われたときに、入力フォーカスが特定の場所にあることを想定しているアプリ内で問題が発生するおそれがあります。

ユーザー補助サービスは AccessibilityNodeInfo.findFocus() メソッドを使って、入力フォーカスまたはユーザー補助フォーカスがどの UI 要素にあるかを判断できます。また、focusSearch() メソッドを使って、入力フォーカスで選択できる要素を検索することもできます。 さらに、ユーザー補助サービスは performAction(AccessibilityNodeInfo.ACTION_SET_ACCESSIBILITY_FOCUS) メソッドを使ってユーザー補助フォーカスを設定できます。

情報を入手する

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

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

Android システムは UI の操作に関する情報を AccessibilityEvent オブジェクトを介してユーザー補助サービスに提供します。Android 4.0 より前のシステムでのユーザー補助イベントでは、ユーザーが選択した UI コントロールについて詳細な情報が提供されましたが、コンテキストに関して利用できる情報は限られていました。こうしたコンテキスト情報がないために、選択されたコントロールの意味を理解できない場合もよくありました。

コンテキストが重要なインターフェースの例としては、カレンダーや予定表があります。ユーザーが月曜日から金曜日までのリストの午後 4 時の時間帯を選択し、ユーザー補助サービスが「午後 4 時」と読み上げても、曜日や日付についての通知がないと、そのフィードバックは理解しにくいものになります。この場合、UI コントロールのコンテキストは会議のスケジュールを設定しようとするユーザーにとってとても重要です。

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

ユーザー補助サービスは、UI イベントに関する情報を、システムからサービスの onAccessibilityEvent() コールバック メソッドに渡される AccessibilityEvent から取得します。このオブジェクトは、操作対象となったオブジェクトの種類、説明テキストなど、イベントに関する詳細を提供します。Android 4.0 以降では(および、それ以前のリリースではサポート ライブラリの AccessibilityEventCompat オブジェクトを介したサポートによって)、以下のような呼び出しを使用して、イベントに関する追加情報を取得できます。

  • AccessibilityEvent.getRecordCount()getRecord(int) - これらのメソッドを使用すると、AccessibilityRecord オブジェクトのセットが取得できます。これらのオブジェクトは、システムから渡される AccessibilityEvent に付与されます。この詳細レベルでは、ユーザー補助サービスをトリガーしたイベントの詳細なコンテキストが提供されます。
  • AccessibilityEvent.getSource() - このメソッドは AccessibilityNodeInfo オブジェクトを返します。このオブジェクトを使用すると、ユーザー補助イベントを発生させたコンポーネントのビュー レイアウト階層(親と子)をリクエストできます。 この機能を使用すると、コンポーネントを囲むビューや子ビューのコンテンツや状態など、イベントの完全なコンテキストをユーザー補助サービスで調べることができます。

    重要: AccessibilityEvent からビュー階層を調べる機能を使用すると、ユーザー補助サービスにユーザーの個人情報が開示されるおそれがあります。そのため、ユーザー補助サービスではこのレベルのアクセス権をリクエストする必要があります。リクエストするには、ユーザー補助サービスの設定 XML ファイルに canRetrieveWindowContent 属性を含めて true に設定します。サービスの設定 XML ファイルにこの設定を含めない場合、getSource() の呼び出しが失敗します。

    注: Android 4.1(API レベル 16)以上では、getSource() メソッドだけでなく AccessibilityNodeInfo.getChild()getParent() も、ユーザー補助機能にとって重要と考えられるビュー オブジェクトのみ返します(コンテンツを描画するビューまたはユーザー アクションに応答するビュー)。すべてのビューが必要になるサービスは、そのサービスの AccessibilityServiceInfo インスタンスの flags メンバーを FLAG_INCLUDE_NOT_IMPORTANT_VIEWS に設定することでリクエストできます。

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

Android 9(API レベル 28)以上では、アプリが複数のウィンドウを同時に再描画したときにウィンドウの更新を追跡できます。TYPE_WINDOWS_CHANGED イベントが発生した場合、getWindowChanges() API を使って、ウィンドウがどのように変わったかを判断します。マルチウィンドウの更新の際、各ウィンドウが独自のイベントセットを生成します。getSource() メソッドは、各イベントに関連付けられたウィンドウのルートビューを返します。

アプリが View オブジェクトに対するユーザー補助機能のペインタイトルを定義している場合、サービスではアプリの UI がいつ更新されたかを認識できます。TYPE_WINDOW_STATE_CHANGED イベントが発生したとき、getContentChangeTypes() が返すタイプを使って、ウィンドウがどのように変わったかを判断します。このフレームワークでは、たとえば、ペインのタイトルが新しくなったときや、ペインが消えたときを検出できます。

ユーザー補助ノードの詳細を収集する

このステップは省略可能ですが、非常に有用です。Android プラットフォームには、AccessibilityService がビュー階層を照会する機能があり、イベントを生成した UI コンポーネントとその親と子に関する情報を収集します。これを行うには、必ず XML 設定に次の行を指定します。

android:canRetrieveWindowContent="true"

指定した後に、getSource() を使って AccessibilityNodeInfo オブジェクトを取得します。イベントが発生したウィンドウがまだアクティブなウィンドウの場合のみ、この呼び出しはオブジェクトを返します。そうでない場合、null が返されるので、それに応じて動作します。下記の例は、イベントを受け取ると以下を行うコード スニペットです。

  1. 直ちに、イベントが発生したビューの親を取得します。
  2. そのビューで、子ビューとなるラベルとチェックボックスを探します。
  3. ラベルとチェックボックスが見つかった場合は、ラベルと、チェックボックスがオンかどうかを示す文字列を作成してユーザーに伝えます。
  4. ビュー階層を走査中に 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 fired 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 or not it's complete, based on
    // the text inside the label, and the state of the check-box.
    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 fired 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 or not it's complete, based on
    // the text inside the label, and the state of the check-box.
    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() を使ってツールチップのテキストを読み取り、ACTION_SHOW_TOOLTIPACTION_HIDE_TOOLTIP を使って View のインスタンスにツールチップを表示または非表示にするよう指示します。

ヒントのテキスト

Android 8.0(API レベル 26)には、テキストベースのオブジェクトのヒントテキストを操作するためのメソッドがいくつか用意されています。

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

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

Android 8.0(API レベル 26)以上を搭載するデバイス上でユーザー補助サービスは、表示される各文字の境界ボックスの画面座標を TextView ウィジェット内で判断できます。この座標を調べるには、refreshWithExtraData() を呼び出して EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY を最初の引数とし、Bundle オブジェクトを 2 番目の引数として渡します。このメソッドを実行すると、システムは Bundle 引数に Rect オブジェクトの Parcelable 配列を渡します。各 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);
    ...
}

参考情報

詳細については、以下のリソースをご覧ください。

コードラボ