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

ユーザー補助サービスは、障がいがあるユーザーや一時的にデバイスを十分に操作できないユーザーを支援するために、ユーザー インターフェースを拡張するアプリです。これらのサービスはバックグラウンドで実行され、システムと通信して画面のコンテンツを検査し、ユーザーに代わってアプリを操作します。たとえば、スクリーン リーダー(TalkBack など)、スイッチ アクセス ツール、音声操作システムなどがあります。

このガイドでは、Android ユーザー補助サービスの構築の基本について説明します。

ユーザー補助サービスのライフサイクル

ユーザー補助サービスを作成するには、AccessibilityService クラスを拡張し、アプリのマニフェストでサービスを宣言する必要があります。

サービス クラスを作成する

AccessibilityService を拡張するクラスを作成します。次のメソッドをオーバーライドする必要があります。

  • onAccessibilityEvent: サービスの設定に一致するイベント(フォーカスの変更やボタンのクリックなど)がシステムで検出されたときに呼び出されます。サービスがユーザー インターフェースを解釈する場所です。
  • onInterrupt: システムがサービスのフィードバックを中断したときに呼び出されます(たとえば、ユーザーがフォーカスをすばやく移動したときに音声出力を停止する場合など)。
package com.example.android.apis.accessibility

import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.AccessibilityServiceInfo
import android.accessibilityservice.FingerprintGestureController
import android.accessibilityservice.AccessibilityButtonController
import android.accessibilityservice.GestureDescription
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.graphics.Path
import android.os.Build
import android.media.AudioManager
import android.content.Context

class MyAccessibilityService : AccessibilityService() {

    override fun onAccessibilityEvent(event: AccessibilityEvent) {
        // Interpret the event and provide feedback to the user
    }

    override fun onInterrupt() {
        // Interrupt any ongoing feedback
    }

    override fun onServiceConnected() {
        // Perform initialization here
    }
}

マニフェストで宣言する

AndroidManifest.xml ファイルにサービスを登録します。システムのみがサービスにバインドできるように、BIND_ACCESSIBILITY_SERVICE 権限を厳密に適用する必要があります。

設定ボタンが機能するように、ServiceSettingsActivity を宣言します。

<application>
  <service android:name=".accessibility.MyAccessibilityService"
      android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
      android:exported="true"
      android:label="@string/accessibility_service_label">
      <intent-filter>
          <action android:name="android.accessibilityservice.AccessibilityService" />
      </intent-filter>
      <meta-data
          android:name="android.accessibilityservice"
          android:resource="@xml/accessibility_service_config" />
  </service>

  <activity android:name=".accessibility.ServiceSettingsActivity"
      android:exported="true"
      android:label="@string/accessibility_service_settings_label" />
</application>

サービスを構成する

res/xml/accessibility_service_config.xml に構成ファイルを作成します。このファイルでは、サービスが処理するイベントと提供するフィードバックを定義します。マニフェストで宣言した ServiceSettingsActivity を参照してください。

<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/accessibility_service_description"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFlags="flagDefault|flagRequestFingerprintGestures|flagRequestAccessibilityButton"
    android:accessibilityFeedbackType="feedbackSpoken"
    android:notificationTimeout="100"
    android:canRetrieveWindowContent="true"
    android:canPerformGestures="true"
    android:settingsActivity="com.example.android.apis.accessibility.ServiceSettingsActivity" />

構成ファイルには、次のキー属性が含まれます。

  • android:accessibilityEventTypes: 受信するイベント。汎用サービスには typeAllMask を使用します。
  • android:canRetrieveWindowContent: サービスが UI 階層を検査する必要がある場合(画面からテキストを読み取る場合など)、true にする必要があります。
  • android:canPerformGestures: ジェスチャー(スワイプやタップなど)をプログラムでディスパッチする場合は、true にする必要があります。
  • android:accessibilityFlags: フラグを組み合わせて機能を有効にします。指紋ジェスチャーには flagRequestFingerprintGestures が必要です。ソフトウェアのユーザー補助機能ボタンには flagRequestAccessibilityButton が必要です。

構成オプションの完全なリストについては、AccessibilityServiceInfo をご覧ください。

実行時構成

XML 構成は静的ですが、実行時にサービス構成を動的に変更することもできます。これは、ユーザー設定に基づいて機能を切り替える場合に便利です。

onServiceConnected() をオーバーライドして、setServiceInfo() を使用してランタイム アップデートを適用します。

override fun onServiceConnected() {
    val info = AccessibilityServiceInfo()

    // Set the type of events that this service wants to listen to.
    info.eventTypes = AccessibilityEvent.TYPE_VIEW_CLICKED or AccessibilityEvent.TYPE_VIEW_FOCUSED

    // Set the type of feedback your service provides.
    info.feedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN

    // Set flags at runtime.
    info.flags = AccessibilityServiceInfo.FLAG_DEFAULT or
            AccessibilityServiceInfo.FLAG_REQUEST_FINGERPRINT_GESTURES

    this.setServiceInfo(info)
}

UI コンテンツを解釈する

onAccessibilityEvent() がトリガーされると、システムは AccessibilityEvent を提供します。このイベントは、画面コンテンツの階層表現であるユーザー補助ツリーへのエントリ ポイントとして機能します。

サービスは、主に AccessibilityNodeInfo オブジェクトとやり取りします。このオブジェクトは、ボタン、リスト、テキストなどの UI 要素を表します。これらの UI 要素に関するデータは AccessibilityNodeInfo に正規化されます。

次の例は、イベントのソースを取得し、ユーザー補助ツリーをトラバースして情報を検索する方法を示しています。

override fun onAccessibilityEvent(event: AccessibilityEvent) {
    // Get the source node of the event
    val sourceNode: AccessibilityNodeInfo? = event.source

    if (sourceNode == null) return

    // Inspect properties
    if (sourceNode.isCheckable) {
        val state = if (sourceNode.isChecked) "Checked" else "Unchecked"
        val label = sourceNode.text ?: sourceNode.contentDescription
        
        // Provide feedback (for example, speak to the user)
        speakToUser("$label is $state")
    }

    // Always recycle nodes to prevent memory leaks
    sourceNode.recycle()
}

private fun speakToUser(text: String) {
    // Your text-to-speech implementation goes here
}

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

ユーザー補助サービスは、ユーザーに代わってボタンのクリックやリストのスクロールなどの操作を行うことができます。

アクションを実行するには、AccessibilityNodeInfo オブジェクトで performAction() を呼び出します。

fun performClick(node: AccessibilityNodeInfo) {
    if (node.isClickable) {
        node.performAction(AccessibilityNodeInfo.ACTION_CLICK)
    }
}

システム全体に影響するグローバル アクション(戻るボタンを押す、通知シェードを開くなど)には、performGlobalAction() を使用します。

// Navigate back
fun navigateBack() {
    performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK)
}

フォーカスを管理する

Android には、入力フォーカス(キーボード入力の送信先)とユーザー補助フォーカス(ユーザー補助サービスが検査しているもの)の 2 種類のフォーカスがあります。

次のスニペットは、現在アクセシビリティ フォーカスがある要素を見つける方法を示しています。

// Find the node that currently has accessibility focus
// Note: rootInActiveWindow can be null if the window is not available
val root = rootInActiveWindow
if (root != null) {
    val focusedNode = root.findFocus(AccessibilityNodeInfo.FOCUS_ACCESSIBILITY)

    // Do something with focusedNode

    // Always recycle nodes
    focusedNode?.recycle()
    // rootInActiveWindow doesn't need to be recycled, but obtained nodes do.
}

次のスニペットは、ユーザー補助フォーカスを特定の要素に移動する方法を示しています。

// Request that the system give focus to a given node
fun focusNode(node: AccessibilityNodeInfo) {
    node.performAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS)
}

ユーザー補助サービスを作成する際は、ユーザーのフォーカス状態を尊重し、ユーザー操作によって明示的にトリガーされない限り、フォーカスを奪わないようにします。

操作の実行

サービスは、スワイプ、タップ、マルチタッチ操作などのカスタム ジェスチャーを画面にディスパッチできます。これを行うには、dispatchGesture() API を使用できるように、構成で android:canPerformGestures="true" を宣言します。

シンプルなジェスチャー

簡単なジェスチャーを実行するには、まず、特定のジェスチャーに関連付けられた動きを表す Path オブジェクトを作成します。次に、PathGestureDescription でラップしてストロークを記述します。最後に、dispatchGesture を呼び出してジェスチャーをディスパッチします。

fun swipeRight() {
    // Create a path for the swipe (from x=100 to x=500)
    val swipePath = Path()
    swipePath.moveTo(100f, 500f)
    swipePath.lineTo(500f, 500f)

    // Build the stroke description (0ms delay, 500ms duration)
    val stroke = GestureDescription.StrokeDescription(swipePath, 0, 500)

    // Build the gesture description
    val gestureBuilder = GestureDescription.Builder()
    gestureBuilder.addStroke(stroke)

    // Dispatch the gesture
    dispatchGesture(gestureBuilder.build(), object : AccessibilityService.GestureResultCallback() {
        override fun onCompleted(gestureDescription: GestureDescription?) {
            super.onCompleted(gestureDescription)
            // Gesture finished successfully
        }
    }, null)
}

連続ジェスチャー

複雑な操作(L 字を描く、正確な複数ステップのドラッグなど)の場合、willContinue パラメータを使用してストロークを連結できます。

fun performLShapedGesture() {
    val path1 = Path().apply {
        moveTo(200f, 200f)
        lineTo(400f, 200f)
    }

    val path2 = Path().apply {
        moveTo(400f, 200f)
        lineTo(400f, 400f)
    }

    // First stroke: willContinue = true
    val stroke1 = GestureDescription.StrokeDescription(path1, 0, 500, true)

    // Second stroke: continues immediately after stroke1
    val stroke2 = stroke1.continueStroke(path2, 0, 500, false)

    val builder = GestureDescription.Builder()
    builder.addStroke(stroke1)
    builder.addStroke(stroke2)

    dispatchGesture(builder.build(), null, null)
}

音声管理

ユーザー補助サービス(特にスクリーン リーダー)を作成する場合は、STREAM_ACCESSIBILITY 音声ストリームを使用します。これにより、ユーザーはシステム メディアの音量とは別にサービスの音量を制御できます。

fun increaseAccessibilityVolume() {
    val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
    audioManager.adjustStreamVolume(
        AudioManager.STREAM_ACCESSIBILITY,
        AudioManager.ADJUST_RAISE,
        0
    )
}

構成に FLAG_ENABLE_ACCESSIBILITY_VOLUME フラグを必ず含めてください。XML で指定するか、実行時に setServiceInfo を使用して指定します。

高度な機能

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

Android 10(API レベル 29)以上を搭載したデバイスでは、サービスで指紋認証センサーでの方向スワイプをキャプチャできます。これは、代替のナビゲーション コントロールを提供する場合に便利です。

onServiceConnected() メソッドに次のロジックを追加します。

// Import: android.os.Build
// Import: android.accessibilityservice.FingerprintGestureController

private var gestureController: FingerprintGestureController? = null

override fun onServiceConnected() {
    // Check if the device is running Android 10 (Q) or higher
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        gestureController = fingerprintGestureController

        val callback = object : FingerprintGestureController.FingerprintGestureCallback() {
            override fun onGestureDetected(gesture: Int) {
                when (gesture) {
                    FingerprintGestureController.FINGERPRINT_GESTURE_SWIPE_DOWN -> {
                        // Handle swipe down
                    }
                    FingerprintGestureController.FINGERPRINT_GESTURE_SWIPE_UP -> {
                        // Handle swipe up
                    }
                }
            }
        }

        gestureController?.registerFingerprintGestureCallback(callback, null)
    }
}

ユーザー補助機能ボタン

ソフトウェア ナビゲーション キーを使用するデバイスでは、ユーザーはナビゲーション バーのユーザー補助機能ボタンからサービスを呼び出すことができます。

この機能を使用するには、サービス構成に FLAG_REQUEST_ACCESSIBILITY_BUTTON フラグを追加します。次に、登録ロジックを onServiceConnected() メソッドに追加します。

// Import: android.accessibilityservice.AccessibilityButtonController

override fun onServiceConnected() {
    // ... existing initialization code ...

    val controller = accessibilityButtonController

    controller.registerAccessibilityButtonCallback(
        object : AccessibilityButtonController.AccessibilityButtonCallback() {
            override fun onClicked(controller: AccessibilityButtonController) {
                // Respond to button tap
            }
        }
    )
}

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

テキストを読み上げるサービスは、ソーステキストに LocaleSpan タグが付いている場合、言語を自動的に切り替えることができます。これにより、サービスは手動で切り替えることなく、多言語コンテンツを正しく発音できます。

import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.style.LocaleSpan
import java.util.Locale

// Wrap text in LocaleSpan to indicate language
val spannable = SpannableStringBuilder("Bonjour")
spannable.setSpan(
    LocaleSpan(Locale.FRANCE),
    0,
    spannable.length,
    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)

サービスが AccessibilityNodeInfo を処理するときは、LocaleSpan オブジェクトの text プロパティを調べて、正しいテキスト読み上げ言語を特定します。

参考情報

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

ガイド

Codelab

コンテンツの閲覧