Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save smokeman420/3aae7f64614d5ae47d8c5157ae64c3cc to your computer and use it in GitHub Desktop.
Save smokeman420/3aae7f64614d5ae47d8c5157ae64c3cc to your computer and use it in GitHub Desktop.
package com.example.platform.connectivity.bluetooth.ble
import android.Manifest
import android.annotation.SuppressLint
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCallback
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattService
import android.bluetooth.BluetoothProfile
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.annotation.RequiresPermission
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import com.example.platform.connectivity.bluetooth.ble.server.GATTServerSampleService.Companion.CHARACTERISTIC_UUID
import com.example.platform.connectivity.bluetooth.ble.server.GATTServerSampleService.Companion.SERVICE_UUID
import com.google.android.catalog.framework.annotations.Sample
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlin.random.Random
@OptIn(ExperimentalAnimationApi::class)
@SuppressLint("MissingPermission")
@RequiresApi(Build.VERSION_CODES.M)
@Sample(
name = "Connect to a GATT server",
description = "Shows how to connect to a GATT server hosted by the BLE device and perform simple operations",
documentation = "https://developer.android.com/guide/topics/connectivity/bluetooth/connect-gatt-server",
tags = ["bluetooth"],
)
@Composable
fun ConnectGATTSample() {
var selectedDevice by remember {
mutableStateOf<BluetoothDevice?>(null)
}
// Check that BT permissions and that BT is available and enabled
BluetoothSampleBox {
AnimatedContent(targetState = selectedDevice, label = "Selected device") { device ->
if (device == null) {
// Scans for BT devices and handles clicks (see FindDeviceSample)
FindDevicesScreen {
selectedDevice = it
}
} else {
// Once a device is selected show the UI and try to connect device
ConnectDeviceScreen(device = device) {
selectedDevice = null
}
}
}
}
}
@SuppressLint("InlinedApi")
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
@Composable
fun ConnectDeviceScreen(device: BluetoothDevice, onClose: () -> Unit) {
val scope = rememberCoroutineScope()
// Keeps track of the last connection state with the device
var state by remember(device) {
mutableStateOf<DeviceConnectionState?>(null)
}
// Once the device services are discovered find the GATTServerSample service
val service by remember(state?.services) {
mutableStateOf(state?.services?.find { it.uuid == SERVICE_UUID })
}
// If the GATTServerSample service is found, get the characteristic
val characteristic by remember(service) {
mutableStateOf(service?.getCharacteristic(CHARACTERISTIC_UUID))
}
// This effect will handle the connection and notify when the state changes
BLEConnectEffect(device = device) {
// update our state to recompose the UI
state = it
}
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(text = "Devices details", style = MaterialTheme.typography.headlineSmall)
Text(text = "Name: ${device.name} (${device.address})")
Text(text = "Status: ${state?.connectionState?.toConnectionStateString()}")
Text(text = "MTU: ${state?.mtu}")
Text(text = "Services: ${state?.services?.joinToString { it.uuid.toString() + " " + it.type }}")
Text(text = "Message sent: ${state?.messageSent}")
Text(text = "Message received: ${state?.messageReceived}")
Button(
onClick = {
scope.launch(Dispatchers.IO) {
if (state?.connectionState == BluetoothProfile.STATE_DISCONNECTED) {
state?.gatt?.connect()
}
// Example on how to request specific MTUs
// Note that from Android 14 onwards the system will define a default MTU and
// it will only be sent once to the peripheral device
state?.gatt?.requestMtu(Random.nextInt(27, 190))
}
},
) {
Text(text = "Request MTU")
}
Button(
enabled = state?.gatt != null,
onClick = {
scope.launch(Dispatchers.IO) {
// Once we have the connection discover the peripheral services
state?.gatt?.discoverServices()
}
},
) {
Text(text = "Discover")
}
Button(
enabled = state?.gatt != null && characteristic != null,
onClick = {
scope.launch(Dispatchers.IO) {
sendData(state?.gatt!!, characteristic!!)
}
},
) {
Text(text = "Write to server")
}
Button(
enabled = state?.gatt != null && characteristic != null,
onClick = {
scope.launch(Dispatchers.IO) {
state?.gatt?.readCharacteristic(characteristic)
}
},
) {
Text(text = "Read characteristic")
}
Button(onClick = onClose) {
Text(text = "Close")
}
}
}
/**
* Writes "hello world" to the server characteristic
*/
@SuppressLint("MissingPermission")
private fun sendData(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
) {
val data = "Hello world!".toByteArray()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
gatt.writeCharacteristic(
characteristic,
data,
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT,
)
} else {
characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
@Suppress("DEPRECATION")
characteristic.value = data
@Suppress("DEPRECATION")
gatt.writeCharacteristic(characteristic)
}
}
internal fun Int.toConnectionStateString() = when (this) {
BluetoothProfile.STATE_CONNECTED -> "Connected"
BluetoothProfile.STATE_CONNECTING -> "Connecting"
BluetoothProfile.STATE_DISCONNECTED -> "Disconnected"
BluetoothProfile.STATE_DISCONNECTING -> "Disconnecting"
else -> "N/A"
}
private data class DeviceConnectionState(
val gatt: BluetoothGatt?,
val connectionState: Int,
val mtu: Int,
val services: List<BluetoothGattService> = emptyList(),
val messageSent: Boolean = false,
val messageReceived: String = "",
) {
companion object {
val None = DeviceConnectionState(null, -1, -1)
}
}
@SuppressLint("InlinedApi")
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
@Composable
private fun BLEConnectEffect(
device: BluetoothDevice,
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
onStateChange: (DeviceConnectionState) -> Unit,
) {
val context = LocalContext.current
val currentOnStateChange by rememberUpdatedState(onStateChange)
// Keep the current connection state
var state by remember {
mutableStateOf(DeviceConnectionState.None)
}
DisposableEffect(lifecycleOwner, device) {
// This callback will notify us when things change in the GATT connection so we can update
// our state
val callback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(
gatt: BluetoothGatt,
status: Int,
newState: Int,
) {
super.onConnectionStateChange(gatt, status, newState)
state = state.copy(gatt = gatt, connectionState = newState)
currentOnStateChange(state)
if (status != BluetoothGatt.GATT_SUCCESS) {
// Here you should handle the error returned in status based on the constants
// https://developer.android.com/reference/android/bluetooth/BluetoothGatt#summary
// For example for GATT_INSUFFICIENT_ENCRYPTION or
// GATT_INSUFFICIENT_AUTHENTICATION you should create a bond.
// https://developer.android.com/reference/android/bluetooth/BluetoothDevice#createBond()
Log.e("BLEConnectEffect", "An error happened: $status")
}
}
override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
super.onMtuChanged(gatt, mtu, status)
state = state.copy(gatt = gatt, mtu = mtu)
currentOnStateChange(state)
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
super.onServicesDiscovered(gatt, status)
state = state.copy(services = gatt.services)
currentOnStateChange(state)
}
override fun onCharacteristicWrite(
gatt: BluetoothGatt?,
characteristic: BluetoothGattCharacteristic?,
status: Int,
) {
super.onCharacteristicWrite(gatt, characteristic, status)
state = state.copy(messageSent = status == BluetoothGatt.GATT_SUCCESS)
currentOnStateChange(state)
}
@Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
override fun onCharacteristicRead(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int,
) {
super.onCharacteristicRead(gatt, characteristic, status)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
doOnRead(characteristic.value)
}
}
override fun onCharacteristicRead(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray,
status: Int,
) {
super.onCharacteristicRead(gatt, characteristic, value, status)
doOnRead(value)
}
private fun doOnRead(value: ByteArray) {
state = state.copy(messageReceived = value.decodeToString())
currentOnStateChange(state)
}
}
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_START) {
if (state.gatt != null) {
// If we previously had a GATT connection let's reestablish it
state.gatt?.connect()
} else {
// Otherwise create a new GATT connection
state = state.copy(gatt = device.connectGatt(context, false, callback))
}
} else if (event == Lifecycle.Event.ON_STOP) {
// Unless you have a reason to keep connected while in the bg you should disconnect
state.gatt?.disconnect()
}
}
// Add the observer to the lifecycle
lifecycleOwner.lifecycle.addObserver(observer)
// When the effect leaves the Composition, remove the observer and close the connection
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
state.gatt?.close()
state = DeviceConnectionState.None
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment