Sitemap
Stackademic

Stackademic is a learning hub for programmers, devs, coders, and engineers. Our goal is to democratize free coding education for the world.

Establishing Connection with Bluetooth Classic in Android

5 min readSep 14, 2024

--

Press enter or click to view image in full size
Photo by Yulia Matvienko on Unsplash

Interacting with mobile hardware is one of the most exciting aspects of Android development, particularly when it comes to Bluetooth technology. Bluetooth Classic, a widely used protocol for wireless communication, allows us to establish connections between devices, such as smartphones, speakers, and other peripherals. In this blog, we will focus on how to establish and disconnect a Bluetooth Classic connection in Android, discussing key components like UUIDs and the Advanced Audio Distribution Profile (A2DP), which is critical for streaming audio over Bluetooth.

While many existing tutorials cover scanning and listing available Bluetooth devices, this blog will emphasize how to build a reliable Bluetooth connection, leveraging A2DP for handling audio transmission. If you’re already familiar with scanning devices, you can refer to this excellent tutorial by Philipp Lackner on YouTube for additional background:

Now, let’s dive into the technical details of Bluetooth Classic and learn how to create a seamless connection for your Android applications!

Interacting with Bluetooth Classic and its Complexities

When working with Bluetooth in Android, developers typically encounter two types of devices: Bluetooth Classic and Bluetooth Low Energy (BLE). In this post, we’ll focus on Bluetooth Classic, which is suitable for tasks that require sustained, high-bandwidth connections, like audio streaming. You may already know how to scan and list available devices, but our goal here is to manage device connections, particularly with A2DP, ensuring that devices like Bluetooth speakers can receive audio from your Android app.

Most developers try to establish a connection using a method like the one below:

fun connectWithDevice(device: BluetoothDevice){
try {
val MY_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")
val socket = device.createRfcommSocketToServiceRecord(MY_UUID)
socket.connect()

if (socket.isConnected) {
val inputStream = socket.inputStream
val outputStream = socket.outputStream
Log.d("BluetoothController", "connected")
}
} catch (e: SecurityException) {
Log.d("BluetoothController", "SecurityException error")
} catch (e: IOException) {
Log.d("BluetoothController", "IOException error")
} catch (e: Exception) {
Log.d("BluetoothController", "error")
}
}

In the above example, we use the UUID to identify the connection request. Specifically, the UUID we’re using here is for an SSP (Serial Port Profile) connection, which works great for communicating with Bluetooth serial boards. While this method may successfully log a connection event, it often falls short when trying to perform more specific tasks, such as playing audio on a Bluetooth speaker.

Bluetooth Profiles: Why A2DP Matters

Bluetooth communication is managed by profiles, which define how two devices communicate with each other. One of the most essential profiles for audio streaming is A2DP (Advanced Audio Distribution Profile). A2DP allows high-quality audio to be transmitted from one device (such as your Android phone) to another (such as Bluetooth speakers or headphones).

Get G. Raj Kumar’s stories in your inbox

Join Medium for free to get updates from this writer.

To make audio streaming work, you must ensure that your connection is established using A2DP, not just SSP. This is where many developers encounter issues — connecting via SSP doesn’t enable audio transmission.

BluetoothController.kt Interface

We’ll start by creating an interface that outlines the functions required for managing Bluetooth devices. Our focus is on establishing and terminating connections, while additional functionality like scanning is kept out of scope for this particular example.

interface BluetoothController {
val scannedDevices: StateFlow<List<BluetoothDeviceApp>>
val pairedDevice: StateFlow<List<BluetoothDeviceApp>>
val bluetoothState: StateFlow<BluetoothState>

fun resetBluetoothState()
fun startDiscovery()
fun stopDiscovery()
suspend fun connectToDevice(device: BluetoothDeviceApp)
suspend fun disconnectFromDevice(device: BluetoothDeviceApp)
}

Implementing Bluetooth Connection Logic

Below is the implementation of the Bluetooth connection logic. Note that we’ve added proper permission checks and profile-specific connection handling, such as managing A2DP.

@SuppressLint("MissingPermission")
class AndroidBluetoothController(
private val context: Context
) : BluetoothController {
private val bluetoothManager by lazy {
context.getSystemService(BluetoothManager::class.java)
}
private val bluetoothAdapter by lazy {
bluetoothManager.adapter
}

private var a2dp: BluetoothA2dp? = null

override suspend fun connectToDevice(device: BluetoothDevice) {
if (!hasPermissions(Manifest.permission.BLUETOOTH_CONNECT)) {
return // Ensure permission is granted before proceeding
}

try {
stopDiscovery() // Stop discovery before initiating a connection

// Handle device bonding
if (device.bondState == BluetoothDevice.BOND_NONE) {
// if not already bonded, then need to create bond
device.createBond().let { isBondAccepted ->
if (isBondAccepted) {
connectA2dpProfile(device)
}
}
} else {
// if device is already bonded
connectA2dpProfile(device)
}
} catch (e: Exception) {
Log.e("BluetoothController", "Error during connection: ${e.message}")
}
}

override suspend fun disconnectFromDevice(bluetoothDevice: BluetoothDevice) {
try {
// disconnect using A2dp profile, and remove the bond
// Not all phones support this type of disconnection
// In my case, redmi did while motorola didn't
disconnectA2dpProfile {
// Thus to support all device we use this method
// calling hidden method via reflection to force disconnection
bluetoothDevice::class.java.getMethod("removeBond").invoke(bluetoothDevice)

a2dp = null // reset the variable
}
} catch (e: Exception) {
Log.e("BluetoothController", "Disconnection error: ${e.message}")
}
}

private fun connectA2dpProfile(device: BluetoothDevice) {
bluetoothAdapter.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile?) {
if (profile == BluetoothProfile.A2DP) {
a2dp = proxy as BluetoothA2dp
try {
a2dp!!.javaClass.getMethod("connect", BluetoothDevice::class.java).invoke(a2dp, device)
Log.d("BluetoothController", "A2DP connected")
} catch (e: Exception) {
e.printStackTrace()
}
}
}

override fun onServiceDisconnected(profile: Int) {
if (profile == BluetoothProfile.A2DP) {
try {
a2dp?.javaClass?.getMethod("disconnect", BluetoothDevice::class.java)?.invoke(a2dp, device)
a2dp = null
Log.d("BluetoothController", "A2DP disconnected")
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}, BluetoothProfile.A2DP)
}

private fun disconnectA2dpProfile(callback: () -> Unit) {
try {
bluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP, a2dp)
callback()
} catch (e: Exception) {
e.printStackTrace()
}
}

private fun hasPermissions(permission: String): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED
} else {
true
}
}
}

Additionally, To monitor connection and disconnection events, use a BroadcastReceiver that listens for specific Bluetooth actions. The receiver will update the UI or take appropriate actions depending on the state of the Bluetooth connection.

private val btConnectionReceiver = BtConnectionReceiver { isConnected ->
if (isConnected) {
// Logic when connected
} else {
// Logic when disconnected
}
}

init {
registerBtConnectionReceiver()
}

private fun registerBtConnectionReceiver() {
if (!hasPermissions(Manifest.permission.BLUETOOTH_SCAN)) {
return
}

val intentFilter = IntentFilter().apply {
addAction(BluetoothDevice.ACTION_ACL_CONNECTED)
addAction(BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED)
addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
}

context.registerReceiver(btConnectionReceiver, intentFilter)
}

This is your setup for Broadcast Receiver, where we send the result to the AndroidBluetoothController.kt via a callback method with boolean type:

class BtConnectionReceiver(
private val connectionStatus: (Boolean) -> Unit) : BroadcastReceiver()
{
@SuppressLint("MissingPermission")
override fun onReceive(context: Context?, intent: Intent?) {
when(action){
BluetoothDevice.ACTION_ACL_CONNECTED -> {
connectionStatus(true)
}

BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED -> {
Log.d("BtConnectionReceiver","Disconnecting...")
}

BluetoothDevice.ACTION_ACL_DISCONNECTED -> {
connectionStatus(false)
}
}
}
}

By using this broadcast receiver, your app will stay updated on connection status changes, helping you provide a smoother Bluetooth experience.

Conclusion

Establishing a Bluetooth Classic connection in Android, especially for handling audio streaming via A2DP, can be challenging due to the specific profiles involved. By following the outlined approach, you’ll be able to set up reliable connections and manage disconnections effectively. For additional resources, be sure to check out the linked YouTube tutorial and feel free to reach out if you need further assistance. Happy coding!

Stackademic 🎓

Thank you for reading until the end. Before you go:

--

--

Stackademic
Stackademic

Published in Stackademic

Stackademic is a learning hub for programmers, devs, coders, and engineers. Our goal is to democratize free coding education for the world.