Bluetoothデバイスの接続

2 つのデバイス間の接続を作成するには、サーバー側とクライアント側の両方のメカニズムを実装する必要があります。一方のデバイスがサーバー ソケットを開く必要があり、もう一方のデバイスはサーバー デバイスの MAC アドレスを使用して接続を開始する必要があるためです。サーバー デバイスとクライアント デバイスは、それぞれ異なる方法で必要な BluetoothSocket を取得します。サーバーは受信接続が受け入れられると、ソケット情報を受信します。クライアントは、サーバーに対して RFCOMM チャネルを開くときにソケット情報を提供します。

サーバーとクライアントは、同じ RFCOMM チャネル上でそれぞれが接続されている BluetoothSocket を持っている場合、相互に接続されているとみなされます。この時点で、各デバイスは入出力ストリームを取得し、データ転送を開始できます。これについては、Bluetooth データの転送に関するセクションをご覧ください。このセクションでは、2 つのデバイス間で接続を開始する方法について説明します。

Bluetooth デバイスの検出を試みる前に、適切な Bluetooth の権限があることを確認し、Bluetooth 用にアプリをセットアップします。

接続方法

実装方法の 1 つは、各デバイスをサーバーとして自動的に準備し、各デバイスでサーバー ソケットを開いて接続をリッスンするようにすることです。この場合、どちらかのデバイスがもう片方のデバイスとの接続を開始して、クライアントになります。または、一方のデバイスで接続を明示的にホストし、オンデマンドでサーバー ソケットをオープンして、もう一方のデバイスで接続を開始することもできます。


図 1. Bluetooth ペア設定ダイアログ。

サーバーとして接続

2 つのデバイスを接続する場合、一方のデバイスは開いている BluetoothServerSocket を保持してサーバーとして動作する必要があります。サーバー ソケットの目的は、受信接続リクエストをリッスンし、リクエストが受け入れられた後に接続された BluetoothSocket を提供することです。BluetoothSocketBluetoothServerSocket から取得された場合、デバイスでそれ以上の接続を受け入れる場合を除き、BluetoothServerSocket は破棄できます(破棄する必要があります)。

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

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

    この文字列はサービスの識別可能な名前であり、デバイス上の新しいサービス ディスカバリ プロトコル(SDP)データベース エントリに自動的に書き込まれます。名前は任意で、単純にアプリ名を指定できます。Universally Unique Identifier(UUID)も SDP エントリに含まれ、クライアント デバイスとの接続の合意の基礎を形成します。つまり、クライアントはこのデバイスと接続しようとすると、接続するサービスを一意に識別する UUID を受け取ります。接続が受け入れられるには、これらの UUID が一致している必要があります。

    UUID は、情報を一意に識別するために使用される、文字列 ID の標準化された 128 ビット形式です。UUID は、UUID が繰り返される確率が実質的にゼロであるため、システムまたはネットワーク内で一意にする必要がある情報を識別するために使用されます。中央機関を使用せずに、独立して生成されます。この場合、アプリの Bluetooth サービスを一意に識別するために使用されます。アプリで使用する UUID を取得するには、ウェブ上で多くのランダムな UUID ジェネレータのいずれかを使用して、UUID を fromString(String) で初期化します。

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

    これはブロッキング呼び出しです。接続が受け入れられたか、例外が発生したときに返されます。接続が受け入れられるのは、リモート デバイスが、このリスニング サーバー ソケットに登録されているものと一致する UUID を含む接続リクエストを送信した場合のみです。成功すると、accept() は接続された BluetoothSocket を返します。

  3. 追加の接続を受け入れる場合を除き、close() を呼び出します。

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

accept() 呼び出しはブロッキング呼び出しであるため、メイン アクティビティの UI スレッドでは実行しないでください。別のスレッドで実行することで、アプリが他のユーザー操作に引き続き応答できるようになります。通常は、アプリが管理する新しいスレッドで BluetoothServerSocket または BluetoothSocket を含むすべての処理を行うのが合理的です。accept() などのブロックされた呼び出しを中止するには、別のスレッドから BluetoothServerSocket または BluetoothSocketclose() を呼び出します。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() メソッドは、データを転送するためのスレッドを開始するように設計されています。これについては、Bluetooth データの転送に関するトピックで説明しています。

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

クライアントとして接続

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

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

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

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

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

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

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

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?.let { 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() が呼び出されています。常に connect() の前に cancelDiscovery() を呼び出す必要があります。これは、現在デバイスの検出が進行中かどうかにかかわらず、cancelDiscovery() は成功するためです。アプリがデバイス検出が進行中かどうかを判断する必要がある場合は、isDiscovering() を使用して確認できます。

アプリ固有の manageMyConnectedSocket() メソッドは、データ転送用のスレッドを開始するように設計されています。これについては、Bluetooth データの転送に関するセクションをご覧ください。

BluetoothSocket による作業が完了したら、必ず close() を呼び出します。これを行うと、接続されているソケットがすぐに閉じられ、関連するすべての内部リソースが解放されます。