Bluetooth

The Android platform includes support for the Bluetooth network stack. The Bluetooth network stack allows a device to wirelessly exchange data with other Bluetooth devices. The application framework provides access to Bluetooth functionality through the Android Bluetooth APIs.

Android Things extends these APIs to enable apps to control the Bluetooth system settings, device pairing, and connection process.

Adding the required permissions

In addition to the BLUETOOTH and BLUETOOTH_ADMIN permissions, add the following to your app's manifest file to use the Bluetooth connection and pairing APIs:

<uses-permission android:name="com.google.android.things.permission.MANAGE_BLUETOOTH" />

Configuring device attributes

The Android Things Bluetooth APIs enable you to control the device class and supported profiles exposed by the local Bluetooth adapter. To configure the Bluetooth Class of Device (CoD), create a BluetoothClass instance via the BluetoothClassFactory and set it using the BluetoothConfigManager:

Kotlin

import android.bluetooth.BluetoothClass
import com.google.android.things.bluetooth.BluetoothClassFactory
import com.google.android.things.bluetooth.BluetoothConfigManager
...

val manager = BluetoothConfigManager.getInstance()
// Report the local Bluetooth device class as a speaker
manager.bluetoothClass = BluetoothClassFactory.build(
        BluetoothClass.Service.AUDIO,
        BluetoothClass.Device.AUDIO_VIDEO_LOUDSPEAKER
)

Java

import android.bluetooth.BluetoothClass;
import com.google.android.things.bluetooth.BluetoothClassFactory;
import com.google.android.things.bluetooth.BluetoothConfigManager;
...

BluetoothConfigManager manager = BluetoothConfigManager.getInstance();
// Report the local Bluetooth device class as a speaker
BluetoothClass deviceClass = BluetoothClassFactory.build(
        BluetoothClass.Service.AUDIO,
        BluetoothClass.Device.AUDIO_VIDEO_LOUDSPEAKER);

manager.setBluetoothClass(deviceClass);

I/O capabilities

Use BluetoothConfigManager to report the Input/Output capabilities of your device to the Bluetooth service. Set the I/O capabilities for Bluetooth with setIoCapability() and Bluetooth Low Energy (LE) with setLeIoCapability() These methods accept one of the following values:

  • IO_CAPABILITY_NONE: Device has no input or output capabilities. This is the default value.
  • IO_CAPABILITY_OUT: Device has a display only.
  • IO_CAPABILITY_IN: Device can accept keyboard user input only.
  • IO_CAPABILITY_IO: Device has a display and can accept basic (yes/no) input.
  • IO_CAPABILITY_KBDISP: Device has a display and can accept keyboard user input.

The I/O capabilities of the device are shared with remote devices during the Pairing Feature Exchange phase of device pairing. The Bluetooth service determines which pairing variants your device can support based on its I/O capabilities.

Kotlin

import com.google.android.things.bluetooth.BluetoothConfigManager
...
val manager = BluetoothConfigManager.getInstance()
// Report full input/output capability for this device
manager.ioCapability = BluetoothConfigManager.IO_CAPABILITY_IO

Java

import com.google.android.things.bluetooth.BluetoothConfigManager;
...

BluetoothConfigManager manager = BluetoothConfigManager.getInstance();
// Report full input/output capability for this device
manager.setIoCapability(BluetoothConfigManager.IO_CAPABILITY_IO);

Enabled profiles

To configure the enabled Bluetooth profiles on the local device, use the BluetoothProfileManager. Each BluetoothProfile must be enabled before a remote device can connect to it. Query the current set of enabled profiles with getEnabledProfiles() and update them using one of the following methods: enableProfiles() or disableProfiles():

Kotlin

import com.google.android.things.bluetooth.BluetoothProfileManager;
import com.google.android.things.bluetooth.BluetoothProfile;
...

val manager = BluetoothProfileManager.getInstance()
val enabledProfiles = manager.enabledProfiles
if (!enabledProfiles.contains(BluetoothProfile.A2DP_SINK)) {
    Log.d(TAG, "Enabling A2DP sink mode.")
    val toDisable = listOf(BluetoothProfile.A2DP)
    val toEnable = listOf(BluetoothProfile.A2DP_SINK, BluetoothProfile.AVRCP_CONTROLLER)

    manager.enableAndDisableProfiles(toEnable, toDisable)
}

Java

import com.google.android.things.bluetooth.BluetoothProfileManager;
import com.google.android.things.bluetooth.BluetoothProfile;
...

BluetoothProfileManager manager = BluetoothProfileManager.getInstance();
List<Integer> enabledProfiles = manager.getEnabledProfiles();
if (!enabledProfiles.contains(BluetoothProfile.A2DP_SINK)) {
    Log.d(TAG, "Enabling A2DP sink mode.");
    List<Integer> toDisable = Arrays.asList(BluetoothProfile.A2DP);
    List<Integer> toEnable = Arrays.asList(
        BluetoothProfile.A2DP_SINK,
        BluetoothProfile.AVRCP_CONTROLLER);

    manager.enableAndDisableProfiles(toEnable, toDisable);
}

Pairing with a remote device

See Finding Devices in the Android Bluetooth Guide for more details on discovering remote devices and determining if they are already bonded with the local device. If the remote device is already bonded, you can jump directly to connecting to a remote device.

To begin the pairing process with a remote device:

  1. Register a BluetoothPairingCallback with the BluetoothConnectionManager
  2. Call initiatePairing() with the discovered peer device. For peer devices which report their Input/Output Capability as IO_CAPABILITY_NONE, you will receive a callback in onPaired(), once pairing is complete.
  3. If the peer device requires user input to pair, handle the pairing request in onPairingInitiated().

Kotlin

import android.bluetooth.BluetoothDevice
import com.google.android.things.bluetooth.BluetoothConnectionManager
import com.google.android.things.bluetooth.BluetoothPairingCallback
import com.google.android.things.bluetooth.PairingParams
...

class PairingActivity : Activity() {

    private lateinit var mBluetoothConnectionManager: BluetoothConnectionManager

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mBluetoothConnectionManager = BluetoothConnectionManager.getInstance().apply {
            registerPairingCallback(mBluetoothPairingCallback)
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        mBluetoothConnectionManager.unregisterPairingCallback(mBluetoothPairingCallback)
    }

    private fun startPairing(remoteDevice: BluetoothDevice) {
        mBluetoothConnectionManager.initiatePairing(remoteDevice)
    }

    private val mBluetoothPairingCallback = object : BluetoothPairingCallback {

        override fun onPairingInitiated(
                bluetoothDevice: BluetoothDevice,
                pairingParams: PairingParams
        ) {
            // Handle incoming pairing request or confirmation of outgoing pairing request
            handlePairingRequest(bluetoothDevice, pairingParams)
        }

        override fun onPaired(bluetoothDevice: BluetoothDevice) {
            // Device pairing complete
        }

        override fun onUnpaired(bluetoothDevice: BluetoothDevice) {
            // Device unpaired
        }

        override fun onPairingError(
                bluetoothDevice: BluetoothDevice,
                pairingError: BluetoothPairingCallback.PairingError
        ) {
            // Something went wrong!
        }
    }
}

Java

import android.bluetooth.BluetoothDevice;
import com.google.android.things.bluetooth.BluetoothConnectionManager;
import com.google.android.things.bluetooth.BluetoothPairingCallback;
import com.google.android.things.bluetooth.PairingParams;
...

public class PairingActivity extends Activity {

    BluetoothConnectionManager mBluetoothConnectionManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBluetoothConnectionManager = BluetoothConnectionManager.getInstance();
        mBluetoothConnectionManager.registerPairingCallback(mBluetoothPairingCallback);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mBluetoothConnectionManager.unregisterPairingCallback(mBluetoothPairingCallback);
    }

    private void startPairing(BluetoothDevice remoteDevice) {
        mBluetoothConnectionManager.initiatePairing(remoteDevice);
    }

    private BluetoothPairingCallback mBluetoothPairingCallback = new BluetoothPairingCallback() {

        @Override
        public void onPairingInitiated(BluetoothDevice bluetoothDevice,
                PairingParams pairingParams) {
            // Handle incoming pairing request or confirmation of outgoing pairing request
            handlePairingRequest(bluetoothDevice, pairingParams);
        }

        @Override
        public void onPaired(BluetoothDevice bluetoothDevice) {
            // Device pairing complete
        }

        @Override
        public void onUnpaired(BluetoothDevice bluetoothDevice) {
            // Device unpaired
        }

        @Override
        public void onPairingError(BluetoothDevice bluetoothDevice,
                BluetoothPairingCallback.PairingError pairingError) {
            // Something went wrong!
        }
    };
}

The PairingParams provided to the callback defines the Bluetooth pairing variant required by the remote device. These represent different pairing security schemes, and your app is responsible for taking the appropriate action as documented for each variant. For pairing variants that require user input, complete the process by calling finishPairing().

Kotlin

private fun handlePairingRequest(bluetoothDevice: BluetoothDevice, pairingParams: PairingParams) {
    when (pairingParams.pairingType) {
        PairingParams.PAIRING_VARIANT_DISPLAY_PIN,
        PairingParams.PAIRING_VARIANT_DISPLAY_PASSKEY -> {
            // Display the required PIN to the user
            Log.d(TAG, "Display Passkey - ${pairingParams.pairingPin}")
        }
        PairingParams.PAIRING_VARIANT_PIN, PairingParams.PAIRING_VARIANT_PIN_16_DIGITS -> {
            // Obtain PIN from the user
            val pin = ...
            // Pass the result to complete pairing
            mBluetoothConnectionManager.finishPairing(bluetoothDevice, pin)
        }
        PairingParams.PAIRING_VARIANT_CONSENT,
        PairingParams.PAIRING_VARIANT_PASSKEY_CONFIRMATION -> {
            // Show confirmation of pairing to the user
            ...
            // Complete the pairing process
            mBluetoothConnectionManager.finishPairing(bluetoothDevice)
        }
    }
}

Java

private void handlePairingRequest(BluetoothDevice bluetoothDevice, PairingParams pairingParams) {
    switch (pairingParams.getPairingType()) {
        case PairingParams.PAIRING_VARIANT_DISPLAY_PIN:
        case PairingParams.PAIRING_VARIANT_DISPLAY_PASSKEY:
            // Display the required PIN to the user
            Log.d(TAG, "Display Passkey - " + pairingParams.getPairingPin());
            break;
        case PairingParams.PAIRING_VARIANT_PIN:
        case PairingParams.PAIRING_VARIANT_PIN_16_DIGITS:
            // Obtain PIN from the user
            String pin = ...;
            // Pass the result to complete pairing
            mBluetoothConnectionManager.finishPairing(bluetoothDevice, pin);
            break;
        case PairingParams.PAIRING_VARIANT_CONSENT:
        case PairingParams.PAIRING_VARIANT_PASSKEY_CONFIRMATION:
            // Show confirmation of pairing to the user
            ...
            // Complete the pairing process
            mBluetoothConnectionManager.finishPairing(bluetoothDevice);
            break;
    }
}

Connecting to a remote device

Once you have successfully paired over Bluetooth, your app can connect to profiles and services on the remote device. The Android Bluetooth API exposes connection features for a restricted set of device profiles. See Connecting Devices in the Android Bluetooth Guide and the Bluetooth Low Energy Guide for more details on connecting to devices using the RFCOMM and GATT profiles.

The Android Things BluetoothConnectionManager enables apps to connect to additional profiles and services on remote devices. Use the getConnectableProfiles() method to report the available profiles on the remote device.

To connect with a specific profile on a given BluetoothDevice:

  1. Register a BluetoothConnectionCallback with the BluetoothConnectionManager
  2. Initiate the connection with the connect() method, providing the device and the target BluetoothProfile
  3. Handle the connection request in onConnectionRequested()

Kotlin

import android.bluetooth.BluetoothDevice
import com.google.android.things.bluetooth.BluetoothConnectionManager
import com.google.android.things.bluetooth.BluetoothConnectionCallback
import com.google.android.things.bluetooth.BluetoothProfile
import com.google.android.things.bluetooth.ConnectionParams
...

class ConnectActivity : Activity() {

    private lateinit var mBluetoothConnectionManager: BluetoothConnectionManager

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mBluetoothConnectionManager = BluetoothConnectionManager.getInstance().apply {
            registerConnectionCallback(mBluetoothConnectionCallback)
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        mBluetoothConnectionManager.unregisterConnectionCallback(mBluetoothConnectionCallback)
    }

    private fun connectToA2dp(bluetoothDevice: BluetoothDevice) {
        mBluetoothConnectionManager.connect(bluetoothDevice, BluetoothProfile.A2DP_SINK)
    }

    // Set up callbacks for the profile connection process.
    private val mBluetoothConnectionCallback = object : BluetoothConnectionCallback {
        override fun onConnectionRequested(
                bluetoothDevice: BluetoothDevice,
                connectionParams: ConnectionParams
        ) {
            // Handle incoming connection request
            handleConnectionRequest(bluetoothDevice, connectionParams)
        }

        override fun onConnectionRequestCancelled(
                bluetoothDevice: BluetoothDevice,
                requestType: Int
        ) {
            // Request cancelled
        }

        override fun onConnected(bluetoothDevice: BluetoothDevice, profile: Int) {
            // Connection completed successfully
        }

        override fun onDisconnected(bluetoothDevice: BluetoothDevice, profile: Int) {
            // Remote device disconnected
        }
    }
}

Java

import android.bluetooth.BluetoothDevice;
import com.google.android.things.bluetooth.BluetoothConnectionManager;
import com.google.android.things.bluetooth.BluetoothConnectionCallback;
import com.google.android.things.bluetooth.BluetoothProfile;
import com.google.android.things.bluetooth.ConnectionParams;
...

public class ConnectActivity extends Activity {

    BluetoothConnectionManager mBluetoothConnectionManager;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBluetoothConnectionManager = BluetoothConnectionManager.getInstance();
        mBluetoothConnectionManager.registerConnectionCallback(mBluetoothConnectionCallback);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mBluetoothConnectionManager.unregisterConnectionCallback(mBluetoothConnectionCallback);
    }

    private void connectToA2dp(BluetoothDevice bluetoothDevice) {
        mBluetoothConnectionManager.connect(bluetoothDevice, BluetoothProfile.A2DP_SINK);
    }

    // Set up callbacks for the profile connection process.
    private final BluetoothConnectionCallback mBluetoothConnectionCallback = new BluetoothConnectionCallback() {
        @Override
        public void onConnectionRequested(BluetoothDevice bluetoothDevice, ConnectionParams connectionParams) {
            // Handle incoming connection request
            handleConnectionRequest(bluetoothDevice, connectionParams);
        }

        @Override
        public void onConnectionRequestCancelled(BluetoothDevice bluetoothDevice, int requestType) {
            // Request cancelled
        }

        @Override
        public void onConnected(BluetoothDevice bluetoothDevice, int profile) {
            // Connection completed successfully
        }

        @Override
        public void onDisconnected(BluetoothDevice bluetoothDevice, int profile) {
            // Remote device disconnected
        }
    };
}

The ConnectionParams provided to the callback include additional details about the type of connection requested. Inspect those parameters in your code, and then determine whether to accept or reject the request with confirmOrDenyConnection():

Kotlin

private fun handleConnectionRequest(
        bluetoothDevice: BluetoothDevice,
        connectionParams: ConnectionParams
) {
    // Determine whether to accept the connection request
    val accept = connectionParams.requestType == ConnectionParams.REQUEST_TYPE_PROFILE_CONNECTION

    // Pass that result on to the BluetoothConnectionManager
    mBluetoothConnectionManager.confirmOrDenyConnection(bluetoothDevice, connectionParams, accept)
}

Java

private void handleConnectionRequest(BluetoothDevice bluetoothDevice, ConnectionParams connectionParams) {
    // Determine whether to accept the connection request
    boolean accept = false;
    if (connectionParams.getRequestType() == ConnectionParams.REQUEST_TYPE_PROFILE_CONNECTION) {
        accept = true;
    }

    // Pass that result on to the BluetoothConnectionManager
    mBluetoothConnectionManager.confirmOrDenyConnection(bluetoothDevice, connectionParams, accept);
}