Introducción general a Bluetooth

La plataforma de Android incluye compatibilidad con la pila de red Bluetooth, que permite que un dispositivo intercambie datos de forma inalámbrica con otros dispositivos Bluetooth. El framework de la aplicación proporciona acceso a la funcionalidad de Bluetooth a través de las APIs de Bluetooth de Android. Estas APIs permiten que las aplicaciones se conecten de forma inalámbrica con otros dispositivos Bluetooth, lo que habilita las funciones inalámbricas punto a punto y de varios puntos.

Con las APIs de Bluetooth, una aplicación para Android puede realizar lo siguiente:

  • Buscar otros dispositivos Bluetooth
  • Consultar el adaptador local de Bluetooth en busca de dispositivos Bluetooth sincronizados
  • Establecer canales RFCOMM
  • Conectarse con otros dispositivos mediante el descubrimiento de servicios
  • Transferir datos hacia otros dispositivos y desde estos
  • Administrar varias conexiones

Esta página se enfoca en el Bluetooth clásico. El Bluetooth clásico es la opción ideal para las operaciones que consumen más batería, como la transmisión y la comunicación entre dispositivos Android. Para dispositivos Bluetooth con bajos requisitos de energía, Android 4.3 (nivel de API 18) presenta compatibilidad de API con Bluetooth de bajo consumo. Para obtener más información, consulta Bluetooth de bajo consumo.

En este documento, se describen diferentes perfiles de Bluetooth, incluido el perfil del dispositivo de salud. Luego, se explica cómo usar las APIs de Bluetooth de Android para realizar las cuatro tareas principales necesarias para comunicarse a través de Bluetooth: configurar Bluetooth, encontrar dispositivos que estén vinculados o disponibles en el área local, conectar dispositivos y transferir datos entre dispositivos.

Conceptos básicos

Para que los dispositivos con Bluetooth puedan transmitir datos entre sí, primero deben formar un canal de comunicación mediante un proceso de vinculación. Un dispositivo, un dispositivo detectable, se pone a disposición para las solicitudes de conexión entrantes. Otro dispositivo encuentra el dispositivo detectable mediante un proceso de descubrimiento de servicios. Después de que el dispositivo detectable acepta la solicitud de vinculación, los dos dispositivos completan un proceso de vinculación en el que intercambian llaves de seguridad. Ambos dispositivos almacenan estas claves en caché para usarlas posteriormente. Una vez que se completan los procesos de vinculación y vinculación, los dos dispositivos intercambian información. Cuando se completa la sesión, el dispositivo que inició la solicitud de vinculación libera el canal que lo había vinculado al dispositivo detectable. Sin embargo, los dos dispositivos permanecen vinculados para que puedan volver a conectarse automáticamente durante una sesión futura, siempre y cuando estén dentro del alcance entre sí y ninguno de los dispositivos haya eliminado la vinculación.

Permisos de Bluetooth

Para usar las funciones de Bluetooth en tu aplicación, debes declarar dos permisos. El primero es BLUETOOTH. Necesitas este permiso para realizar cualquier comunicación por Bluetooth, como solicitar una conexión, aceptar una conexión y transferir datos.

El otro permiso que debes declarar es ACCESS_FINE_LOCATION. Tu app necesita este permiso porque se puede usar una búsqueda de Bluetooth para recopilar información sobre la ubicación del usuario. Esta información puede provenir de los dispositivos del usuario o de las balizas Bluetooth que se usan en ubicaciones, como tiendas y centros de transporte público.

Los servicios que se ejecutan en Android 10 y versiones posteriores no pueden detectar dispositivos Bluetooth, a menos que tengan el permiso ACCESS_BACKGROUND_LOCATION. Para obtener más información sobre este requisito, consulta Cómo acceder a la ubicación en segundo plano.

En el siguiente fragmento de código, se muestra cómo verificar el permiso.

Kotlin

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    if (ContextCompat.checkSelfPermission(baseContext,
        Manifest.permission.ACCESS_BACKGROUND_LOCATION)
        != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(
                this,
                arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION),
                    PERMISSION_CODE)
        }
}

Java

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
  if (ContextCompat.checkSelfPermission(baseContext,
      Manifest.permission.ACCESS_BACKGROUND_LOCATION)
      != PackageManager.PERMISSION_GRANTED) {
          ActivityCompat.requestPermissions(
              MyActivity.this,
              new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION},
                  PERMISSION_CODE)
      }
}

Una excepción a este requisito de permiso ocurre cuando la app se instala en un dispositivo con Android 11 o versiones posteriores, y usa la vinculación de dispositivos complementarios para asociar un dispositivo. En este caso, una vez que se asocia un dispositivo, las apps pueden buscar sus dispositivos Bluetooth asociados sin requerir un permiso de ubicación.

En los dispositivos que ejecutan Android 8.0 (nivel de API 26) y versiones posteriores, puedes usar CompanionDeviceManager para realizar una búsqueda de dispositivos complementarios cercanos en nombre de tu app sin requerir el permiso de ubicación. Para obtener más información sobre esta opción, consulta Vinculación de dispositivos complementarios.

Nota: Si tu app se orienta a Android 9 (nivel de API 28) o versiones anteriores, puedes declarar el permiso ACCESS_COARSE_LOCATION en lugar del permiso ACCESS_FINE_LOCATION.

Si quieres que tu app inicie la detección de dispositivos o controle la configuración de Bluetooth, debes declarar los permisos BLUETOOTH_ADMIN y BLUETOOTH. La mayoría de las aplicaciones necesitan este permiso solamente para poder descubrir dispositivos Bluetooth locales. Las otras capacidades que otorga este permiso no deben usarse, a menos que la aplicación sea un "administrador de energía" que modifique la configuración de Bluetooth a pedido del usuario.

Declara los permisos de Bluetooth del archivo de manifiesto de tu aplicación. Por ejemplo:

<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>

Consulta la referencia <uses-permission> para obtener más información sobre cómo declarar permisos de la aplicación.

Trabajar con perfiles

A partir de Android 3.0, la API de Bluetooth incluye compatibilidad para trabajar con perfiles de Bluetooth. Un perfil de Bluetooth es una especificación de interfaz inalámbrica para la comunicación entre dispositivos basada en Bluetooth. Un ejemplo es el perfil de manos libres. Para que un teléfono celular se conecte a auriculares inalámbricos, ambos dispositivos deben admitir el perfil de manos libres.

La API de Bluetooth de Android proporciona implementaciones para los siguientes perfiles de Bluetooth:

  • Auriculares. El perfil de auriculares ofrece compatibilidad con auriculares Bluetooth para usarlos con teléfonos móviles. Android proporciona la clase BluetoothHeadset, que es un proxy para controlar el servicio de auriculares Bluetooth. Esto incluye los perfiles de manos libres (v1.5) y de auriculares Bluetooth. La clase BluetoothHeadset incluye compatibilidad con comandos de AT. Para obtener más información sobre este tema, consulta Comandos de AT específicos del proveedor.
  • A2DP: El perfil de distribución de audio avanzada (A2DP) define el nivel de audio de alta calidad que se puede transmitir de un dispositivo a otro a través de una conexión Bluetooth. Android proporciona la clase BluetoothA2dp, que es un proxy para controlar el servicio de A2DP Bluetooth.
  • Dispositivo de salud. Android 4.0 (nivel de API 14) incluye compatibilidad con el perfil de dispositivos de salud (HDP) Bluetooth. Esto te permite crear aplicaciones que usan Bluetooth para comunicarse con dispositivos de salud compatibles, como monitores de frecuencia cardíaca, medidores de la sangre, termómetros y balanzas, entre otros. Para obtener una lista de los dispositivos compatibles y sus códigos de especialización de datos del dispositivo correspondientes, consulta las especializaciones de datos de dispositivos del HDP de Bluetooth. También se hace referencia a estos valores en la especificación ISO/IEEE 11073-20601 [7] como MDC_DEV_SPEC_PROFILE_* en el anexo de códigos de nomenclatura. Para obtener más información sobre el HDP, consulta Perfil de dispositivos de salud.

A continuación, se detallan los pasos básicos para trabajar con un perfil:

  1. Obtén el adaptador predeterminado, como se describe en Cómo configurar Bluetooth.
  2. Configura un BluetoothProfile.ServiceListener. Este objeto de escucha notifica a los clientes BluetoothProfile cuando se conectan al servicio o se desconectan de él.
  3. Usa getProfileProxy() para establecer una conexión con el objeto del proxy de perfil asociado al perfil. En el siguiente ejemplo, el objeto del proxy de perfil es una instancia de BluetoothHeadset.
  4. En onServiceConnected(), obtén un handle para el objeto del proxy de perfil.
  5. Una vez que tengas el objeto del proxy de perfil, puedes usarlo para supervisar el estado de la conexión y realizar otras operaciones relevantes para ese perfil.

Por ejemplo, en este fragmento de código, se muestra cómo conectarse a un objeto proxy BluetoothHeadset para que puedas controlar el perfil de auriculares:

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

Comandos de AT específicos del proveedor

A partir de Android 3.0 (nivel de API 11), las aplicaciones pueden registrarse para recibir transmisiones del sistema de comandos de AT específicos del proveedor predefinidos que envían los auriculares (como un comando +XEVENT de Plantronics). Por ejemplo, una aplicación podría recibir transmisiones que indiquen el nivel de batería de un dispositivo conectado y notificar al usuario o tomar otras medidas, según sea necesario. Crea un receptor de emisión para el intent ACTION_VENDOR_SPECIFIC_HEADSET_EVENT a fin de controlar los comandos de AT específicos del proveedor para los auriculares.

Perfil de dispositivos de salud

Android 4.0 (nivel de API 14) presenta compatibilidad con el perfil de dispositivos de salud (HDP) Bluetooth. Esto te permite crear aplicaciones que usan Bluetooth para comunicarse con dispositivos de salud compatibles con Bluetooth, como monitores de frecuencia cardíaca, medidores de la sangre, termómetros y balanzas. La API de Bluetooth Health incluye las clases BluetoothHealth, BluetoothHealthCallback y BluetoothHealthAppConfiguration, que se describen en Interfaces y clases clave.

Cuando se usa la API de salud de Bluetooth, resulta útil comprender estos conceptos clave del HDP:

Source
Dispositivo de salud, como una báscula de peso, un glucosímetro o un termómetro, que transmite datos médicos a un dispositivo inteligente, como un teléfono o una tablet Android.
Receptor
Es el dispositivo inteligente que recibe los datos médicos. En una aplicación del HDP de Android, el receptor se representa con un objeto BluetoothHealthAppConfiguration.
Registro
Es el proceso que se usa para registrar un receptor a fin de comunicarse con un dispositivo de salud específico.
Conexión
Es el proceso que se usa para abrir un canal entre un dispositivo de salud (fuente) y un dispositivo inteligente (receptor).

Crear una aplicación del HDP

Estos son los pasos básicos para crear una aplicación del HDP de Android:

  1. Obtén una referencia al objeto de proxy BluetoothHealth.

    Al igual que con los dispositivos de perfil A2DP y de auriculares normales, debes llamar a getProfileProxy() con un BluetoothProfile.ServiceListener y el tipo de perfil HEALTH para establecer una conexión con el objeto del proxy de perfil.

  2. Crea un BluetoothHealthCallback y registra una configuración de la aplicación (BluetoothHealthAppConfiguration) que actúe como receptor de estado.
  3. Establece una conexión con el dispositivo de salud.

    Nota: Algunos dispositivos inician la conexión automáticamente. No es necesario seguir este paso en el caso de esos dispositivos.

  4. Cuando te conectes de forma correcta a un dispositivo de salud, realiza operaciones de lectura o escritura en el dispositivo de salud mediante el descriptor de archivo. Los datos recibidos deben interpretarse mediante un administrador de salud que implemente las especificaciones IEEE 11073.
  5. Cuando finalices, cierra el canal de salud y elimina el registro de la aplicación. El canal también se cierra cuando existe inactividad por tiempo prolongado.

Configurar el sistema Bluetooth

Antes de que tu aplicación pueda comunicarse a través de Bluetooth, debes verificar que Bluetooth sea compatible con el dispositivo y, de ser así, asegurarte de que esté habilitado.

Si Bluetooth no es compatible, debes inhabilitar correctamente cualquier función de Bluetooth. Si Bluetooth es compatible, pero está inhabilitado, puedes solicitar que el usuario lo habilite sin salir de tu aplicación. Esta configuración se realiza en dos pasos con BluetoothAdapter:

  1. Obtén el BluetoothAdapter.

    Se requiere el BluetoothAdapter para toda la actividad Bluetooth. Para obtener el BluetoothAdapter, llama al método estático getDefaultAdapter(). Se mostrará un BluetoothAdapter que representa el propio adaptador de Bluetooth del dispositivo (la radio Bluetooth). Existe un adaptador de Bluetooth para todo el sistema y tu aplicación puede interactuar con él usando este objeto. Si getDefaultAdapter() muestra null, significa que el dispositivo no es compatible con Bluetooth. Por ejemplo:

    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. Habilita Bluetooth.

    A continuación, debes asegurarte de que Bluetooth esté habilitado. Llama a isEnabled() para verificar si Bluetooth está habilitado actualmente. Si este método muestra "false", Bluetooth está inhabilitado. Para solicitar que se habilite Bluetooth, llama a startActivityForResult() y pasa una acción de intent ACTION_REQUEST_ENABLE. Esta llamada emite una solicitud para habilitar Bluetooth a través de la configuración del sistema (sin detener tu aplicación). Por ejemplo:

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

    Aparecerá un cuadro de diálogo en el que se le solicitará al usuario permiso para habilitar Bluetooth, como se muestra en la Figura 1. Si el usuario responde "Sí", el sistema comienza a habilitar Bluetooth y el enfoque regresa a tu aplicación una vez que se completa (o falla) el proceso.

    Figura 1: Diálogo de habilitación de Bluetooth

    La constante REQUEST_ENABLE_BT que se pasa a startActivityForResult() es un número entero definido localmente que debe ser mayor que 0. El sistema te devuelve esta constante en la implementación de onActivityResult() como el parámetro requestCode.

    Si la habilitación de Bluetooth se realiza correctamente, tu actividad recibe el código de resultado RESULT_OK en la devolución de llamada onActivityResult(). Si Bluetooth no se habilitó debido a un error (o a que el usuario respondió "No"), el código de resultado será RESULT_CANCELED.

De manera opcional, tu aplicación también puede escuchar el intent de transmisión ACTION_STATE_CHANGED, que el sistema emite cada vez que cambia el estado de Bluetooth. Esta transmisión incluye los campos adicionales EXTRA_STATE y EXTRA_PREVIOUS_STATE, que incluyen los estados de Bluetooth nuevo y antiguo, respectivamente. Los valores posibles para estos campos adicionales son STATE_TURNING_ON, STATE_ON, STATE_TURNING_OFF y STATE_OFF. La escucha de esta transmisión puede ser útil si tu app necesita detectar cambios en el tiempo de ejecución realizados en el estado de Bluetooth.

Nota: Cuando se habilita la visibilidad, se habilita Bluetooth automáticamente. Si planeas habilitar la visibilidad de dispositivos de manera constante antes de realizar la actividad de Bluetooth, puedes omitir el paso 2 anterior. Para obtener más información, consulta la sección Cómo habilitar la visibilidad en esta página.

Buscar dispositivos

Con el BluetoothAdapter, puedes encontrar dispositivos Bluetooth remotos mediante la detección de dispositivos o consultando la lista de dispositivos vinculados.

La detección de dispositivos es un procedimiento de escaneo que busca dispositivos compatibles con Bluetooth en el área local y solicita información sobre cada uno. A veces, este proceso se denomina descubrimiento, consulta o análisis. Sin embargo, un dispositivo Bluetooth cercano responde a una solicitud de detección solo si actualmente acepta solicitudes de información por ser detectable. Si un dispositivo es detectable, comparte información como el nombre del dispositivo, su clase y su dirección MAC única como respuesta a la solicitud de descubrimiento. Con esta información, el dispositivo que realiza el proceso de descubrimiento puede elegir iniciar una conexión con el dispositivo detectado.

Como los dispositivos detectables pueden revelar información sobre la ubicación del usuario, el proceso de descubrimiento del dispositivo requiere acceso a la ubicación. Si la app se usa en un dispositivo con Android 8.0 (nivel de API 26) o una versión posterior, usa la API de Companion Device Manager. Esta API realiza el descubrimiento de dispositivos en nombre de tu app, por lo que esta no necesita solicitar permisos de ubicación.

Una vez que se establece una conexión con un dispositivo remoto por primera vez, se presenta automáticamente al usuario una solicitud de vinculación. Cuando se vincula un dispositivo, la información básica sobre este (como el nombre, la clase y la dirección MAC) se guarda y se puede leer con las APIs de Bluetooth. Usando la dirección MAC conocida de un dispositivo remoto, se puede establecer una conexión con este en cualquier momento sin llevar a cabo la detección, suponiendo que el dispositivo aún se encuentre dentro del rango.

Ten presente que existe una diferencia entre la vinculación y la conexión:

  • Para estar vinculados, dos dispositivos conocen la existencia del otro, tienen una clave de vínculo compartida que se puede usar para la autenticación y son capaces de establecer una conexión encriptada entre sí.
  • Para estar conectados, los dispositivos actualmente comparten un canal RFCOMM y pueden transmitir datos entre sí. Las API de Bluetooth de Android actuales requieren que los dispositivos estén vinculados para que se pueda establecer una conexión RFCOMM. La vinculación se realiza automáticamente cuando inicias una conexión encriptada con las APIs de Bluetooth.

En las siguientes secciones, se describe cómo encontrar dispositivos vinculados o nuevos con la detección de dispositivos.

Nota: Los dispositivos con Android no son detectables de forma predeterminada. Un usuario puede hacer que el dispositivo sea detectable durante un tiempo limitado a través de la configuración del sistema o una aplicación puede solicitar que el usuario habilite la visibilidad sin salir de la aplicación. Para obtener más información, consulta la sección Cómo habilitar la visibilidad en esta página.

Realizar consultas a dispositivos sincronizados

Antes de llevar a cabo la detección de dispositivos, es conveniente consultar el conjunto de dispositivos vinculados para ver si el dispositivo deseado ya es conocido. Para ello, llama a getBondedDevices(). Se muestra un conjunto de objetos BluetoothDevice que representan dispositivos vinculados. Por ejemplo, puedes consultar todos los dispositivos vinculados y obtener el nombre y la dirección MAC de cada uno, como se muestra en el siguiente fragmento de código:

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

Para iniciar una conexión con un dispositivo Bluetooth, todo lo que se necesita del objeto BluetoothDevice asociado es la dirección MAC, que recuperas llamando a getAddress(). Puedes obtener más información para crear una conexión en la sección Conexión de dispositivos.

Precaución: La detección de dispositivos consume muchos recursos del adaptador de Bluetooth. Después de encontrar un dispositivo al cual conectarte, asegúrate de detener la detección con cancelDiscovery() antes de intentar establecer una conexión. Además, no debes realizar la detección mientras estás conectado a un dispositivo, ya que el proceso de descubrimiento reduce significativamente el ancho de banda disponible para las conexiones existentes.

Detectar dispositivos

Para comenzar a detectar dispositivos, simplemente llama a startDiscovery(). El proceso es asíncrono y muestra un valor booleano que indica si el descubrimiento se inició de forma correcta. Por lo general, el proceso de descubrimiento implica un análisis de consulta de aproximadamente 12 segundos, seguido de un escaneo de la página de cada dispositivo encontrado para recuperar su nombre de Bluetooth.

A fin de recibir información sobre cada dispositivo detectado, tu aplicación debe registrar un BroadcastReceiver para el intent ACTION_FOUND. El sistema transmite este intent por cada dispositivo. El intent contiene los campos adicionales EXTRA_DEVICE y EXTRA_CLASS, que, a su vez, contienen un BluetoothDevice y un BluetoothClass, respectivamente. En el siguiente fragmento de código, se muestra cómo puedes registrarte para controlar la transmisión cuando se detectan los dispositivos:

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

Para iniciar una conexión con un dispositivo Bluetooth, todo lo que se necesita del objeto BluetoothDevice asociado es la dirección MAC, que recuperas llamando a getAddress(). Puedes obtener más información para crear una conexión en la sección Conexión de dispositivos.

Precaución: La detección de dispositivos consume muchos recursos del adaptador de Bluetooth. Después de encontrar un dispositivo al cual conectarte, asegúrate de detener la detección con cancelDiscovery() antes de intentar establecer una conexión. Además, no debes realizar la detección mientras estás conectado a un dispositivo, ya que el proceso de descubrimiento reduce significativamente el ancho de banda disponible para las conexiones existentes.

Habilitar la visibilidad

Si deseas que el dispositivo local sea detectable para otros dispositivos, llama a startActivityForResult(Intent, int) con el intent ACTION_REQUEST_DISCOVERABLE. Esto emite una solicitud para habilitar el modo detectable del sistema sin tener que navegar a la app de Configuración, lo que detenería tu propia app. De forma predeterminada, el dispositivo se hace detectable durante 120 segundos o 2 minutos. Puedes definir una duración diferente, hasta 3,600 segundos (1 hora), si agregas el EXTRA_DISCOVERABLE_DURATION adicional.

Precaución: Si estableces el valor adicional EXTRA_DISCOVERABLE_DURATION en 0, el dispositivo siempre será detectable. Esta configuración no es segura y, por lo tanto, no se recomienda.

Con el siguiente fragmento de código, el dispositivo permanece detectable durante cinco minutos (300 segundos):

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);
Figura 2: Diálogo de habilitación de la visibilidad

Aparecerá un cuadro de diálogo en el que se solicitará permiso al usuario para que el dispositivo sea detectable, como se muestra en la Figura 2. Si el usuario responde "Sí", el dispositivo será detectable durante el período especificado. Luego, tu actividad recibe una llamada a la devolución de llamada onActivityResult(), con un código de resultado igual a la duración durante la cual el dispositivo es detectable. Si el usuario respondió "No" o si se produjo un error, el código de resultado será RESULT_CANCELED.

Nota: Si no se habilitó Bluetooth en el dispositivo, se habilitará Bluetooth automáticamente cuando se habilite la visibilidad de dispositivo.

El dispositivo se mantendrá silenciosamente en el modo detectable durante el tiempo asignado. Si deseas recibir una notificación cuando cambie el modo detectable, puedes registrar un BroadcastReceiver para el intent ACTION_SCAN_MODE_CHANGED. Este intent contiene los campos adicionales EXTRA_SCAN_MODE y EXTRA_PREVIOUS_SCAN_MODE, que proporcionan el modo de análisis nuevo y el antiguo, respectivamente. Estos son los posibles valores de cada campo adicional:

SCAN_MODE_CONNECTABLE_DISCOVERABLE
El dispositivo está en modo detectable.
SCAN_MODE_CONNECTABLE
El dispositivo no está en modo detectable, pero puede recibir conexiones de todos modos.
SCAN_MODE_NONE
El dispositivo no está en modo detectable y no puede recibir conexiones.

Si inicias la conexión a un dispositivo remoto, no es necesario que habilites la visibilidad del dispositivo. Solo es necesario habilitar la visibilidad cuando quieres que tu aplicación aloje un socket de servidor que acepte conexiones entrantes, ya que los dispositivos remotos deben poder descubrir otros dispositivos antes de iniciar conexiones en ellos.

Conectar dispositivos

Para crear una conexión entre dos dispositivos, debes implementar los mecanismos del servidor y del cliente, ya que un dispositivo debe abrir un socket de servidor y el otro debe iniciar la conexión mediante la dirección MAC del dispositivo del servidor. El dispositivo del servidor y el dispositivo del cliente obtienen el BluetoothSocket requerido de diferentes maneras. El servidor recibe la información del socket cuando se acepta una conexión entrante. El cliente proporciona información del socket cuando abre un canal RFCOMM al servidor.

El servidor y el cliente se consideran conectados entre sí cuando tienen un BluetoothSocket conectado en el mismo canal RFCOMM. En este punto, cada dispositivo puede obtener flujos de entrada y salida, y se puede iniciar la transferencia de datos, lo que se analiza en la sección Cómo administrar una conexión. En esta sección, se describe cómo iniciar la conexión entre dos dispositivos.

Técnicas de conexión

Una técnica de implementación consiste en preparar automáticamente cada dispositivo como un servidor, de modo que cada uno tenga un socket de servidor abierto y escuche las conexiones. En este caso, cualquiera de los dispositivos puede iniciar una conexión con el otro y convertirse en cliente. Como alternativa, un dispositivo puede alojar explícitamente la conexión y abrir un socket de servidor a pedido, y el otro dispositivo inicia la conexión.

Figura 3: Diálogo de vinculación de Bluetooth

Nota: Si los dos dispositivos no se vincularon anteriormente, el framework de Android muestra automáticamente una notificación de solicitud de vinculación o un diálogo al usuario durante el procedimiento de conexión, como se muestra en la Figura 3. Por lo tanto, cuando tu aplicación intenta conectar dispositivos, no es necesario preocuparse por si los dispositivos están sincronizados o no. El intento de conexión RFCOMM se bloquea hasta que el usuario vincule correctamente los dos dispositivos, y falla si el usuario rechaza la vinculación, o si este proceso falla o se agota el tiempo de espera.

Conectarse como servidor

Cuando desees conectar dos dispositivos, uno deberá actuar como servidor y tener un BluetoothServerSocket abierto. El propósito del socket de servidor es escuchar las solicitudes de conexión entrantes y proporcionar un BluetoothSocket conectado cuando se acepta una solicitud. Cuando el BluetoothSocket se adquiere del BluetoothServerSocket, el BluetoothServerSocket puede y debería descartarse, a menos que quieras que el dispositivo acepte más conexiones.

Para configurar un socket de servidor y aceptar una conexión, completa la siguiente secuencia de pasos:

  1. Llama a listenUsingRfcommWithServiceRecord() para obtener un BluetoothServerSocket.

    La string es un nombre de identificación de tu servicio, que el sistema escribe automáticamente en una entrada nueva de la base de datos del protocolo de detección de servicios (SDP) en el dispositivo. El nombre es arbitrario y puede ser simplemente el de tu aplicación. El identificador único universal (UUID) también se incluye en la entrada del SDP y constituye la base del acuerdo de conexión con el dispositivo del cliente. Es decir, cuando el cliente intenta conectarse con este dispositivo, lleva un UUID que identifica de forma única el servicio con el que desea conectarse. Estos UUID deben coincidir para que se acepte la conexión.

    Un UUID es un formato estandarizado de 128 bits para un ID de cadena que se usa con el objetivo de identificar información de manera única. La ventaja de un UUID es que es lo suficientemente grande como para que puedas seleccionar cualquier ID aleatorio y que no haya conflictos. En este caso, se usa para identificar de manera única el servicio Bluetooth de tu aplicación. A fin de obtener un UUID para usar con tu aplicación, puedes utilizar uno de los numerosos generadores de UUID aleatorios en la Web y, luego, inicializar un UUID con fromString(String).

  2. Llama a accept() para comenzar a detectar solicitudes de conexión.

    Esta es una llamada de bloqueo. Se muestra cuando se acepta una conexión o se produce una excepción. Una conexión se acepta solo cuando un dispositivo remoto envía una solicitud de conexión que contiene un UUID que coincide con el que se registró con este socket de servidor de escucha. Si se realiza de forma correcta, accept() muestra un BluetoothSocket conectado.

  3. A menos que desees aceptar conexiones adicionales, llama a close().

    Este método libera el socket de servidor y todos sus recursos, pero no cierra el BluetoothSocket conectado que muestra accept(). A diferencia del TCP/IP, RFCOMM solo permite un cliente conectado por canal a la vez, por lo que en la mayoría de los casos, tiene sentido llamar a close() en el BluetoothServerSocket inmediatamente después de aceptar un socket conectado.

Como la llamada a accept() es de bloqueo, no se debe ejecutar en el subproceso de IU de la actividad principal para que tu aplicación pueda seguir respondiendo a otras interacciones del usuario. Por lo general, tiene sentido hacer todo el trabajo que involucra BluetoothServerSocket o BluetoothSocket en un subproceso nuevo que administra tu aplicación. Para anular una llamada bloqueada, como accept(), llama a close() en BluetoothServerSocket o BluetoothSocket desde otro subproceso. Ten en cuenta que todos los métodos en BluetoothServerSocket o BluetoothSocket cuentan con protección de subprocesos.

Ejemplo

A continuación, se muestra un subproceso simplificado para el componente de servidor que acepta conexiones entrantes:

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

En este ejemplo, solo se desea una conexión entrante, por lo que, apenas se acepta una conexión y se adquiere BluetoothSocket, la app pasa el BluetoothSocket adquirido a un subproceso independiente, cierra el BluetoothServerSocket y sale del bucle.

Ten en cuenta que, cuando accept() muestra el BluetoothSocket, el socket ya está conectado. Por lo tanto, no debes llamar a connect(), como lo haces del lado del cliente.

El método manageMyConnectedSocket() específico de la app está diseñado para iniciar el subproceso de transferencia de datos, que se analiza en la sección Cómo administrar una conexión.

Por lo general, debes cerrar tu BluetoothServerSocket en cuanto termines de escuchar las conexiones entrantes. En este ejemplo, se llama a close() en cuanto se adquiere el objeto BluetoothSocket. También te recomendamos que proporciones un método público en tu subproceso que pueda cerrar el BluetoothSocket privado en caso de que necesites dejar de escuchar en ese socket de servidor.

Conectarse como cliente

Para iniciar una conexión con un dispositivo remoto que acepta conexiones en un socket de servidor abierto, primero debes obtener un objeto BluetoothDevice que represente al dispositivo remoto. Para obtener información sobre cómo crear un BluetoothDevice, consulta Cómo buscar dispositivos. Luego, debes usar BluetoothDevice para adquirir un BluetoothSocket y, luego, iniciar la conexión.

El procedimiento básico es el siguiente:

  1. Con el BluetoothDevice, obtén un BluetoothSocket llamando a createRfcommSocketToServiceRecord(UUID).

    Este método inicializa un objeto BluetoothSocket que permite al cliente conectarse a un BluetoothDevice. El UUID que se pasa aquí debe coincidir con el que usó el dispositivo del servidor cuando llamó a listenUsingRfcommWithServiceRecord(String, UUID) para abrir su BluetoothServerSocket. Para usar un UUID que coincida, codifica la string del UUID en tu aplicación y, luego, haz referencia a ella desde el código del servidor y del cliente.

  2. Para iniciar la conexión, llama a connect(). Ten presente que este método es una llamada de bloqueo.

    Después de que un cliente llama a este método, el sistema realiza una búsqueda del SDP para encontrar el dispositivo remoto con el UUID que coincide. Si la búsqueda se realiza correctamente y el dispositivo remoto acepta la conexión, este comparte el canal RFCOMM que se usará durante la conexión, y se muestra el método connect(). Si la conexión falla o si se agota el tiempo de espera del método connect() (después de unos 12 segundos), el método arroja una IOException.

    Como connect() es una llamada de bloqueo, siempre debes realizar este procedimiento de conexión en un subproceso independiente del subproceso de la IU de la actividad principal.

    Nota: Siempre debes llamar a cancelDiscovery() para asegurarte de que el dispositivo no esté realizando la detección de dispositivos antes de llamar a connect(). Si el descubrimiento está en curso, el intento de conexión se ralentiza considerablemente y es más probable que falle.

Ejemplo

A continuación, se muestra un ejemplo básico de un subproceso de cliente que inicia una conexión 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);
        }
    }
}

Ten en cuenta que, en este fragmento, se llama a cancelDiscovery() antes de que se produzca el intento de conexión. Siempre debes llamar a cancelDiscovery() antes de connect(), especialmente porque cancelDiscovery() funciona correctamente independientemente de si el descubrimiento del dispositivo está en curso o no. Sin embargo, si tu app necesita determinar si el descubrimiento de dispositivos está en curso, puedes verificarlo con isDiscovering().

El método manageMyConnectedSocket() específico de la app está diseñado para iniciar el subproceso de transferencia de datos, que se analiza en la sección Cómo administrar una conexión.

Cuando termines con BluetoothSocket, siempre llama a close(). Cuando lo hagas, se cerrará de inmediato el socket conectado y se liberarán todos los recursos internos relacionados.

Administrar una conexión

Después de conectar correctamente varios dispositivos, cada uno tiene un BluetoothSocket conectado. Aquí es donde comienza la diversión porque puedes compartir información entre dispositivos. Con BluetoothSocket, el procedimiento general para transferir datos es el siguiente:

  1. Obtén el InputStream y el OutputStream que controlan las transmisiones a través del socket mediante getInputStream() y getOutputStream(), respectivamente.
  2. Lee y escribe datos en las transmisiones con read(byte[]) y write(byte[]).

Por supuesto, existen detalles relacionados con la implementación que deben tenerse en cuenta. En particular, debes usar un subproceso dedicado para leer desde la transmisión y escribir en ella. Esto es importante porque los métodos read(byte[]) y write(byte[]) son llamadas de bloqueo. El método read(byte[]) se bloquea hasta que haya algo que leer de la transmisión. Por lo general, el método write(byte[]) no realiza el bloqueo, pero sí puede bloquearlo para el control del flujo si el dispositivo remoto no llama a read(byte[]) lo suficientemente rápido y, como resultado, se llenan los búferes intermedios. Por lo tanto, el bucle principal del subproceso debe dedicarse a leer desde InputStream. Se puede usar un método público separado en el subproceso para iniciar operaciones de escritura en OutputStream.

Ejemplo

Este es un ejemplo de cómo puedes transferir datos entre dos dispositivos conectados a través de Bluetooth:

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

Después de que el constructor obtiene las transmisiones necesarias, el subproceso espera a que los datos lleguen a través de InputStream. Cuando read(byte[]) muestra datos de la transmisión, estos se envían a la actividad principal mediante un miembro Handler de la clase superior. Luego, el subproceso espera a que se lean más bytes del InputStream.

El envío de datos salientes solo implica llamar al método write() del subproceso desde la actividad principal y pasar los bytes que se enviarán. Este método llama a write(byte[]) para enviar los datos al dispositivo remoto. Si se arroja una IOException cuando se llama a write(byte[]), el subproceso envía un aviso a la actividad principal y le explica al usuario que el dispositivo no pudo enviar los bytes dados al otro dispositivo (conectado).

El método cancel() del subproceso permite finalizar la conexión en cualquier momento cuando cierras BluetoothSocket. Siempre debes llamar a este método cuando dejes de usar la conexión Bluetooth.

Para ver una demostración de cómo usar las APIs de Bluetooth, consulta la app de ejemplo de Chat de Bluetooth.

Clases fundamentales e interfaces

Todas las APIs de Bluetooth están disponibles en el paquete android.bluetooth. A continuación, se muestra un resumen de las clases e interfaces que necesitas para crear conexiones Bluetooth:

BluetoothAdapter
Representa el adaptador local de Bluetooth (radio Bluetooth). El BluetoothAdapter es el punto de entrada de toda la interacción de Bluetooth. Con esto, puedes descubrir otros dispositivos Bluetooth, consultar una lista de dispositivos vinculados (vinculados), crear una instancia de BluetoothDevice con una dirección MAC conocida y crear un BluetoothServerSocket para escuchar las comunicaciones de otros dispositivos.
BluetoothDevice
Representa un dispositivo Bluetooth remoto. Úsalo para solicitar una conexión con un dispositivo remoto a través de un BluetoothSocket o consultar información sobre el dispositivo, como su nombre, dirección, clase y estado de vinculación.
BluetoothSocket
Representa la interfaz de un socket Bluetooth (similar a un Socket de TCP). Este es el punto de conexión que permite que una aplicación intercambie datos con otro dispositivo Bluetooth mediante InputStream y OutputStream.
BluetoothServerSocket
Representa un socket de servidor abierto que escucha las solicitudes entrantes (similar a un ServerSocket de TCP). Para conectar dos dispositivos Android, un dispositivo debe abrir un socket de servidor con esta clase. Cuando un dispositivo Bluetooth remoto envía una solicitud de conexión a este dispositivo, este la acepta y muestra un BluetoothSocket conectado.
BluetoothClass
Describe las características y capacidades generales de un dispositivo Bluetooth. Se trata de un conjunto de propiedades de solo lectura que define las clases y los servicios del dispositivo. Si bien esta información brinda una sugerencia útil sobre el tipo de dispositivo, los atributos de esta clase no necesariamente describen todos los perfiles y servicios de Bluetooth que admite el dispositivo.
BluetoothProfile
Interfaz que representa un perfil de Bluetooth. Un perfil de Bluetooth es una especificación de interfaz inalámbrica para la comunicación entre dispositivos basada en Bluetooth. Un ejemplo es el perfil de manos libres. Para obtener más información sobre los perfiles, consulta Trabaja con perfiles.
BluetoothHeadset
Proporciona compatibilidad para usar auriculares Bluetooth con teléfonos móviles. Esto incluye el perfil de auriculares Bluetooth y el perfil de manos libres (v1.5).
BluetoothA2dp
Define cómo se puede transmitir audio de alta calidad de un dispositivo a otro mediante una conexión Bluetooth mediante el perfil de distribución de audio avanzada (A2DP).
BluetoothHealth
: Representa un proxy de perfil de dispositivos de salud que controla el servicio Bluetooth.
BluetoothHealthCallback
Es una clase abstracta que se usa para implementar devoluciones de llamada de BluetoothHealth. Debes extender esta clase e implementar los métodos de devolución de llamada para recibir actualizaciones sobre los cambios en el estado de registro de la aplicación y el estado del canal de Bluetooth.
BluetoothHealthAppConfiguration
Representa una configuración de la aplicación que la aplicación de terceros de salud de Bluetooth registra para comunicarse con un dispositivo de salud Bluetooth remoto.
BluetoothProfile.ServiceListener
Una interfaz que notifica a los clientes de comunicación entre procesos (IPC) de BluetoothProfile cuando se conectan al servicio interno que ejecuta un perfil en particular o se desconectan de él.