Android AccessibilityButtonController.java ソースコード解説

ソースコードの場所

以下のURLからAOSP(Android Open Source Project)のソースコードを参照できます:

https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/accessibilityservice/AccessibilityButtonController.java

ソースコード

package android.accessibilityservice;

import android.annotation.NonNull;
import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
import android.util.ArrayMap;
import android.util.Slog;

import java.util.Objects;

public final class AccessibilityButtonController {
    private static final String DEBUG_TAG = "A11yButtonController";

    private final IAccessibilityServiceConnection serviceConnection;
    private final Object syncLock;
    private ArrayMap<AccessibilityButtonCallback, Handler> callbackRegistry;

    AccessibilityButtonController(@NonNull IAccessibilityServiceConnection connection) {
        this.serviceConnection = connection;
        this.syncLock = new Object();
    }

    public boolean isAccessibilityButtonAvailable() {
        if (serviceConnection != null) {
            try {
                return serviceConnection.isAccessibilityButtonAvailable();
            } catch (RemoteException error) {
                Slog.w(DEBUG_TAG, "Failed to get accessibility button availability.", error);
                error.rethrowFromSystemServer();
                return false;
            }
        }
        return false;
    }

    public void registerAccessibilityButtonCallback(@NonNull AccessibilityButtonCallback callback) {
        registerAccessibilityButtonCallback(callback, new Handler(Looper.getMainLooper()));
    }
    
    public void registerAccessibilityButtonCallback(@NonNull AccessibilityButtonCallback callback,
            @NonNull Handler callbackHandler) {
        Objects.requireNonNull(callback);
        Objects.requireNonNull(callbackHandler);
        synchronized (syncLock) {
            if (callbackRegistry == null) {
                callbackRegistry = new ArrayMap<>();
            }
            callbackRegistry.put(callback, callbackHandler);
        }
    }

    public void unregisterAccessibilityButtonCallback(
            @NonNull AccessibilityButtonCallback callback) {
        Objects.requireNonNull(callback);
        synchronized (syncLock) {
            if (callbackRegistry == null) {
                return;
            }
            final int targetIndex = callbackRegistry.indexOfKey(callback);
            final boolean exists = targetIndex >= 0;
            if (exists) {
                callbackRegistry.removeAt(targetIndex);
            }
        }
    }
    
    void dispatchAccessibilityButtonClicked() {
        final ArrayMap<AccessibilityButtonCallback, Handler> copiedRegistry;
        synchronized (syncLock) {
            if (callbackRegistry == null || callbackRegistry.isEmpty()) {
                Slog.w(DEBUG_TAG, "Received accessibility button click with no callbacks!");
                return;
            }
            copiedRegistry = new ArrayMap<>(callbackRegistry);
        }

        for (int index = 0, totalCount = copiedRegistry.size(); index < totalCount; index++) {
            final AccessibilityButtonCallback targetCallback = copiedRegistry.keyAt(index);
            final Handler targetHandler = copiedRegistry.valueAt(index);
            targetHandler.post(() -> targetCallback.onClicked(this));
        }
    }

    void dispatchAccessibilityButtonAvailabilityChanged(boolean available) {
        final ArrayMap<AccessibilityButtonCallback, Handler> copiedRegistry;
        synchronized (syncLock) {
            if (callbackRegistry == null || callbackRegistry.isEmpty()) {
                Slog.w(DEBUG_TAG,
                        "Received accessibility button availability change with no callbacks!");
                return;
            }
            copiedRegistry = new ArrayMap<>(callbackRegistry);
        }

        for (int index = 0, totalCount = copiedRegistry.size(); index < totalCount; index++) {
            final AccessibilityButtonCallback targetCallback = copiedRegistry.keyAt(index);
            final Handler targetHandler = copiedRegistry.valueAt(index);
            targetHandler.post(() -> targetCallback.onAvailabilityChanged(this, available));
        }
    }

    public static abstract class AccessibilityButtonCallback {
        public void onClicked(AccessibilityButtonController controller) {}
        public void onAvailabilityChanged(AccessibilityButtonController controller,
                boolean available) {}
    }
}

メンバー変数の詳細分析

定数とサービス接続

    private static final String DEBUG_TAG = "A11yButtonController";
    private final IAccessibilityServiceConnection serviceConnection;
    private final Object syncLock;
    private ArrayMap<AccessibilityButtonCallback, Handler> callbackRegistry;

DEBUG_TAGはログ出力時に使用する識別子です。「A11y」という略称は「Accessibility」の省略形で、ログフィルタリングに活用されます。

serviceConnectionは無障碍サービスとシステム間の通信を担当するインターフェースです。このフィールドがfinalで宣言されているのは、インスタンス生成後に変更されることがないためです。

syncLockはスレッドセーフティを確保するための同期オブジェクトです。マルチスレッド環境において、コールバック一覧の整合性を保つために使用されます。

callbackRegistryは登録されたコールバックとそれに対応するHandlerのMappingを保持します。ArrayMapを使用することで、キーの重複防止と効率的なルックアップを実現しています。

コンストラクタ

    AccessibilityButtonController(@NonNull IAccessibilityServiceConnection connection) {
        this.serviceConnection = connection;
        this.syncLock = new Object();
    }

コンストラクタでは、IAccessibilityServiceConnectionの実装を注入することで、AccessibilityButtonControllerがシステムサービスと通信できる状態にします。同期ロックオブジェクトも同時に初期化され、後続の操作でスレッドセーフティを確保できます。

メソッドの詳細分析

isAccessibilityButtonAvailable メソッド

    public boolean isAccessibilityButtonAvailable() {
        if (serviceConnection != null) {
            try {
                return serviceConnection.isAccessibilityButtonAvailable();
            } catch (RemoteException error) {
                Slog.w(DEBUG_TAG, "Failed to get accessibility button availability.", error);
                error.rethrowFromSystemServer();
                return false;
            }
        }
        return false;
    }

このメソッドは、現在の状態においてアクセシビリティボタンが操作可能であるかを判定します。実装の流れとして、まずサービス接続の存在を確認し、IPC通信を通じてシステムから可用性情報を取得します。RemoteExceptionが発生した場合は、ログ出力後に例外を再スローし、finally的な意味合いでfalseを返します。

この設計により、接続障害時にはクラッシュを防ぐとともに、呼び出し元に無効な状態であることを明確に伝達できます。

registerAccessibilityButtonCallback メソッド(簡易版)

    public void registerAccessibilityButtonCallback(@NonNull AccessibilityButtonCallback callback) {
        registerAccessibilityButtonCallback(callback, new Handler(Looper.getMainLooper()));
    }

このオーバーロード版は、呼び出し元がHandlerを意識することなくコールバック登録を行えるようにするためのシンタックスシュガーです。内部的にはメインネループにバインドされたHandlerを自動生成し、もう一方のメソッドに処理を委譲します。

registerAccessibilityButtonCallback メソッド(完全版)

    public void registerAccessibilityButtonCallback(@NonNull AccessibilityButtonCallback callback,
            @NonNull Handler callbackHandler) {
        Objects.requireNonNull(callback);
        Objects.requireNonNull(callbackHandler);
        synchronized (syncLock) {
            if (callbackRegistry == null) {
                callbackRegistry = new ArrayMap<>();
            }
            callbackRegistry.put(callback, callbackHandler);
        }
    }

このメソッドでは、引数のNULLチェックをObjects.requireNonNullで行い、不正な引数を早期に検出します。同期ブロック内でcallbackRegistryの初期化と値の追加を行うことで、データ競合を防止します。

unregisterAccessibilityButtonCallback メソッド

    public void unregisterAccessibilityButtonCallback(
            @NonNull AccessibilityButtonCallback callback) {
        Objects.requireNonNull(callback);
        synchronized (syncLock) {
            if (callbackRegistry == null) {
                return;
            }
            final int targetIndex = callbackRegistry.indexOfKey(callback);
            final boolean exists = targetIndex >= 0;
            if (exists) {
                callbackRegistry.removeAt(targetIndex);
            }
        }
    }

登録解除処理では、対象キーの存在をindexOfKeyで確認した後、存在する場合のみremoveAtで削除します。callbackRegistryがNULLの場合は早期リターンし、無意味な操作を回避します。

dispatchAccessibilityButtonClicked メソッド

    void dispatchAccessibilityButtonClicked() {
        final ArrayMap<AccessibilityButtonCallback, Handler> copiedRegistry;
        synchronized (syncLock) {
            if (callbackRegistry == null || callbackRegistry.isEmpty()) {
                Slog.w(DEBUG_TAG, "Received accessibility button click with no callbacks!");
                return;
            }
            copiedRegistry = new ArrayMap<>(callbackRegistry);
        }

        for (int index = 0, totalCount = copiedRegistry.size(); index < totalCount; index++) {
            final AccessibilityButtonCallback targetCallback = copiedRegistry.keyAt(index);
            final Handler targetHandler = copiedRegistry.valueAt(index);
            targetHandler.post(() -> targetCallback.onClicked(this));
        }
    }

クリックイベントの配信において重要なのは、コールバックが自身を削除する可能性があるため、同期ブロックの外でコピーを作成する点です。これにより、反復処理中のConcurrentModificationExceptionを防止します。各Handlerのpostメソッドを通じて、非同期的にコールバックが実行されます。

dispatchAccessibilityButtonAvailabilityChanged メソッド

    void dispatchAccessibilityButtonAvailabilityChanged(boolean available) {
        final ArrayMap<AccessibilityButtonCallback, Handler> copiedRegistry;
        synchronized (syncLock) {
            if (callbackRegistry == null || callbackRegistry.isEmpty()) {
                Slog.w(DEBUG_TAG,
                        "Received accessibility button availability change with no callbacks!");
                return;
            }
            copiedRegistry = new ArrayMap<>(callbackRegistry);
        }

        for (int index = 0, totalCount = copiedRegistry.size(); index < totalCount; index++) {
            final AccessibilityButtonCallback targetCallback = copiedRegistry.keyAt(index);
            final Handler targetHandler = copiedRegistry.valueAt(index);
            targetHandler.post(() -> targetCallback.onAvailabilityChanged(this, available));
        }
    }

可用性変更イベントの配信も、クリックイベントと同様のパターンを採用しています。唯一の違いは、onAvailabilityChangedメソッドに可用性フラグ(available)が渡されることです。これにより、コールバックはアクセシビリティボタンの状態変化に応じて適応的な処理を実行できます。

AccessibilityButtonCallback 抽象クラス

    public static abstract class AccessibilityButtonCallback {
        public void onClicked(AccessibilityButtonController controller) {}
        public void onAvailabilityChanged(AccessibilityButtonController controller,
                boolean available) {}
    }

この抽象クラスは、アクセシビリティボタンに関連するイベントハンドラを定義するインターフェースとして機能します。staticネストクラスとして実装することで、外部からインスタンス化せずにサブクラスのみを生成できます。onClickedはボタン押下時、onAvailabilityChangedはボタン可用性の状態変化時にそれぞれ呼ばれます。

設計パターンの考察

AccessibilityButtonControllerの実装には、Androidフレームワークにおける複数の重要な設計原則が見られます。

まず、Observerパターンの適用により、複数のコンポーネントがアクセシビリティボタンのイベントを同時に受信できる構造が実現されています。callbackRegistryへの登録・解除メカニズムは、Publisher-Subscriberモデルそのものです。

次に、スレッドセーフティの確保において、synchronizedブロックとシャローコピーの組み合わせが巧みに使用されています。同期ブロックを最小化することでパフォーマンスへの影響を抑えつつ、データ整合性を保っています。

さらに、Handlerを通じた非同期実行により、IPC通信や長時間処理の影響を UI スレッドから隔離しています。これにより、アクセシビリティサービスの応答性を維持しながら、柔軟なイベント処理が可能になっています。

タグ: Android accessibility android-framework source-code arraymap

8月6日 14:40 投稿