Android Tips #40 AccessibilityService でユーザー補助サービスを作ってみる

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

AccessibilityService とは

AccessibilityService とは障害者や高齢者などのハンディキャップのあるユーザーでもアプリを使うことができるように補助する機能のことです。 例えば Android に標準搭載されている TalkBack はお知らせ (Notification) やユーザーの操作を音声またはバイブレーションで フィードバックしてくれます。視覚にハンディキャップがあるユーザーにとって優しいサービスですよね。
今回はそんな AccessibilityService を使ったオリジナルのユーザー補助サービスの実装を通してユーザー補助サービスの作りかたを学びたいと思います!

ユーザー補助サービスを作ってみる

ユーザー補助サービスの概要についてざっくりわかったところで、早速作ってみましょう! 今回はサンプルということで操作内容を Toast で表示する機能を作りたいと思います。

1. AccessibilityService を作る

まずは AccessibilityService を継承した Service クラスを作ります。abstract なメソッドなど最低限な実装をすると以下のような感じになります。

package jp.classmethod.android.sample.accessibilityservice;

import android.accessibilityservice.AccessibilityService;
import android.support.v4.view.accessibility.AccessibilityEventCompat;
import android.view.accessibility.AccessibilityEvent;
import android.widget.Toast;

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

    @Override
    public void onInterrupt() {
    }

}

AccessibilityService#onAccessibilityEvent() はユーザーが操作したり Notification を受け取ったりといったユーザー補助できるアクションが実行されたときに実行されるメソッドです。音声で知らせたいときはこのタイミングで音を鳴らしたりします。また AccessibilityService#onInterrupt() はユーザー操作などによってこのサービス自体の提供が中断されたときに呼び出されます。処理が特に不要な場合は何も書かなくて良いです。
今回はどんなユーザーアクションが起きたか Toast で表示させたいと思うので AccessibilityService#onAccessibilityEvent() メソッドを以下のように実装してみます。

@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
    int type = event.getEventType();
    String typeName = "";
    switch (type) {
    // Notificationの表示に変更があったとき
    case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:
        typeName = "TYPE_NOTIFICATION_STATE_CHANGED";
        break;
    // View をタップしたとき
    case AccessibilityEvent.TYPE_VIEW_CLICKED:
        typeName = "TYPE_VIEW_CLICKED";
        break;
    // View にフォーカスがあたったとき
    case AccessibilityEvent.TYPE_VIEW_FOCUSED:
        typeName = "TYPE_VIEW_FOCUSED";
        break;
    // View をロングタップしたとき
    case AccessibilityEvent.TYPE_VIEW_LONG_CLICKED:
        typeName = "TYPE_VIEW_LONG_CLICKED";
        break;
    // View が選択されたとき
    case AccessibilityEvent.TYPE_VIEW_SELECTED:
        typeName = "TYPE_VIEW_SELECTED";
        break;
    // View のテキストが変更されたとき
    case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED:
        typeName = "TYPE_VIEW_TEXT_CHANGED";
        break;
    // 画面の表示に変更があったとき
    case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
        typeName = "TYPE_WINDOW_STATE_CHANGED";
        break;
    // アナウンスがあったとき
    // case AccessibilityEvent.TYPE_ANNOUNCEMENT:
    case AccessibilityEventCompat.TYPE_ANNOUNCEMENT:
        typeName = "TYPE_ANNOUNCEMENT";
        break;
    // ジェスチャーが終わったとき
    // case AccessibilityEvent.TYPE_GESTURE_DETECTION_END:
    case AccessibilityEventCompat.TYPE_GESTURE_DETECTION_END:
        typeName = "TYPE_GESTURE_DETECTION_END";
        break;
    // ジェスチャーが始まったとき
    // case AccessibilityEvent.TYPE_GESTURE_DETECTION_START:
    case AccessibilityEventCompat.TYPE_GESTURE_DETECTION_START:
        typeName = "TYPE_GESTURE_DETECTION_START";
        break;
    // タッチ探索が終わったとき
    // case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END:
    case AccessibilityEventCompat.TYPE_TOUCH_EXPLORATION_GESTURE_END:
        typeName = "TYPE_TOUCH_EXPLORATION_GESTURE_END";
        break;
    // タッチ探索が始まったとき
    // case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START:
    case AccessibilityEventCompat.TYPE_TOUCH_EXPLORATION_GESTURE_START:
        typeName = "TYPE_TOUCH_EXPLORATION_GESTURE_START";
        break;
    // タッチ操作が終わったとき
    // case AccessibilityEvent.TYPE_TOUCH_INTERACTION_END:
    case AccessibilityEventCompat.TYPE_TOUCH_INTERACTION_END:
        typeName = "TYPE_TOUCH_INTERACTION_END";
        break;
    // タッチ操作が始まったとき
    // case AccessibilityEvent.TYPE_TOUCH_INTERACTION_START:
    case AccessibilityEventCompat.TYPE_TOUCH_INTERACTION_START:
        typeName = "TYPE_TOUCH_INTERACTION_START";
        break;
    // View のアクセシビリティ・フォーカスがクリアされたとき
    // case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED:
    case AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED:
        typeName = "TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED";
        break;
    // View がアクセシビリティ・フォーカスされたとき
    // case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED:
    case AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED:
        typeName = "TYPE_VIEW_ACCESSIBILITY_FOCUSED";
        break;
    // View のホバーが始まったとき
    // case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER:
    case AccessibilityEventCompat.TYPE_VIEW_HOVER_ENTER:
        typeName = "TYPE_VIEW_HOVER_ENTER";
        break;
    // View のホバーが終わったとき
    // case AccessibilityEvent.TYPE_VIEW_HOVER_EXIT:
    case AccessibilityEventCompat.TYPE_VIEW_HOVER_EXIT:
        typeName = "TYPE_VIEW_HOVER_EXIT";
        break;
    // View をスクロールしたとき
    // case AccessibilityEvent.TYPE_VIEW_SCROLLED:
    case AccessibilityEventCompat.TYPE_VIEW_SCROLLED:
        typeName = "TYPE_VIEW_SCROLLED";
        break;
    // View のテキスト範囲が変更されたとき
    // case AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED:
    case AccessibilityEventCompat.TYPE_VIEW_TEXT_SELECTION_CHANGED:
        typeName = "TYPE_VIEW_TEXT_SELECTION_CHANGED";
        break;
    // View のテキストを横断したとき
    // case AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY:
    case AccessibilityEventCompat.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY:
        typeName = "TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY";
        break;
    // 画面内のコンテンツが変更されたとき
    // case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED:
    case AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED:
        typeName = "TYPE_WINDOW_CONTENT_CHANGED";
        break;
    default:
        typeName = "UNKNOWN_TYPE";
    }
    if (mToast == null) {
        mToast = Toast.makeText(getApplicationContext(), typeName, Toast.LENGTH_SHORT);
    } else {
        mToast.setText(typeName);
    }
    mToast.show();
}

ただひたすらにイベントタイプを取得して Toast に流しています。定数を参照しているところが AccessibilityEvent だったり AccessibilityEventCompat だったりしていますが AccessibilityEventCompat を使っている定数は Android 4.0 (APIレベル14) 以降に追加されたイベントタイプです。Android 4.0 で AccessibilityService の機能が大幅に追加されて、より多くのイベントがハンドリングできるようになりました。ですが Android 4.0 より前のバージョンでもアクセスできるよう Support Package の AccessibilityEventCompat の定数を使っています。これで Android 4.0 以降でもそれ以前でも動作する実装が書けます。

2. AccessibilityService の詳細情報を設定する

(超簡単な) AccessibilityService ができたので次に詳細情報を xml で設定します。 res/xml フォルダ内に xml ファイルを作り、以下のように accessibility-service タグに設定を記述していきます。この xml ファイルは後述する AndroidManifest で設定して読みこむようにします。

<?xml version="1.0" encoding="UTF-8" ?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFeedbackType="feedbackAllMask"
    android:accessibilityFlags="flagDefault"
    android:canRetrieveWindowContent="true"
    android:description="@string/description"
    />

3. AndroidManifest を定義する

最後に、サービスを AndroidManifest.xml で定義します。普通の Service と同じように application タグ内に service タグで定義します。intent-filter に android.accessibilityservice.AccessibilityService のアクションを入れておくと、アプリがユーザー補助サービスとして登録されます。また meta-data タグを作り、先ほど作成した xml ファイルを詳細情報として設定します。

<service
    android:name=".ToastAccessibilityService"
    android:label="AccessibilityServiceSample"
    android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
    <intent-filter>
        <action android:name="android.accessibilityservice.AccessibilityService" />
    </intent-filter>
    <meta-data
        android:name="android.accessibilityservice"
        android:resource="@xml/accessibillity_service_config" />
</service>

2013/12/18 Android 4.1 以降では android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" が必須でしたので追加しました。ご指摘いただいたむとう様、ありがとうございました!

また Android 4.1 以降は android.permission.BIND_ACCESSIBILITY_SERVICE が必要なので入れておきましょう。

<uses-permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE" />

4. 実行してみる

すごーーーく簡単なサービスができました。実際に使ってみましょう!まずは起動させます。もちろん Service だけなので何も起きません。。 ということで「設定>ユーザー補助」を開きます。

accessibility_service01

すると、ユーザー補助サービスの一覧が表示されます。その中にいま作ったアプリ名 (SampleAccessibilityService) があると思うので、タップします。

accessibility_service02

先ほど設定した説明が表示されています。トグルボタンを有効にするとサービスが実行されます!

accessibility_service03

ホーム画面はもちろん。どのようなアプリでも、画面をタップしたり表示を切り替えたりといったアクションが発生すると Toast を表示してくれます!下図はホームをスクロールしたときのキャプチャです。 Toast がやたらチラチラ表示されて邪魔ですが。。サンプルということでw

accessibility_service04

ソースコード

簡単なサンプルではありますが、今回実装したソースコードを github に公開しました。ぜひご覧ください♪

suwa-yuki/SampleAccessibilityService

まとめ

AccessibilityService で多くの操作をハンドリングできるので、そこからアイデア次第でさまざまなユーザー支援ができると思います。また "ユーザー補助機能" という名目ではありますが、この機能を使って面白いサービスも実現できそうですね(Notificationの通知をPCにお知らせするアプリなど)。ぜひアイデアを広げて面白い(または便利な)アプリを作ってみてください。

参考