Conectar dispositivo Bluetooth

Para criar uma conexão entre dois dispositivos, é necessário implementar os mecanismos do lado do servidor e do cliente, porque um dispositivo precisa abrir um soquete de servidor e o outro precisa iniciar a conexão usando o endereço MAC do dispositivo do servidor. O dispositivo servidor e o dispositivo cliente recebem o BluetoothSocket necessário de maneiras diferentes. O servidor recebe informações de soquete quando uma conexão de entrada é aceita. O cliente fornece informações sobre o soquete quando abre um canal RFCOMM para o servidor.

O servidor e o cliente são considerados conectados entre si quando cada um tem um BluetoothSocket conectado no mesmo canal RFCOMM. Nesse ponto, cada dispositivo pode receber streams de entrada e saída, e a transferência de dados pode começar, o que é discutido na seção sobre como transferir dados do Bluetooth. Esta seção descreve como iniciar a conexão entre dois dispositivos.

Verifique se você tem as permissões de Bluetooth adequadas e configure seu app para usar esse recurso antes de tentar encontrar dispositivos Bluetooth.

Técnicas de conexão

Uma técnica de implementação é preparar automaticamente cada dispositivo como servidor para que cada dispositivo tenha um soquete de servidor aberto e detecte conexões. Nesse caso, qualquer um dos dispositivos pode iniciar uma conexão com o outro e se tornar o cliente. Como alternativa, um dispositivo pode hospedar explicitamente a conexão e abrir um soquete de servidor sob demanda, e o outro dispositivo inicia a conexão.


Figura 1. Caixa de diálogo de pareamento do Bluetooth.

Conectar como servidor

Para conectar dois dispositivos, um deles precisa atuar como servidor, mantendo um BluetoothServerSocket aberto. O objetivo do soquete do servidor é detectar solicitações de conexão de entrada e fornecer um BluetoothSocket conectado depois que uma solicitação é aceita. Quando o BluetoothSocket é adquirido do BluetoothServerSocket, o BluetoothServerSocket pode (e precisa) ser descartado, a menos que você queira que o dispositivo aceite mais conexões.

Para configurar um soquete de servidor e aceitar uma conexão, siga estas etapas:

  1. Para receber um BluetoothServerSocket, chame listenUsingRfcommWithServiceRecord(String, UUID).

    A string é um nome identificável do serviço, que o sistema grava automaticamente em uma nova entrada de banco de dados do Service Discovery Protocol (SDP) no dispositivo. Ele é arbitrário e pode ser simplesmente o nome do app. O identificador universal exclusivo (UUID) também está incluído na entrada SDP e forma a base para o contrato de conexão com o dispositivo cliente. Ou seja, quando o cliente tenta se conectar com esse dispositivo, ele carrega um UUID que identifica exclusivamente o serviço com que quer se conectar. Esses UUIDs precisam ser correspondentes para que a conexão seja aceita.

    Um UUID é um formato padronizado de 128 bits para um ID de string usado para identificar informações de maneira exclusiva. Um UUID é usado para identificar informações que precisam ser exclusivas em um sistema ou rede, porque a probabilidade de um UUID ser repetido é efetivamente zero. Eles são gerados de forma independente, sem o uso de uma autoridade centralizada. Nesse caso, ele é usado para identificar exclusivamente o serviço Bluetooth do app. Para ter um UUID a ser usado com seu app, use um dos muitos geradores aleatórios UUID na Web e inicialize um UUID com fromString(String).

  2. Comece a detectar solicitações de conexão chamando accept().

    Essa é uma chamada de bloqueio. Ela retorna quando uma conexão é aceita ou ocorre uma exceção. A conexão só será aceita quando um dispositivo remoto enviar uma solicitação de conexão contendo um UUID correspondente ao registrado nesse soquete do servidor de detecção. Quando bem-sucedido, accept() retorna um BluetoothSocket conectado.

  3. A menos que você queira aceitar outras conexões, chame close().

    Essa chamada de método libera o soquete do servidor e todos os recursos dele, mas não fecha o BluetoothSocket conectado retornado por accept(). Ao contrário do TCP/IP, o RFCOMM permite apenas um cliente conectado por canal por vez. Portanto, na maioria dos casos, faz sentido chamar close() no BluetoothServerSocket imediatamente após aceitar um soquete conectado.

Como a chamada accept() é de bloqueio, não a execute na linha de execução de interface da atividade principal. Executá-lo em outra linha de execução garante que seu app ainda possa responder a outras interações do usuário. Geralmente, faz sentido fazer todo o trabalho que envolve um BluetoothServerSocket ou BluetoothSocket em uma nova linha de execução gerenciada pelo app. Para cancelar uma chamada bloqueada, como accept(), chame close() no BluetoothServerSocket ou BluetoothSocket em outra linha de execução. Todos os métodos em um BluetoothServerSocket ou BluetoothSocket são seguros para linha de execução.

Exemplo

Veja a seguir uma linha de execução simplificada para o componente do servidor que aceita conexões de entrada:

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);
       }
   }
}

Neste exemplo, apenas uma conexão de entrada é desejada. Portanto, assim que uma conexão for aceita e a BluetoothSocket for adquirida, o app vai transmitir a BluetoothSocket adquirida para uma linha de execução separada, fechar a BluetoothServerSocket e sair da repetição.

Quando accept() retornar o BluetoothSocket, o soquete já estará conectado. Portanto, não chame connect(), como faz no lado do cliente.

O método manageMyConnectedSocket() específico do app foi projetado para iniciar a linha de execução para transferência de dados, o que é discutido no tópico sobre como transferir dados do Bluetooth.

Normalmente, você precisa fechar o BluetoothServerSocket assim que terminar de detectar as conexões recebidas. Neste exemplo, close() é chamado assim que o BluetoothSocket é adquirido. Também convém fornecer um método público na linha de execução que possa fechar o BluetoothSocket particular caso você precise parar de detectar no soquete do servidor.

Conectar como cliente

Para iniciar uma conexão com um dispositivo remoto que aceite conexões em um soquete de servidor aberto, primeiro consiga um objeto BluetoothDevice que represente o dispositivo remoto. Para aprender a criar um BluetoothDevice, consulte Encontrar dispositivos Bluetooth. É necessário usar o BluetoothDevice para adquirir um BluetoothSocket e iniciar a conexão.

Este é o procedimento básico:

  1. Usando o BluetoothDevice, receba um BluetoothSocket chamando createRfcommSocketToServiceRecord(UUID).

    Esse método inicializa um objeto BluetoothSocket que permite que o cliente se conecte a um BluetoothDevice. O UUID transmitido aqui precisa corresponder ao UUID usado pelo dispositivo servidor quando ele chamou listenUsingRfcommWithServiceRecord(String, UUID) para abrir o BluetoothServerSocket. Para usar um UUID correspondente, codifique a string UUID no seu app e faça referência a ela no código do servidor e do cliente.

  2. Inicie a conexão chamando connect(). Observe que esse método é uma chamada de bloqueio.

    Depois que um cliente chama esse método, o sistema executa uma pesquisa de SDP para encontrar o dispositivo remoto com o UUID correspondente. Se a pesquisa for bem-sucedida e o dispositivo remoto aceitar a conexão, ele compartilhará o canal RFCOMM para uso durante a conexão, e o método connect() será retornado. Se a conexão falhar ou se o método connect() expirar (após cerca de 12 segundos), o método vai gerar uma IOException.

Como connect() é uma chamada de bloqueio, sempre execute esse procedimento de conexão em uma linha de execução separada da linha de execução da atividade principal (interface).

Exemplo

Este é um exemplo básico de uma linha de execução de cliente que inicia uma conexão 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);
       }
   }
}

Nesse snippet, cancelDiscovery() é chamado antes da tentativa de conexão. Sempre chame cancelDiscovery() antes de connect(), principalmente porque cancelDiscovery() será bem-sucedido, independente de a descoberta do dispositivo estar em andamento ou não. Caso seu app precise determinar se a descoberta do dispositivo está em andamento, verifique usando isDiscovering().

O método manageMyConnectedSocket() específico do app foi projetado para iniciar a linha de execução para transferência de dados, o que é discutido na seção sobre como transferir dados do Bluetooth.

Quando terminar de usar o BluetoothSocket, sempre chame close(). Essa ação fecha imediatamente o soquete conectado e libera todos os recursos internos relacionados.