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

コンセプトと Jetpack Compose の実装

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

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

ユーザー補助サービスは、通常のアプリにバンドルするか、スタンドアロンの Android プロジェクトとして作成することができます。サービスの作成手順はどちらの場合も同じです。

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

プロジェクト内で、AccessibilityService を拡張するクラスを作成します。

Kotlin

package com.example.android.apis.accessibility

import android.accessibilityservice.AccessibilityService
import android.view.accessibility.AccessibilityEvent

class MyAccessibilityService : AccessibilityService() {
...
    override fun onInterrupt() {}

    override fun onAccessibilityEvent(event: AccessibilityEvent?) {}
...
}

Java

package com.example.android.apis.accessibility;

import android.accessibilityservice.AccessibilityService;
import android.view.accessibility.AccessibilityEvent;

public class MyAccessibilityService extends AccessibilityService {
...
    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
    }

    @Override
    public void onInterrupt() {
    }

...
}

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

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

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

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

アプリをユーザー補助サービスとして扱うには、マニフェストの 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);

}

もう 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 ファイルを指す <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 が検出されたときに、このメソッドが呼び出されます。たとえば、ユーザーがボタンをタップしたときや、ユーザー補助サービスがフィードバックを提供しているアプリ内のユーザー インターフェース(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() を使用するコールバックを登録できます。

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

Kotlin

private var mAccessibilityButtonController: AccessibilityButtonController? = null
private var accessibilityButtonCallback:
        AccessibilityButtonController.AccessibilityButtonCallback? = null
private var mIsAccessibilityButtonAvailable: Boolean = false

override fun onServiceConnected() {
    mAccessibilityButtonController = accessibilityButtonController
    mIsAccessibilityButtonAvailable =
            mAccessibilityButtonController?.isAccessibilityButtonAvailable ?: false

    if (!mIsAccessibilityButtonAvailable) return

    serviceInfo = serviceInfo.apply {
        flags = flags or AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON
    }

    accessibilityButtonCallback =
        object : AccessibilityButtonController.AccessibilityButtonCallback() {
            override fun onClicked(controller: AccessibilityButtonController) {
                Log.d("MY_APP_TAG", "Accessibility button pressed!")

                // Add custom logic for a service to react to the
                // accessibility button being pressed.
            }

            override fun onAvailabilityChanged(
                    controller: AccessibilityButtonController,
                    available: Boolean
            ) {
                if (controller == mAccessibilityButtonController) {
                    mIsAccessibilityButtonAvailable = available
                }
            }
    }

    accessibilityButtonCallback?.also {
        mAccessibilityButtonController?.registerAccessibilityButtonCallback(it, null)
    }
}

Java

private AccessibilityButtonController accessibilityButtonController;
private AccessibilityButtonController
        .AccessibilityButtonCallback accessibilityButtonCallback;
private boolean mIsAccessibilityButtonAvailable;

@Override
protected void onServiceConnected() {
    accessibilityButtonController = getAccessibilityButtonController();
    mIsAccessibilityButtonAvailable =
            accessibilityButtonController.isAccessibilityButtonAvailable();

    if (!mIsAccessibilityButtonAvailable) {
        return;
    }

    AccessibilityServiceInfo serviceInfo = getServiceInfo();
    serviceInfo.flags
            |= AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON;
    setServiceInfo(serviceInfo);

    accessibilityButtonCallback =
        new AccessibilityButtonController.AccessibilityButtonCallback() {
            @Override
            public void onClicked(AccessibilityButtonController controller) {
                Log.d("MY_APP_TAG", "Accessibility button pressed!");

                // Add custom logic for a service to react to the
                // accessibility button being pressed.
            }

            @Override
            public void onAvailabilityChanged(
              AccessibilityButtonController controller, boolean available) {
                if (controller.equals(accessibilityButtonController)) {
                    mIsAccessibilityButtonAvailable = available;
                }
            }
        };

    if (accessibilityButtonCallback != null) {
        accessibilityButtonController.registerAccessibilityButtonCallback(
                accessibilityButtonCallback, null);
    }
}

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

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

Android 8.0(API レベル 26)以上を搭載するデバイス上のユーザー補助サービスでは、デバイスの指紋認証センサーでのスワイプとその方向(上下左右)に応じることができます。こうした操作に関するコールバックをサービスで受け取るように設定するには、以下の手順を行います。

  1. USE_BIOMETRIC 権限と CAPABILITY_CAN_REQUEST_FINGERPRINT_GESTURES 機能を宣言します。
  2. 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);
        }
    }
}

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

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

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

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

ユーザーに代わって操作する

2011 年以降、ユーザー補助サービスがユーザーの代わりに、入力フォーカスの変更や、UI 要素の選択(有効化)などのアクションを行えます。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);
}

詳細については、Android のユーザー補助の新機能に関する Google I/O 2017 セッション動画の 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 は「ユーザー補助フォーカス」と呼ばれる UI フォーカスを導入しました。ユーザー補助サービスでは、このフォーカスを使用して、表示されている UI 要素を選択して操作できます。このフォーカス タイプは、入力フォーカスとは異なります。入力フォーカスは、ユーザーがキーボード上の文字を入力して Enter キーを押すか、D-pad の中央ボタンを押したときに、画面上のどの UI 要素が入力を受け取るかを決定します。

UI 上の要素のいずれかに入力フォーカスがあるときに、それとは別の要素にユーザー補助フォーカスを当てることができます。ユーザー補助フォーカスの目的は、システム上の入力フォーカスがどの要素にあるかにかかわらず、ユーザー補助サービスが画面に表示されている任意の要素を操作できるようにすることです。ユーザー補助サービスがアプリの入力要素と正しくやり取りできるようにするには、アプリのユーザー補助機能をテストするためのガイドラインに沿って、一般的なアプリを使用しながらサービスをテストします。

ユーザー補助サービスは AccessibilityNodeInfo.findFocus() メソッドを使って、入力フォーカスまたはユーザー補助フォーカスがどの UI 要素にあるかを判断できます。また、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 は UI の操作に関する情報を AccessibilityEvent オブジェクトを介してユーザー補助サービスに提供します。以前の Android バージョンでは、ユーザー補助イベントでユーザーが選択した UI コントロールに関する詳細な情報が提供されていましたが、コンテキストに関する情報は限られていました。こうしたコンテキスト情報がないために、選択されたコントロールの意味を理解できない場合もよくありました。

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

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

ユーザー補助サービスは、UI イベントに関する情報を、システムからサービスの 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 を最初の引数とし、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);
    ...
}

参考情報

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

ガイド

Codelab