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

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

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)サービスでは、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;
}

詳しくは、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() を使用してツールチップのテキストを読み取り、ACTION_SHOW_TOOLTIPACTION_HIDE_TOOLTIP を使用して View のインスタンスに対してツールチップの表示 / 非表示を指示します。

ヒントのテキスト

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

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

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

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

参考情報

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

ガイド

Codelab