Bluetooth の概要

Android プラットフォームでは Bluetooth ネットワーク スタックをサポートしているため、端末はワイヤレスで他の Bluetooth 端末とデータを交換できます。このアプリのフレームワークにより、Android Bluetooth API を介した Bluetooth 機能へのアクセスが可能になります。これらの API を使用して、アプリは他の Bluetooth 端末とワイヤレスで接続し、ポイントツーポイントおよびマルチポイントのワイヤレス機能を使用できます。

Bluetooth API を使用して、Android アプリは次の操作を実行できます。

  • 他の Bluetooth 端末をスキャンする
  • ペア設定された Bluetooth 端末のローカル Bluetooth アダプタを問い合わせる
  • RFCOMM チャンネルを確立する
  • Service Discovery を使用して他の端末に接続する
  • 他の端末とデータを送受信する
  • 複数の接続を管理する

このページでは、Classic Bluetooth について重点的に説明します。Classic Bluetooth は、ストリーミングや Android 端末間の通信など、電池の消費量が多い操作に適した選択肢です。省電力要件がある Bluetooth 端末向けに、Android 4.3(API レベル 18)で Bluetooth Low Energy の API サポートが導入されました。詳細については、Bluetooth Low Energy をご覧ください。

このドキュメントでは、Health Device Profile などのさまざまな Bluetooth プロファイルについて説明します。また、Android Bluetooth API を使用して Bluetooth 通信に不可欠な 4 つの主なタスクを完了する方法について説明します。そのタスクとは、Bluetooth の設定、ペア設定済みの端末またはローカルエリアで使用可能な端末の検索、端末の接続、端末間でのデータ通信です。

基本

Bluetooth 対応端末が相互にデータを送信するには、まずペア設定プロセスを使用して通信チャンネルを形成する必要があります。一方の検出可能端末では、受信接続リクエストに対応できるようにします。もう一方の端末では、サービス検出プロセスを検索します。検出可能端末がペア設定リクエストを受け入れると、2 台の端末は、セキュリティ キーを交換するボンディング プロセスを完了します。これらのキーは、後で使用するためにキャッシュに保存されます。ペア設定プロセスとボンディング プロセスが完了すると、2 台の端末は情報を交換します。セッションが完了すると、ペア設定リクエストを開始した端末は、その端末を検出可能端末にリンクしたチャンネルを解放します。2 台のチャンネルはボンディングされたままですが、相互の範囲内に存在し、どちらの端末もボンディングを解除しない限り、以降のセッションで自動的に再接続できます。

Bluetooth のパーミッション

アプリで Bluetooth 機能を使用するには、2 つのパーミッションを宣言する必要があります。1 つ目のパーミッションは BLUETOOTH です。このパーミッションは、接続のリクエスト、接続の受け入れ、データの転送など、Bluetooth 通信を実行するために必要です。

宣言する必要があるもう 1 つのパーミッションは、ACCESS_FINE_LOCATION です。Bluetooth スキャンを使用してユーザーの位置に関する情報を収集できるため、アプリにはこのパーミッションが必要です。この情報は、ユーザー自身の端末から、そして店舗や輸送施設などの場所で使用される Bluetooth ビーコンから取得される場合があります。

注:アプリが Android 9(API レベル 28)以下をターゲットにしている場合、ACCESS_FINE_LOCATION パーミッションの代わりに、ACCESS_COARSE_LOCATION パーミッションを宣言できます。

アプリで端末の検出を開始したり Bluetooth 設定を操作したりする場合は、BLUETOOTH パーミッションに加えて、BLUETOOTH_ADMIN パーミッションも宣言する必要があります。ほとんどのアプリは、ローカル Bluetooth 端末の検出のみのためにこのパーミッションが必要です。このパーミッションで付与されたその他の機能は、アプリがユーザーのリクエストに応じて Bluetooth の設定を変更する「Power Manager」として機能する場合を除いて、使用すべきではありません。

アプリのマニフェスト ファイルで Bluetooth のパーミッションを宣言してください。次に例を示します。

<manifest ... >
  <uses-permission android:name="android.permission.BLUETOOTH" />
  <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

  <!-- If your app targets Android 9 or lower, you can declare
       ACCESS_COARSE_LOCATION instead. -->
  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
  ...
</manifest>

アプリのパーミッション宣言の詳細については、<uses-permission> のリファレンスをご覧ください。

プロファイルの使用

Android 3.0 以降、Bluetooth API では Bluetooth プロファイルの操作がサポートされています。Bluetooth プロファイルは、端末間で Bluetooth ベースの通信を行うためのワイヤレス インターフェース仕様です。その一例が、Hands-Free プロファイルです。スマートフォンをワイヤレス ヘッドセットに接続する場合、どちらの端末も Hands-Free プロファイルをサポートしている必要があります。

Android Bluetooth API は、次の Bluetooth プロファイルの実装を提供しています。

  • Headset。Headset プロファイルは、スマートフォンで使用できる Bluetooth ヘッドセットのサポートを提供します。Android は BluetoothHeadset クラスを提供しています。これは Bluetooth Headset Service を制御するためのプロキシです。これには、Bluetooth Headset と Hands-Free(v1.5)の両方のプロファイルが含まれています。BluetoothHeadset クラスには、AT コマンドのサポートが含まれています。このトピックの詳細については、ベンダー固有の AT コマンドをご覧ください。
  • A2DP。Advanced Audio Distribution Profile(A2DP)プロファイルは、Bluetooth 接続を介して端末間で高品質のオーディオをストリーミングできる方法を定義しています。Android が提供する BluetoothA2dp クラスは、Bluetooth A2DP Service を制御するためのプロキシです。
  • Health Device。Android 4.0(API レベル 14)で、Bluetooth Health Device Profile(HDP)のサポートが導入されました。このプロファイルを使用すると、Bluetooth を使用して、心拍モニタ、血圧計、体温計、体重計など、Bluetooth をサポートするヘルス機器と通信するアプリを作成できます。サポートされる端末のリストと対応する端末のデータ専用のコードについては、Bluetooth の HDP Device Data Specializations をご覧ください。これらの値は、ISO/IEEE 11073-20601 [7] 仕様でも Nomenclature Codes Annex の MDC_DEV_SPEC_PROFILE_* として掲載されています。HDP の詳細については、Health Device Profile をご覧ください。

プロファイルを操作するための基本的なステップは次のとおりです。

  1. Bluetooth の設定で説明したとおり、デフォルトのアダプタを取得します。
  2. BluetoothProfile.ServiceListener を設定します。このリスナーは BluetoothProfile クライアントに、サービスへの接続またはサービスからの切断を通知します。
  3. getProfileProxy() を使用して、プロファイルと関連付けられたプロファイル プロキシ オブジェクトとの接続を確立します。以下の例で、プロファイル プロキシ オブジェクトは BluetoothHeadset のインスタンスです。
  4. onServiceConnected() で、プロファイル プロキシ オブジェクトのハンドルを取得します。
  5. プロファイル プロキシ オブジェクトを取得したら、それを使用して接続状態を監視し、プロファイルに関連したその他の操作を実行できます。

たとえば、このコード スニペットは BluetoothHeadset プロキシ オブジェクトに接続して、Headset プロファイルを制御する方法を示しています。

Kotlin

var bluetoothHeadset: BluetoothHeadset? = null

// Get the default adapter
val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()

private val profileListener = object : BluetoothProfile.ServiceListener {

    override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
        if (profile == BluetoothProfile.HEADSET) {
            bluetoothHeadset = proxy as BluetoothHeadset
        }
    }

    override fun onServiceDisconnected(profile: Int) {
        if (profile == BluetoothProfile.HEADSET) {
            bluetoothHeadset = null
        }
    }
}

// Establish connection to the proxy.
bluetoothAdapter?.getProfileProxy(context, profileListener, BluetoothProfile.HEADSET)

// ... call functions on bluetoothHeadset

// Close proxy connection after use.
bluetoothAdapter?.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset)

Java

BluetoothHeadset bluetoothHeadset;

// Get the default adapter
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

private BluetoothProfile.ServiceListener profileListener = new BluetoothProfile.ServiceListener() {
    public void onServiceConnected(int profile, BluetoothProfile proxy) {
        if (profile == BluetoothProfile.HEADSET) {
            bluetoothHeadset = (BluetoothHeadset) proxy;
        }
    }
    public void onServiceDisconnected(int profile) {
        if (profile == BluetoothProfile.HEADSET) {
            bluetoothHeadset = null;
        }
    }
};

// Establish connection to the proxy.
bluetoothAdapter.getProfileProxy(context, profileListener, BluetoothProfile.HEADSET);

// ... call functions on bluetoothHeadset

// Close proxy connection after use.
bluetoothAdapter.closeProfileProxy(bluetoothHeadset);

ベンダー固有の AT コマンド

Android 3.0(API レベル 11)以降、ヘッドセットから送信された事前定義済みのベンダー固有の AT コマンドに対するシステムのブロードキャストを受信するため、アプリを登録できるようになりました(Plantronics +XEVENT コマンドなど)。たとえば、接続された端末の電池レベルを示すブロードキャストをアプリが受信できれば、ユーザーに通知するなど、必要に応じてアクションを実行できます。ヘッドセットのベンダー固有の AT コマンドを処理するには、ACTION_VENDOR_SPECIFIC_HEADSET_EVENT インテントのブロードキャスト レシーバを作成します。

Health Device Profile

Android 4.0(API レベル 14)で、Bluetooth Health Device Profile(HDP)のサポートが導入されました。これにより、Bluetooth を使用して、心拍モニタ、血圧計、体温計、体重計など、Bluetooth をサポートするヘルス機器と通信するアプリを作成できます。Bluetooth Health API には、BluetoothHealthBluetoothHealthCallbackBluetoothHealthAppConfiguration の各クラスが含まれています。クラスの詳細については、主なクラスとインターフェースをご覧ください。

Bluetooth Health API を使用するにあたり、HDP の主なコンセプトを理解しておくと役に立ちます。

ソース
Android スマートフォンやタブレットなどのスマート端末に医療データを送信するヘルス機器(体重計、血糖測定器、体温計など)です。
シンク
医療データを受信するスマート端末です。Android HDP アプリでは、シンクは BluetoothHealthAppConfiguration オブジェクトで表されます。
登録
特定のヘルス機器と通信するシンクを登録するために使用されるプロセスです。
接続
ヘルス機器(ソース)とスマート端末(シンク)間のチャンネルを開くために使用されるプロセスです。

HDP アプリの作成

Android HDP アプリの作成に必要な基本ステップは次のとおりです。

  1. BluetoothHealth プロキシ オブジェクトへの参照を取得します。

    通常のヘッドセットや A2DP プロファイル端末と同様に、BluetoothProfile.ServiceListener とプロファイルの種類である HEALTH を指定して getProfileProxy() を呼び出し、プロファイル プロキシ オブジェクトとの接続を確立します。

  2. BluetoothHealthCallback を作成して、Health Sink として機能するアプリ設定(BluetoothHealthAppConfiguration)を登録します。
  3. ヘルス機器との接続を確立します。

    注:一部の端末は接続を自動的に開始します。そのような端末では、このステップは実行する必要がありません。

  4. ヘルス機器と正常に接続できたら、ファイル ディスクリプタを使用してヘルス機器に対して読み取り / 書き込みを行います。受信したデータは、IEEE 11073 仕様を実装した Health Manager を使用して解釈する必要があります。
  5. 完了したら、Health チャンネルをクローズしてアプリの登録を解除します。チャンネルは、長時間使用されていない場合にもクローズされます。

Bluetooth の設定

アプリで Bluetooth 通信を行う前に、その Bluetooth が端末でサポートされているか、サポートされている場合は有効かどうかを確認する必要があります。

Bluetooth がサポートされていない場合は、すべての Bluetooth 機能を適切に無効にしてください。Bluetooth がサポートされているものの無効にされている場合、ユーザーがアプリから離れることなく Bluetooth を有効にするようにリクエストすることができます。この設定は、BluetoothAdapter を使用して、2 つのステップで完了します。

  1. BluetoothAdapter を取得します。

    すべての Bluetooth のアクティビティには BluetoothAdapter が必要です。BluetoothAdapter を取得するには、静的メソッド getDefaultAdapter() を呼び出します。これは、端末独自の Bluetooth アダプタ(Bluetooth 無線通信)を表す BluetoothAdapter を返します。システム全体には 1 つの Bluetooth アダプタが存在し、アプリはこのオブジェクトを使用して Bluetooth アダプタとやり取りができます。getDefaultAdapter()null を返す場合、端末は Bluetooth をサポートしていません。次に例を示します。

    Kotlin

    val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()
    if (bluetoothAdapter == null) {
        // Device doesn't support Bluetooth
    }
    

    Java

    BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    if (bluetoothAdapter == null) {
        // Device doesn't support Bluetooth
    }
    
  2. Bluetooth を有効にします。

    次に、Bluetooth が有効であることを確認する必要があります。isEnabled() を呼び出して、Bluetooth が現在有効かどうかを確認します。このメソッドが false を返したら、Bluetooth は無効です。Bluetooth を有効にするようリクエストするには、startActivityForResult() を呼び出して ACTION_REQUEST_ENABLE インテント アクションを渡します。この呼び出しによって、(アプリを停止せずに)システム設定で Bluetooth を有効にするリクエストが発行されます。次に例を示します。

    Kotlin

    if (bluetoothAdapter?.isEnabled == false) {
        val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
        startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
    }
    

    Java

    if (!bluetoothAdapter.isEnabled()) {
        Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
        startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
    }
    

    図 1 に示すように、ユーザーに Bluetooth を有効にするためのパーミッションを求めるダイアログが表示されます。ユーザーが [Yes] を選択すると、システムは Bluetooth を有効にし、プロセスが完了(または失敗)するとフォーカスをアプリに戻します。

    図 1: Bluetooth を有効にするダイアログ

    startActivityForResult() に渡される REQUEST_ENABLE_BT 定数は、ローカルで定義された整数(1 以上)です。システムはこの定数を requestCode パラメータとして onActivityResult() の実装に渡します。

    Bluetooth の有効化に成功した場合、アクティビティは onActivityResult() コールバックで結果コード RESULT_OK を受け取ります。エラー(またはユーザーが [No] を選択)により Bluetooth が有効にならなかった場合、結果コードは RESULT_CANCELED です。

オプションで、アプリは ACTION_STATE_CHANGED ブロードキャスト インテントをリッスンします。このインテントは Bluetooth の状態が変更されるたびに、システムによってブロードキャストされます。このブロードキャストには追加フィールドの EXTRA_STATEEXTRA_PREVIOUS_STATE が含まれており、それぞれ Bluetooth の以前の状態と新しい状態が格納されています。これらの追加フィールドで使用できる値は、STATE_TURNING_ONSTATE_ONSTATE_TURNING_OFFSTATE_OFF です。このブロードキャストのリッスンは、アプリが実行中に Bluetooth 状態の変更を検出するうえで便利です。

ヒント: 検出の許可を有効にすると、Bluetooth が自動的に有効になります。Bluetooth アクティビティを実行する前に、端末の検出の許可を一貫して有効にする場合は、上記のステップ 2 をスキップできます。詳細は、このページの検出の許可の有効化セクションをご覧ください。

端末の検出

BluetoothAdapter を使用して、端末の検出を使用するか、ペア設定された端末のリストを問い合わせて、リモート Bluetooth 端末を見つけることができます。

端末の検出は、ローカルエリアで Bluetooth 対応端末を検索し、各端末の情報をリクエストするスキャン手順です。この処理は、検出、問い合わせ、スキャン、と呼ばれる場合もあります。ただし、付近の Bluetooth 端末は、現在検出可能であるために情報リクエストを受け入れている場合にのみ検出リクエストに応答します。端末が検出可能な場合、端末名、クラス、一意の MAC アドレスなどの情報を共有することで検出リクエストに応答します。検出プロセスを実行した端末はこの情報を使用して、検出された端末への接続開始を選択できます。

リモート端末との接続が初めて確立されると、ペア設定リクエストが自動的にユーザーに表示されます。端末がペア設定されると、端末の基本情報(端末名、クラス、MAC アドレスなど)が保存され、Bluetooth API を使用して読み取れるようになります。リモート端末の既知の MAC アドレスを使用すると、検出を実行しなくてもいつでも接続を開始できます(端末がまだ範囲内に存在することが前提です)。

ペア設定と接続には違いがある点に注意してください。

  • ペア設定は、端末がお互いの存在を認識し、認証に使用できる共有のリンクキーを持ち、相互に暗号化された接続を確立できることを意味します。
  • 接続は、端末が現在 RFCOMM チャンネルを共有し、相互にデータを送信できることを意味します。現在の Android Bluetooth API では、RFCOMM 接続を確立する前に端末のペア設定が必要です。Bluetooth API を使用して暗号化された接続を開始すると、自動的にペア設定が実行されます。

次のセクションでは、ペア設定済みの端末を見つける、または端末の検出を使用して新しい端末を見つける方法について説明します。

注:Android 搭載端末は、デフォルトでは検出可能になっていません。ユーザーは、システム設定を使用して一定の時間だけ端末を検出可能にすることができます。またはアプリで、ユーザーがアプリから離れることなく検出の許可を有効にするようにリクエストできます。詳細については、このページの検出の許可の有効化セクションをご覧ください。

ペア設定済みの端末の問い合わせ

端末検出を実行する前に、ペア設定済みの端末のセットを問い合わせて、目的の端末が既に認識されているかどうかを問い合わせることをお勧めします。問い合わせるには、getBondedDevices() を呼び出します。これは、ペア設定済みの端末を表す BluetoothDevice オブジェクトのセットを返します。たとえば、次のコード スニペットに示すように、ペア設定済みの端末をすべて問い合わせて、各端末の名前と MAC アドレスを取得できます。

Kotlin

val pairedDevices: Set<BluetoothDevice>? = bluetoothAdapter?.bondedDevices
pairedDevices?.forEach { device ->
    val deviceName = device.name
    val deviceHardwareAddress = device.address // MAC address
}

Java

Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices();

if (pairedDevices.size() > 0) {
    // There are paired devices. Get the name and address of each paired device.
    for (BluetoothDevice device : pairedDevices) {
        String deviceName = device.getName();
        String deviceHardwareAddress = device.getAddress(); // MAC address
    }
}

Bluetooth 端末との接続を開始するために、関連付けられている BluetoothDevice オブジェクトから取得する必要があるのは、MAC アドレスだけです。この例では、getAddress() を呼び出して取得しています。接続の確立の詳細については、端末の接続に関するセクションをご覧ください。

注意:端末検出を実行すると、Bluetooth アダプタの多くのリソースが消費されます。接続する端末が見つかったら、接続を試みる前に、cancelDiscovery() を使用して必ず検出を停止してください。検出プロセスを実行すると、既存の接続に使用できる帯域幅が大幅に削減されるため、端末に接続している間は検出を実行しないでください。

端末の検出

端末の検出を開始するには、startDiscovery() を呼び出すだけです。このプロセスは非同期で実行され、検出が正常に開始されたどうかを示すブール値を返します。検出プロセスでは通常、約 12 秒間の問い合わせのスキャンが行われ、その後、検出された各端末のページスキャンによってそれぞれの Bluetooth 名を取得します。

検出された各端末に関する情報を受信するには、アプリで ACTION_FOUND インテントの BroadcastReceiver を登録する必要があります。端末ごとに、システムはこのインテントをブロードキャストします。このインテントには追加フィールドの EXTRA_DEVICEEXTRA_CLASS があり、それぞれに BluetoothDeviceBluetoothClass が格納されています。次のコード スニペットは、端末が検出されたときに、ブロードキャストを処理するために登録する方法を示しています。

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    ...

    // Register for broadcasts when a device is discovered.
    val filter = IntentFilter(BluetoothDevice.ACTION_FOUND)
    registerReceiver(receiver, filter)
}

// Create a BroadcastReceiver for ACTION_FOUND.
private val receiver = object : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        val action: String = intent.action
        when(action) {
            BluetoothDevice.ACTION_FOUND -> {
                // Discovery has found a device. Get the BluetoothDevice
                // object and its info from the Intent.
                val device: BluetoothDevice =
                        intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
                val deviceName = device.name
                val deviceHardwareAddress = device.address // MAC address
            }
        }
    }
}

override fun onDestroy() {
    super.onDestroy()
    ...

    // Don't forget to unregister the ACTION_FOUND receiver.
    unregisterReceiver(receiver)
}

Java

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...

    // Register for broadcasts when a device is discovered.
    IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
    registerReceiver(receiver, filter);
}

// Create a BroadcastReceiver for ACTION_FOUND.
private final BroadcastReceiver receiver = new BroadcastReceiver() {
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (BluetoothDevice.ACTION_FOUND.equals(action)) {
            // Discovery has found a device. Get the BluetoothDevice
            // object and its info from the Intent.
            BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
            String deviceName = device.getName();
            String deviceHardwareAddress = device.getAddress(); // MAC address
        }
    }
};

@Override
protected void onDestroy() {
    super.onDestroy();
    ...

    // Don't forget to unregister the ACTION_FOUND receiver.
    unregisterReceiver(receiver);
}

Bluetooth 端末との接続を開始するために、関連付けられている BluetoothDevice オブジェクトから取得する必要があるのは、MAC アドレスだけです。この例では、getAddress() を呼び出して取得しています。接続の確立の詳細については、端末の接続に関するセクションをご覧ください。

注意:端末検出を実行すると、Bluetooth アダプタの多くのリソースが消費されます。接続する端末が見つかったら、接続を試みる前に、cancelDiscovery() を使用して必ず検出を停止してください。検出プロセスを実行すると、既存の接続に使用できる帯域幅が大幅に削減されるため、端末に接続している間は検出を実行しないでください。

検出の許可の有効化

ローカル端末を他の端末から検出可能にするには、ACTION_REQUEST_DISCOVERABLE インテントを指定して startActivityForResult(Intent, int) を呼び出します。これにより、設定アプリに移動して独自のアプリを停止することなく、システムの検出可能モードを有効にするリクエストが発行されます。デフォルトでは、端末は 120 秒(2 分)間検出可能になります。EXTRA_DISCOVERABLE_DURATION をさらに追加して、別の期間を 3,600 秒(1 時間)まで定義できます。

注意:EXTRA_DISCOVERABLE_DURATION エクストラの値を 0 に設定した場合、端末は常に検出可能になります。この設定は安全ではないため、使用しないことをお勧めします。

次のコード スニペットは、端末を 5 分間(300 秒間)検出可能になるように設定しています。

Kotlin

val discoverableIntent: Intent = Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE).apply {
    putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300)
}
startActivity(discoverableIntent)

Java

Intent discoverableIntent =
        new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
startActivity(discoverableIntent);
図 2: 検出の許可を有効にするダイアログ

図 2 に示すように、端末を検出可能にするユーザーのパーミッションをリクエストするダイアログが表示されます。ユーザーが [Yes] を選択すると、端末は指定された時間、検出可能になります。アクティビティは onActivityResult() コールバックの呼び出しを受け取ります。この結果コードは、端末を検出可能にする時間と同じです。ユーザーが [No] を選択するかエラーが発生した場合、結果コードは RESULT_CANCELED になります。

注:Bluetooth が端末で有効になっていない場合、端末の検出の許可を有効にすると、Bluetooth が自動的に有効になります。

端末は割り当てられた時間、検出可能モードをサイレントに継続します。検出可能モードに変化があったときに通知を受けたい場合は、ACTION_SCAN_MODE_CHANGED インテントの BroadcastReceiver を登録できます。このインテントには、追加フィールドの EXTRA_SCAN_MODEEXTRA_PREVIOUS_SCAN_MODE が含まれており、それぞれ新しいスキャンモードと以前のスキャンモードを示します。各エクストラに使用できる値は次のとおりです。

SCAN_MODE_CONNECTABLE_DISCOVERABLE
端末は検出可能モードです。
SCAN_MODE_CONNECTABLE
端末は検出可能モードではありませんが、まだ接続を受信できます。
SCAN_MODE_NONE
端末は検出可能モードではなく、接続も受信できません。

リモート端末との接続を開始する場合、端末の検出の許可を有効にする必要はありません。リモート端末は、他の端末との接続を開始する前に検出可能である必要があるため、検出の許可の有効化は、アプリで接続要求を受け入れるサーバー ソケットをホストしなければならない場合に限られます。

端末の接続

2 台の端末間で接続を確立するには、サーバー側とクライアント側の両方の仕組みを実装する必要があります。一方の端末はサーバー ソケットをオープンし、他方の端末はサーバー端末の MAC アドレスを使用して接続を開始する必要があるためです。サーバー端末とクライアント端末はそれぞれ、異なる方法で必要な BluetoothSocket を取得します。サーバーは、接続要求を受け入れたときにソケット情報を受け取ります。クライアントは、RFCOMM チャンネルをサーバーに対してオープンしたときにソケット情報を提供します。

サーバーとクライアントは、それぞれが同じ RFCOMM チャンネルで接続された BluetoothSocket を取得できれば接続完了と見なされます。この時点で、各端末は入力ストリームと出力ストリームを取得してデータ転送を開始できます。この内容については、接続の管理のセクションで取り上げます。このセクションでは、2 台の端末間で接続を開始する方法について説明します。

接続テクニック

実装のテクニックとして、各端末を自動的にサーバーとして準備しておくと、それぞれがサーバー ソケットをオープンして接続をリッスンできます。この場合、片方の端末がもう一方の端末との接続を開始して、クライアントになることができます。または、片方の端末がオンデマンドで明示的に接続をホストしてサーバー ソケットをオープンすると、もう一方の端末は接続を開始します。

図 3: Bluetooth のペア設定のダイアログ

注:2 台の端末がまだペア設定されていない場合、Android フレームワークは自動的にペア設定リクエストの通知を表示するか、または図 3 に示すように、接続手順の実行中にユーザーにダイアログを表示します。したがって、アプリが端末に接続しようとするとき、端末がペア設定済みかどうかをアプリで考慮する必要はありません。RFCOMM 接続の試行は、ユーザーが 2 台の端末のペア設定に成功するまではブロックされます。またはユーザーがペア設定を拒否するか、ペア設定プロセスが失敗 / タイムアウトしたときには失敗します。

サーバー側の接続

2 台の端末を接続するには、一方がオープンした BluetoothServerSocket を保持してサーバーとして動作する必要があります。サーバー ソケットの目的は、受信接続リクエストをリッスンして、それを受け入れたときに、接続した BluetoothSocket を提供することです。BluetoothSocketBluetoothServerSocket から取得したら、端末で複数の接続を受け入れる必要がない限り、BluetoothServerSocket は破棄できます(破棄する必要があります)。

サーバー ソケットを設定して接続を受け入れるには、次の手順を実行します。

  1. listenUsingRfcommWithServiceRecord() を呼び出して BluetoothServerSocket を取得します。

    この文字列はサービスを識別できる名前です。端末上のシステムは自動的に新しい Service Discovery Protocol(SDP)データベースのエントリを書き込みます。この名前は任意です。単純にアプリ名を指定することもできます。Universally Unique Identifier(UUID)は SDP エントリにも含まれており、クライアント端末との接続許可のベースを形成します。つまり、クライアントがこの端末との接続を試みたとき、端末は接続に使用したいサービスを一意に識別できる UUID を持っています。接続を受け入れるには、この UUID が一致している必要があります。

    UUID は、情報を一意に識別するために使用される、標準化された 128 ビット形式の文字列 ID です。UUID のポイントは、ランダム ID を選択できるほど十分に大きく、他の ID でクラッシュしないことです。今回は、アプリの Bluetooth サービスを一意に識別するために使用します。アプリで使用する UUID を取得するには、ウェブ上に数多く存在するランダム UUID ジェネレータのうちいずれかを使用し、fromString(String) を使用して UUID を初期化します。

  2. accept() を呼び出して、接続リクエストのリッスンを開始します。

    これはブロッキング コールです。接続が受け入れられたか、例外が発生したときに制御が戻ります。接続が受け入れられるのは、リモート端末が、このリスニング サーバー ソケットで登録した UUID と一致する UUID を含む接続リクエストを送信した場合だけです。接続に成功した場合、accept() は接続された BluetoothSocket を返します。

  3. 追加の接続を受け入れる必要がない限り、close() を呼び出します。

    このメソッド呼び出しにより、サーバー ソケットとそのすべてのリソースが解放されますが、accept() で返された接続済みの BluetoothSocket はクローズしません。TCP/IP とは異なり、RFCOMM はチャンネルごとに、同時に 1 つのクライアントのみ接続を許可します。したがってほとんどの場合、接続されたソケットの受け入れ直後に BluetoothServerSocketclose() を呼び出すのが適切です。

accept() の呼び出しはブロッキング コールであるため、メイン アクティビティの UI スレッドで実行しないでください。アプリが他のユーザー操作に応答できなくなります。通常、BluetoothServerSocket または BluetoothSocket を伴う操作はすべて、アプリで管理される新しいスレッドで実行するのが適切な方法です。accept() のようなブロックされる呼び出しを中止するには、BluetoothServerSocket または BluetoothSocket で別のスレッドから close() を呼び出します。BluetoothServerSocket または BluetoothSocket のすべてのメソッドがスレッドセーフというわけではないため、注意してください。

これは、接続要求を受け入れるサーバー コンポーネントのスレッドを簡略化したものです。

Kotlin

private inner class AcceptThread : Thread() {
    
    private val mmServerSocket: BluetoothServerSocket? by lazy(LazyThreadSafetyMode.NONE) {
        bluetoothAdapter?.listenUsingInsecureRfcommWithServiceRecord(NAME, MY_UUID)
    }

    override fun run() {
        // Keep listening until exception occurs or a socket is returned.
        var shouldLoop = true
        while (shouldLoop) {
            val socket: BluetoothSocket? = try {
                mmServerSocket?.accept()
            } catch (e: IOException) {
                Log.e(TAG, "Socket's accept() method failed", e)
                shouldLoop = false
                null
            }
            socket?.also {
                manageMyConnectedSocket(it)
                mmServerSocket?.close()
                shouldLoop = false
            }
        }
    }

    // Closes the connect socket and causes the thread to finish.
    fun cancel() {
        try {
            mmServerSocket?.close()
        } catch (e: IOException) {
            Log.e(TAG, "Could not close the connect socket", e)
        }
    }
}

Java

private class AcceptThread extends Thread {
    private final BluetoothServerSocket mmServerSocket;

    public AcceptThread() {
        // Use a temporary object that is later assigned to mmServerSocket
        // because mmServerSocket is final.
        BluetoothServerSocket tmp = null;
        try {
            // MY_UUID is the app's UUID string, also used by the client code.
            tmp = bluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
        } catch (IOException e) {
            Log.e(TAG, "Socket's listen() method failed", e);
        }
        mmServerSocket = tmp;
    }

    public void run() {
        BluetoothSocket socket = null;
        // Keep listening until exception occurs or a socket is returned.
        while (true) {
            try {
                socket = mmServerSocket.accept();
            } catch (IOException e) {
                Log.e(TAG, "Socket's accept() method failed", e);
                break;
            }

            if (socket != null) {
                // A connection was accepted. Perform work associated with
                // the connection in a separate thread.
                manageMyConnectedSocket(socket);
                mmServerSocket.close();
                break;
            }
        }
    }

    // Closes the connect socket and causes the thread to finish.
    public void cancel() {
        try {
            mmServerSocket.close();
        } catch (IOException e) {
            Log.e(TAG, "Could not close the connect socket", e);
        }
    }
}

この例では、1 つの接続要求のみが想定されており、接続が受け入れられるとすぐに BluetoothSocket が取得され、アプリは取得した BluetoothSocket を別のスレッドに渡し、BluetoothServerSocket をクローズしてループを終了します。

accept()BluetoothSocket を返したとき、ソケットは既に接続されています。そのため、クライアント側で処理したように connect() を呼び出さないでください。

アプリ固有の manageMyConnectedSocket() メソッドは、データ転送用のスレッドを開始します。詳細については、接続の管理に関するセクションで説明します。

通常、接続要求のリッスンが完了したらすぐに BluetoothServerSocket をクローズする必要があります。この例では、BluetoothSocket を取得してすぐ close() が呼び出されています。また、サーバー ソケットのリッスンを停止する必要がある場合は、スレッド内でプライベートの BluetoothSocket をクローズできるパブリック メソッドを提供することもできます。

クライアント側の接続

オープンしているサーバー ソケットで接続を受け入れるリモート端末との接続を開始するには、まずリモート端末を表す BluetoothDevice オブジェクトを取得する必要があります。BluetoothDevice を作成する方法については、端末の検索をご覧ください。BluetoothDevice を使用して BluetoothSocket を取得し、接続を開始する必要があります。

基本的な手順は次のとおりです。

  1. BluetoothDevice を使用し、createRfcommSocketToServiceRecord(UUID) を呼び出して BluetoothSocket を取得します。

    これにより、BluetoothDevice への接続をクライアントに許可する BluetoothSocket オブジェクトが初期化されます。ここで渡される UUID は、サーバー端末が listenUsingRfcommWithServiceRecord(String, UUID) を呼び出して BluetoothServerSocket をオープンしたときに使用した UUID と一致している必要があります。一致する UUID を使用するには、UUID の文字列をアプリにハードコードして、サーバーとクライアントの両方のコードからその文字列を参照します。

  2. connect() を呼び出して接続を開始します。このメソッドはブロッキング コールです。

    クライアントがこのメソッドを呼び出すと、UUID が一致するリモート端末を検索するために SDP ルックアップが実行されます。ルックアップに成功してリモート端末が接続を受け入れると、その接続で使用する RFCOMM チャンネルが共有され、connect() メソッドから制御が戻ります。接続に失敗するか connect() メソッドがタイムアウトすると(約 12 秒後)、このメソッドは IOException をスローします。

    connect() はブロッキング コールであるため、この接続手順は必ずメインのアクティビティ(UI)スレッドとは別のスレッドで実行する必要があります。

    注:cancelDiscovery() を呼び出すときは必ず、connect() を呼び出す前に端末の検出を実行していないことを確認してください。検出を実行していると、接続の試行処理が大幅に遅くなり、失敗する可能性があります。

以下は、Bluetooth 接続を開始するクライアント スレッドの基本的な例です。

Kotlin

private inner class ConnectThread(device: BluetoothDevice) : Thread() {

    private val mmSocket: BluetoothSocket? by lazy(LazyThreadSafetyMode.NONE) {
        device.createRfcommSocketToServiceRecord(MY_UUID)
    }

    public override fun run() {
        // Cancel discovery because it otherwise slows down the connection.
        bluetoothAdapter?.cancelDiscovery()

        mmSocket?.use { socket ->
            // Connect to the remote device through the socket. This call blocks
            // until it succeeds or throws an exception.
            socket.connect()

            // The connection attempt succeeded. Perform work associated with
            // the connection in a separate thread.
            manageMyConnectedSocket(socket)
        }
    }

    // Closes the client socket and causes the thread to finish.
    fun cancel() {
        try {
            mmSocket?.close()
        } catch (e: IOException) {
            Log.e(TAG, "Could not close the client socket", e)
        }
    }
}

Java

private class ConnectThread extends Thread {
    private final BluetoothSocket mmSocket;
    private final BluetoothDevice mmDevice;

    public ConnectThread(BluetoothDevice device) {
        // Use a temporary object that is later assigned to mmSocket
        // because mmSocket is final.
        BluetoothSocket tmp = null;
        mmDevice = device;

        try {
            // Get a BluetoothSocket to connect with the given BluetoothDevice.
            // MY_UUID is the app's UUID string, also used in the server code.
            tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
        } catch (IOException e) {
            Log.e(TAG, "Socket's create() method failed", e);
        }
        mmSocket = tmp;
    }

    public void run() {
        // Cancel discovery because it otherwise slows down the connection.
        bluetoothAdapter.cancelDiscovery();

        try {
            // Connect to the remote device through the socket. This call blocks
            // until it succeeds or throws an exception.
            mmSocket.connect();
        } catch (IOException connectException) {
            // Unable to connect; close the socket and return.
            try {
                mmSocket.close();
            } catch (IOException closeException) {
                Log.e(TAG, "Could not close the client socket", closeException);
            }
            return;
        }

        // The connection attempt succeeded. Perform work associated with
        // the connection in a separate thread.
        manageMyConnectedSocket(mmSocket);
    }

    // Closes the client socket and causes the thread to finish.
    public void cancel() {
        try {
            mmSocket.close();
        } catch (IOException e) {
            Log.e(TAG, "Could not close the client socket", e);
        }
    }
}

このスニペットで、cancelDiscovery() は、接続試行の前に呼び出される点に注意してください。cancelDiscovery() は、必ず connect() の前に呼び出してください。特に、cancelDiscovery() は端末検出が現在実行中かどうかに関係なく成功するためです。ただし、端末検出が実行中かどうかをアプリで確認する必要がある場合は、isDiscovering() を使用して確認できます。

アプリ固有の manageMyConnectedSocket() メソッドは、データ転送用のスレッドを開始します。詳細については、接続の管理に関するセクションで説明します。

BluetoothSocket の処理が完了したら、必ず close() を呼び出してください。これにより接続済みソケットがすぐにクローズされ、関連する内部のリソースがすべて解放されます。

接続の管理

複数の端末を正常に接続すると、各端末は接続済みの BluetoothSocket を受け取ります。ここから、いよいよ端末間の情報共有が始まります。BluetoothSocket を使用したデータ転送の一般的な手順は次のとおりです。

  1. ソケットを介した転送を処理する InputStreamOutputStream を取得します。それぞれ、getInputStream()getOutputStream() を使用します。
  2. read(byte[])write(byte[]) を使用して、ストリームに対してデータを読み書きします。

もちろん、実装の詳細については検討が必要です。特に、ストリームに対する読み書きには専用のスレッドを使用する必要があります。これは、read(byte[]) メソッドと write(byte[]) メソッドがどちらもブロッキング コールであるため重要です。read(byte[]) メソッドは、ストリームから何かを読み取るまでブロックします。write(byte[]) メソッドは通常ブロックしませんが、リモート端末がすぐに read(byte[]) を呼び出さずに中間バッファがいっぱいになったとき、フロー制御のためにブロックする場合があります。したがって、スレッド内のメインループは InputStream からの読み取り専用にする必要があります。スレッド内の別のパブリック メソッドを使用して、OutputStream への書き込みを開始できます。

Bluetooth を介して接続された 2 台の端末間でデータを転送する例を次に示します。

Kotlin

private const val TAG = "MY_APP_DEBUG_TAG"

// Defines several constants used when transmitting messages between the
// service and the UI.
const val MESSAGE_READ: Int = 0
const val MESSAGE_WRITE: Int = 1
const val MESSAGE_TOAST: Int = 2
// ... (Add other message types here as needed.)

class MyBluetoothService(
        // handler that gets info from Bluetooth service
        private val handler: Handler) {

    private inner class ConnectedThread(private val mmSocket: BluetoothSocket) : Thread() {

        private val mmInStream: InputStream = mmSocket.inputStream
        private val mmOutStream: OutputStream = mmSocket.outputStream
        private val mmBuffer: ByteArray = ByteArray(1024) // mmBuffer store for the stream

        override fun run() {
            var numBytes: Int // bytes returned from read()

            // Keep listening to the InputStream until an exception occurs.
            while (true) {
                // Read from the InputStream.
                numBytes = try {
                    mmInStream.read(mmBuffer)
                } catch (e: IOException) {
                    Log.d(TAG, "Input stream was disconnected", e)
                    break
                }

                // Send the obtained bytes to the UI activity.
                val readMsg = handler.obtainMessage(
                        MESSAGE_READ, numBytes, -1,
                        mmBuffer)
                readMsg.sendToTarget()
            }
        }

        // Call this from the main activity to send data to the remote device.
        fun write(bytes: ByteArray) {
            try {
                mmOutStream.write(bytes)
            } catch (e: IOException) {
                Log.e(TAG, "Error occurred when sending data", e)

                // Send a failure message back to the activity.
                val writeErrorMsg = handler.obtainMessage(MESSAGE_TOAST)
                val bundle = Bundle().apply {
                    putString("toast", "Couldn't send data to the other device")
                }
                writeErrorMsg.data = bundle
                handler.sendMessage(writeErrorMsg)
                return
            }

            // Share the sent message with the UI activity.
            val writtenMsg = handler.obtainMessage(
                    MESSAGE_WRITE, -1, -1, mmBuffer)
            writtenMsg.sendToTarget()
        }

        // Call this method from the main activity to shut down the connection.
        fun cancel() {
            try {
                mmSocket.close()
            } catch (e: IOException) {
                Log.e(TAG, "Could not close the connect socket", e)
            }
        }
    }
}

Java

public class MyBluetoothService {
    private static final String TAG = "MY_APP_DEBUG_TAG";
    private Handler handler; // handler that gets info from Bluetooth service

    // Defines several constants used when transmitting messages between the
    // service and the UI.
    private interface MessageConstants {
        public static final int MESSAGE_READ = 0;
        public static final int MESSAGE_WRITE = 1;
        public static final int MESSAGE_TOAST = 2;

        // ... (Add other message types here as needed.)
    }

    private class ConnectedThread extends Thread {
        private final BluetoothSocket mmSocket;
        private final InputStream mmInStream;
        private final OutputStream mmOutStream;
        private byte[] mmBuffer; // mmBuffer store for the stream

        public ConnectedThread(BluetoothSocket socket) {
            mmSocket = socket;
            InputStream tmpIn = null;
            OutputStream tmpOut = null;

            // Get the input and output streams; using temp objects because
            // member streams are final.
            try {
                tmpIn = socket.getInputStream();
            } catch (IOException e) {
                Log.e(TAG, "Error occurred when creating input stream", e);
            }
            try {
                tmpOut = socket.getOutputStream();
            } catch (IOException e) {
                Log.e(TAG, "Error occurred when creating output stream", e);
            }

            mmInStream = tmpIn;
            mmOutStream = tmpOut;
        }

        public void run() {
            mmBuffer = new byte[1024];
            int numBytes; // bytes returned from read()

            // Keep listening to the InputStream until an exception occurs.
            while (true) {
                try {
                    // Read from the InputStream.
                    numBytes = mmInStream.read(mmBuffer);
                    // Send the obtained bytes to the UI activity.
                    Message readMsg = handler.obtainMessage(
                            MessageConstants.MESSAGE_READ, numBytes, -1,
                            mmBuffer);
                    readMsg.sendToTarget();
                } catch (IOException e) {
                    Log.d(TAG, "Input stream was disconnected", e);
                    break;
                }
            }
        }

        // Call this from the main activity to send data to the remote device.
        public void write(byte[] bytes) {
            try {
                mmOutStream.write(bytes);

                // Share the sent message with the UI activity.
                Message writtenMsg = handler.obtainMessage(
                        MessageConstants.MESSAGE_WRITE, -1, -1, mmBuffer);
                writtenMsg.sendToTarget();
            } catch (IOException e) {
                Log.e(TAG, "Error occurred when sending data", e);

                // Send a failure message back to the activity.
                Message writeErrorMsg =
                        handler.obtainMessage(MessageConstants.MESSAGE_TOAST);
                Bundle bundle = new Bundle();
                bundle.putString("toast",
                        "Couldn't send data to the other device");
                writeErrorMsg.setData(bundle);
                handler.sendMessage(writeErrorMsg);
            }
        }

        // Call this method from the main activity to shut down the connection.
        public void cancel() {
            try {
                mmSocket.close();
            } catch (IOException e) {
                Log.e(TAG, "Could not close the connect socket", e);
            }
        }
    }
}

コンストラクタが必要なストリームを取得すると、InputStream からデータを受信するまでスレッドは待機します。read(byte[]) がストリームから取得したデータを返すと、親クラスのメンバーである Handler によって、データはメイン アクティビティに送信されます。スレッドは InputStream からさらにバイトが読み取られるのを待機します。

データの送信は、メイン アクティビティからスレッドの write() メソッドを呼び出し、送信するバイトを渡すだけなので簡単です。このメソッドは、write(byte[]) を呼び出して、データをリモート端末に送信します。write(byte[]) の呼び出し時に IOException がスローされた場合、スレッドはメイン アクティビティにトーストを送信して、端末が他の(接続済み)端末に特定のバイトを送信できなかったことをユーザーに示します。

スレッドの cancel() メソッドを使用すると、BluetoothSocket をクローズしていつでも接続を終了できます。Bluetooth 接続の使用を完了したら、必ずこのメソッドを呼び出してください。

Bluetooth API を使用したデモについては、Bluetooth Chat サンプルアプリをご覧ください。

主なクラスとインターフェース

Bluetooth API はすべて、android.bluetooth パッケージ内にあります。次に、Bluetooth 接続を作成するために必要なクラスとインターフェースの概要を説明します。

BluetoothAdapter
ローカル Bluetooth アダプタ(Bluetooth 無線通信)を表します。BluetoothAdapter は、Bluetooth のあらゆる操作のエントリ ポイントになります。これを使用して他の Bluetooth 端末を検出し、ボンディング(ペア設定)した端末のリストを問い合わせて、既知の MAC アドレスを使用して BluetoothDevice をインスタンス化し、BluetoothServerSocket を作成して他の端末の通信をリッスンします。
BluetoothDevice
リモート Bluetooth 端末を表します。これを使用して、BluetoothSocket を介してリモート端末との接続を要求したり、名前、アドレス、クラス、ボンディング状態など端末に関する情報を問い合わせたりします。
BluetoothSocket
Bluetooth ソケットのインターフェースを表します(TCP Socket と同様)。これは、アプリが InputStream および OutputStream を介して別の Bluetooth 端末とデータ交換するための接続点です。
BluetoothServerSocket
受信リクエストをリッスンするためにオープンしているサーバー ソケットを表します(TCP ServerSocket と同様)。2 台の Android 端末を接続するには、一方の端末がこのクラスを使用してサーバー ソケットをオープンする必要があります。リモート Bluetooth 端末はこの端末への接続リクエストを行い、端末が接続を受け入れると、接続された BluetoothSocket を返します。
BluetoothClass
Bluetooth 端末の一般的な特性と機能を示します。これは端末のクラスとサービスを定義する読み取り専用のプロパティのセットです。この情報は端末の種類に関するヒントとして活用できますが、このクラスの属性は、端末によってサポートされるすべての Bluetooth のプロファイルとサービスについて記述しているとは限りません。
BluetoothProfile
Bluetooth プロファイルを表すインターフェース。Bluetooth プロファイルは、端末間で Bluetooth ベースの通信を行うためのワイヤレス インターフェース仕様です。その一例が、Hands-Free プロファイルです。プロファイルの詳細については、プロファイルの使用をご覧ください。
BluetoothHeadset
スマートフォンで使用する Bluetooth ヘッドセットをサポートします。これには、Bluetooth Headset と Hands-Free(v1.5)の両方のプロファイルが含まれています。
BluetoothA2dp
Advanced Audio Distribution Profile(A2DP)を使用して、Bluetooth 接続を介して端末間で高品質のオーディオをストリーミングできる方法を定義しています。
BluetoothHealth
Bluetooth サービスを制御する Health Device Profile プロキシを表します。
BluetoothHealthCallback
BluetoothHealth コールバックの実装に使用する抽象クラス。このクラスを拡張してコールバック メソッドを実装し、アプリの登録状態と Bluetooth チャンネルの状態の変更に関するアップデートを受信します。
BluetoothHealthAppConfiguration
Bluetooth Health サードパーティ アプリが、リモート Bluetooth ヘルス機器と通信するために登録したアプリの設定を表します。
BluetoothProfile.ServiceListener
特定のプロファイルを実行する内部サービスとの接続時および接続の切断時に BluetoothProfile プロセス間通信(IPC)クライアントに通知するインターフェース。