i am trying to set notification callback in BLE nordic, where i am using the Android BLE library (Nordic Github). But i not ablet to get the notification event when i am changing the value of the characteristics.
`
class BleManagerHP1T(context: Context) : BleManager(context) {
override fun getGattCallback(): BleManagerGattCallback = GattCallback()
override fun log(priority: Int, message: String) {
if (BuildConfig.DEBUG || priority == Log.ERROR) {
Log.println(priority, GattService.TAG, message)
}
}
private inner class GattCallback : BleManagerGattCallback() {
private var myCharacteristic: BluetoothGattCharacteristic? = null
private var rxCharacteristic: BluetoothGattCharacteristic? = null
#SuppressLint("MissingPermission")
override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean {
val service = gatt.getService(GattService.MyServiceProfile.MY_SERVICE_UUID)
myCharacteristic =
service?.getCharacteristic(GattService.MyServiceProfile.MY_CHARACTERISTIC_UUID)
val myCharacteristicProperties = myCharacteristic?.properties ?: 0
Log.d(TAG, "isRequiredServiceSupported: notify ${(myCharacteristicProperties and BluetoothGattCharacteristic.PROPERTY_NOTIFY != 0)}")
rxCharacteristic = service?.getCharacteristic(GattService.MyServiceProfile.RX_CHARACTERISTIC_UUID)
val obj = JSONObject()
obj.put("OPCODE","PROVISION")
rxCharacteristic?.value = obj.toString().encodeToByteArray()
val rxRead = gatt.writeCharacteristic(rxCharacteristic)
Log.d(TAG, "isRequiredServiceSupported: Read $rxRead")
return (myCharacteristicProperties and BluetoothGattCharacteristic.PROPERTY_NOTIFY != 0)
}
override fun initialize() {
enableNotifications(myCharacteristic).enqueue()
requestMtu(260).enqueue();
setNotificationCallback(myCharacteristic).with { _, data ->
Log.d(TAG, "initialize: TX char Notification Called")
if (data.value != null) {
val value = String(data.value!!, Charsets.UTF_8)
Log.d(TAG, "initialize: TX char value $value")
}
}
requestMtu(260).enqueue();
enableNotifications(rxCharacteristic).enqueue()
setNotificationCallback(rxCharacteristic).with { _, data ->
Log.d(TAG, "initialize: RX char Notification Called")
if (data.value != null) {
val value = String(data.value!!, Charsets.UTF_8)
Log.d(TAG, "initialize: RX char value $value")
}
}
beginAtomicRequestQueue()
.add(enableNotifications(myCharacteristic)
.fail { _: BluetoothDevice?, status: Int ->
log(Log.ERROR, "Could not subscribe: $status")
disconnect().enqueue()
}
)
.done {
log(Log.INFO, "Target initialized")
}
.enqueue()
}
override fun onServicesInvalidated() {
myCharacteristic = null
}
}
override fun readCharacteristic(characteristic: BluetoothGattCharacteristic?): ReadRequest {
return Request.newReadRequest(characteristic)
}
}
`
gatt connection is establishing perfectly fine, using this code.
`
val bleManager = BleManagerHP1T(this#ControllerActivity)
synchronized (this) {
bleManager.connect(deviceMainList[position]).useAutoConnect(false).enqueue()
}
`
here is the gatt service file .
`
class GattService : Service() {
private val defaultScope = CoroutineScope(Dispatchers.Default)
private lateinit var bluetoothObserver: BroadcastReceiver
private var myCharacteristicChangedChannel: SendChannel<String>? = null
private val clientManagers = mutableMapOf<String, ClientManager>()
// val connect = BleManager()
#RequiresApi(Build.VERSION_CODES.O)
override fun onCreate() {
super.onCreate()
// Setup as a foreground service
val notificationChannel = NotificationChannel(
GattService::class.java.simpleName,
resources.getString(R.string.gatt_service_name),
NotificationManager.IMPORTANCE_DEFAULT
)
val notificationService =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationService.createNotificationChannel(notificationChannel)
val notification = NotificationCompat.Builder(this, GattService::class.java.simpleName)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(resources.getString(R.string.gatt_service_name))
.setContentText(resources.getString(R.string.gatt_service_running_notification))
.setAutoCancel(true)
startForeground(1, notification.build())
// Observe OS state changes in BLE
bluetoothObserver = object : BroadcastReceiver() {
#SuppressLint("MissingPermission")
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
BluetoothAdapter.ACTION_STATE_CHANGED -> {
val bluetoothState = intent.getIntExtra(
BluetoothAdapter.EXTRA_STATE,
-1
)
when (bluetoothState) {
BluetoothAdapter.STATE_ON -> enableBleServices()
BluetoothAdapter.STATE_OFF -> disableBleServices()
}
}
BluetoothDevice.ACTION_BOND_STATE_CHANGED -> {
val device =
intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
Log.d(TAG, "Bond state changed for device ${device?.address}: ${device?.bondState}")
when (device?.bondState) {
BluetoothDevice.BOND_BONDED -> addDevice(device)
BluetoothDevice.BOND_NONE -> removeDevice(device)
}
}
}
}
}
registerReceiver(bluetoothObserver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED))
registerReceiver(bluetoothObserver, IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED))
// Startup BLE if we have it
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
if (bluetoothManager.adapter?.isEnabled == true) enableBleServices()
}
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(bluetoothObserver)
disableBleServices()
}
override fun onBind(intent: Intent?): IBinder? =
when (intent?.action) {
DATA_PLANE_ACTION -> {
DataPlane()
}
else -> null
}
override fun onUnbind(intent: Intent?): Boolean =
when (intent?.action) {
DATA_PLANE_ACTION -> {
myCharacteristicChangedChannel = null
true
}
else -> false
}
/**
* A binding to be used to interact with data of the service
*/
inner class DataPlane : Binder() {
fun setMyCharacteristicChangedChannel(sendChannel: SendChannel<String>) {
myCharacteristicChangedChannel = sendChannel
}
}
#SuppressLint("MissingPermission")
private fun enableBleServices() {
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
if (bluetoothManager.adapter?.isEnabled == true) {
Log.i(TAG, "Enabling BLE services")
bluetoothManager.adapter.bondedDevices.forEach { device -> addDevice(device) }
} else {
Log.w(TAG, "Cannot enable BLE services as either there is no Bluetooth adapter or it is disabled")
}
}
private fun disableBleServices() {
clientManagers.values.forEach { clientManager ->
clientManager.close()
}
clientManagers.clear()
}
private fun addDevice(device: BluetoothDevice) {
if (!clientManagers.containsKey(device.address)) {
val clientManager = ClientManager()
clientManager.connect(device).useAutoConnect(true).enqueue()
clientManagers[device.address] = clientManager
}
}
private fun removeDevice(device: BluetoothDevice) {
clientManagers.remove(device.address)?.close()
}
/*
* Manages the entire GATT service, declaring the services and characteristics on offer
*/
companion object {
/**
* A binding action to return a binding that can be used in relation to the service's data
*/
const val DATA_PLANE_ACTION = "data-plane"
const val TAG = "gatt-service"
}
private inner class ClientManager : BleManager(this#GattService) {
override fun getGattCallback(): BleManagerGattCallback = GattCallback()
override fun log(priority: Int, message: String) {
if (BuildConfig.DEBUG || priority == Log.ERROR) {
Log.println(priority, TAG, message)
Log.d(TAG, "log: $message")
}
}
private inner class GattCallback : BleManagerGattCallback() {
private var myCharacteristic: BluetoothGattCharacteristic? = null
override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean {
val service = gatt.getService(MyServiceProfile.MY_SERVICE_UUID)
myCharacteristic =
service?.getCharacteristic(MyServiceProfile.MY_CHARACTERISTIC_UUID)
val myCharacteristicProperties = myCharacteristic?.properties ?: 0
return (myCharacteristicProperties and BluetoothGattCharacteristic.PROPERTY_READ != 0) &&
(myCharacteristicProperties and BluetoothGattCharacteristic.PROPERTY_NOTIFY != 0)
}
override fun initialize() {
setNotificationCallback(myCharacteristic).with { _, data ->
if (data.value != null) {
val value = String(data.value!!, Charsets.UTF_8)
defaultScope.launch {
myCharacteristicChangedChannel?.send(value)
}
}
}
beginAtomicRequestQueue()
.add(enableNotifications(myCharacteristic)
.fail { _: BluetoothDevice?, status: Int ->
log(Log.ERROR, "Could not subscribe: $status")
disconnect().enqueue()
}
)
.done {
log(Log.INFO, "Target initialized")
}
.enqueue()
}
override fun onServicesInvalidated() {
myCharacteristic = null
}
}
}
object MyServiceProfile {
val MY_SERVICE_UUID: UUID = UUID.fromString("8d67d51a-801b-43cb-aea2-bbec9d1211fd")
val MY_CHARACTERISTIC_UUID: UUID = UUID.fromString("8d67d51c-801b-43cb-aea2-bbec9d1211fd")
val RX_CHARACTERISTIC_UUID: UUID = UUID.fromString("8d67d51b-801b-43cb-aea2-bbec9d1211fd")
}
}
`
Related
I have a class BatteryInfo which is a life-cycle observer and in this class, there is a BroadCastReceiver which is responsible for getting all battery information. This class is working perfectly with the lifecycle of Activity from where I have called it. This means it is registering the broadcast on activity created and unRegister on closing activity. But I am confused about how to access this broadcast live information in Activity.
class BatteryInfo(
private val _context: Context,
private val _lifecycle: Lifecycle,
): LifecycleObserver {
private var _enabled = false
init {
_lifecycle.addObserver(this)
}
private val broadcastBatteryInfoListener = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
intent?.let {
val level = it.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
val temperature = it.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, -1)
val voltage = it.getIntExtra(BatteryManager.EXTRA_VOLTAGE, -1)
val technology = it.getIntExtra(BatteryManager.EXTRA_TECHNOLOGY, -1)
val plugged = it.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1)
val health = it.getIntExtra(BatteryManager.EXTRA_HEALTH, -1)
// Log.d("TAG", String.format("%.1f", voltage / 1000f) + " V")
}
}
}
#OnLifecycleEvent(Lifecycle.Event.ON_START)
fun start() {
// if (_enabled) {
_context.registerReceiver(broadcastBatteryInfoListener,
IntentFilter(Intent.ACTION_BATTERY_CHANGED)
)
// }
}
#OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun stop() {
_context.unregisterReceiver(broadcastBatteryInfoListener)
}
#OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun destroy() {
_lifecycle.removeObserver(this)
}
// connect if not connected
fun enable() {
_enabled = true
if (_lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
start()
}
}
}
I am Calling this class from MainActivity onCreate method like this
BatteryInfo(this, lifecycle)
A simple listener would work :
data class Stats(val level : Int,
val temp : Int,
val voltage : Int,
val technology : Int,
val plugged : Int,
val health : Int)
class BatteryInfo(
private val _context: Context,
private val _lifecycle: Lifecycle
): LifecycleObserver {
private var _enabled = false
private var listener: ((Stats) -> Unit)? = null
init {
_lifecycle.addObserver(this)
}
private val broadcastBatteryInfoListener = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
intent?.let {
val level = it.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
val temperature = it.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, -1)
val voltage = it.getIntExtra(BatteryManager.EXTRA_VOLTAGE, -1)
val technology = it.getIntExtra(BatteryManager.EXTRA_TECHNOLOGY, -1)
val plugged = it.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1)
val health = it.getIntExtra(BatteryManager.EXTRA_HEALTH, -1)
listener?.invoke(Stats(level, temperature, voltage, technology, plugged, health))
// Log.d("TAG", String.format("%.1f", voltage / 1000f) + " V")
}
}
}
fun setListener(listener: ((Stats) -> Unit)?) {
this.listener = listener
}
#OnLifecycleEvent(Lifecycle.Event.ON_START)
fun start() {
// if (_enabled) {
_context.registerReceiver(broadcastBatteryInfoListener,
IntentFilter(Intent.ACTION_BATTERY_CHANGED)
)
// }
}
#OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun stop() {
_context.unregisterReceiver(broadcastBatteryInfoListener)
}
#OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun destroy() {
_lifecycle.removeObserver(this)
listener = null
}
// connect if not connected
fun enable() {
_enabled = true
if (_lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
start()
}
}
}
class SomeActivity : AppCompatActivity(){
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
BatteryInfo(this, lifecycle)
.apply { setListener { stats: Stats -> Log.d(TAG, stats.toString()) } }
}
}
I know its not you answer but its a different approach of getting things done. It also removes all of your boilerplate code.
You can use an object to extract all your data from intent and return an instance of your data class.
Read more about object in kotlin here - https://kotlinlang.org/docs/tutorials/kotlin-for-py/objects-and-companion-objects.html
object BatteryInfoUtils {
fun getBatteryInfoFromIntent(intent: Intent): Stats { intent?.let {
val level = it.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
val temperature = it.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, -1)
val voltage = it.getIntExtra(BatteryManager.EXTRA_VOLTAGE, -1)
val technology = it.getIntExtra(BatteryManager.EXTRA_TECHNOLOGY, -1)
val plugged = it.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1)
val health = it.getIntExtra(BatteryManager.EXTRA_HEALTH, -1)
return Stats(level, temperature, voltage, technology, plugged, health)
}
}
And inside MainActivity onCreate()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val stats = getBatteryInfo()
}
fun getBatteryInfo(): Stats? {
var stats: Stats? = null
val intent: Intent? = IntentFilter(Intent.ACTION_BATTERY_CHANGED).let { ifilter ->
registerReceiver(null, ifilter)
}
intent?.let { batteryIntent ->
stats = BatteryInfoUtils.getBatteryInfoFromIntent(batteryIntent)
}
return stats
}
I have done this with the help of MutableLiveData & LiveData but I am not sure it is a perfect solution or a hacky solution. I want to do a perfect solution that follows MVVM, clean-architecture, and SOLID principle without the creation of multiple objects because it is very costly.
class BatteryInfo(
private val _context: Context,
private val _lifecycle: Lifecycle
) : LifecycleObserver {
val map = HashMap<String, Int>()
private var _enabled = false
private val _batteryInfoMap = MutableLiveData<HashMap<String, Int>>()
val batteryInfo: LiveData<HashMap<String, Int>>
get() = _batteryInfoMap
init {
_lifecycle.addObserver(this)
}
private val broadcastBatteryInfoListener = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
intent?.let {
val level = it.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
val temperature = it.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, -1)
val voltage = it.getIntExtra(BatteryManager.EXTRA_VOLTAGE, -1)
val technology = it.getIntExtra(BatteryManager.EXTRA_TECHNOLOGY, -1)
val plugged = it.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1)
val health = it.getIntExtra(BatteryManager.EXTRA_HEALTH, -1)
map["level"] = level
map["temperature"] = temperature
_batteryInfoMap.value = map
}
}
}
#OnLifecycleEvent(Lifecycle.Event.ON_START)
fun start() {
if (_enabled) {
_context.registerReceiver(
broadcastBatteryInfoListener,
IntentFilter(Intent.ACTION_BATTERY_CHANGED)
)
}
}
#OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun stop() {
_context.unregisterReceiver(broadcastBatteryInfoListener)
}
#OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun destroy() {
_lifecycle.removeObserver(this)
}
fun enable() {
_enabled = true
if (_lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
start()
}
}
}
And inside MainActivity onCreate()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val into = BatteryInfo(this, lifecycle)
into.enable()
into.batteryInfo.observe(this, Observer {
Log.d("TAG", "${it.values}")
})
}
I'm working on a small app with a BLE device. Sometimes the scan callback works fine and I can find the device but other times the scan callback doesn't work and I was wondering if there's something wrong with my code or if there's a problem with my permission check.
Here's my code:
override fun onCreate(savedInstanceState: Bundle?) {
if (!isBLESupported(this)) {
Toast.makeText(this, "This device doesn't support bluetooth", Toast.LENGTH_SHORT).show()
finish()
} else {
if (!mBtAdapter!!.isEnabled) {
val enableBluetoothIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
startActivityForResult(enableBluetoothIntent, REQUEST_ENABLE_BLUETOOTH)
}
}
adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, macList)
list_view.adapter = adapter
private val lightUUID = UUID.fromString("0000ffb0-0000-1000-8000-00805f9b34fb")
scan_button.setOnClickListener {
scanForDeviceWithFilter(lightUUID)
}
}
private fun scanForDeviceWithFilter(serviceUUID: UUID) {
val uuid = ParcelUuid(id)
val filter = ScanFilter.Builder().setServiceUuid(uuid).build()
val filters = listOf(filter)
val settings = ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build()
checkBTPermissions()
mBtAdapter!!.bluetoothLeScanner.startScan(filters, settings, scanDevicesCallback)
}
private val scanDevicesCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult?) {
result?.let {
if (!macList.contains(result.device.name.toString())) {
macList.add(result.device.name.toString())
adapter.notifyDataSetChanged()
}
Log.d(TAG, "device found:${result.device}")
}
}
override fun onScanFailed(errorCode: Int) {
Log.d(TAG, "Scan failed $errorCode")
}
}
private fun isBLESupported(context: Context): Boolean {
return BluetoothAdapter.getDefaultAdapter() != null && context.packageManager.hasSystemFeature(
PackageManager.FEATURE_BLUETOOTH_LE
)
}
private fun checkBTPermissions() {
val permissionCheck = checkSelfPermission("Manifest.permission.ACCESS_FINE_LOCATION")
if (permissionCheck != 0) {
requestPermissions(
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), 1001
)
}
}
init {
mBtAdapter = BluetoothAdapter.getDefaultAdapter()
}
I'm implementing a small BLE app and I have a list of devices and I want to turn them off when I press on them in the list view.
My service UUID is:0xFFB0
My characteristic UUID is:0xFFB7 Write with response/ Read with response
Characteristic format:
Byte order ; 0 ; 1~19
R/W ON/OFF ; -
0x01 -> ON and 0x00 -> OFF
class MainActivity : AppCompatActivity() {
private var macList: ArrayList<String> = ArrayList()
private var deviceList: ArrayList<BluetoothDevice> = ArrayList()
private lateinit var adapter: ArrayAdapter<String>
private var mBtAdapter: BluetoothAdapter? = null
private var mConnectedGatt: BluetoothGatt? = null
private var mCharacteristic: BluetoothGattCharacteristic? = null
private var on = true
private lateinit var currentDevice: BluetoothDevice
/**
* CHECK IF BT AND LOCATION IS ON!
*/
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (!isBLESupported(this)) {
Toast.makeText(this, "This device doesn't support bluetooth", Toast.LENGTH_SHORT).show()
finish()
} else {
if (!mBtAdapter!!.isEnabled) {
val enableBluetoothIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
startActivityForResult(enableBluetoothIntent, REQUEST_ENABLE_BLUETOOTH)
}
}
adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, macList)
list_view.adapter = adapter
list_view.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ ->
currentDevice = deviceList[position]
mConnectedGatt = currentDevice.connectGatt(applicationContext, false, gattCallback)
}
scan_button.setOnClickListener {
scanForDeviceWithFilter(LIGHT_SERVICE)
}
power_button.setOnClickListener {
mConnectedGatt = currentDevice.connectGatt(applicationContext, true, powerCallback)
on = !on
}
}
override fun onStop() {
super.onStop()
//Disconnect from any active tag connection
if (mConnectedGatt != null) {
mConnectedGatt!!.disconnect()
mConnectedGatt = null
}
}
private fun scanForDeviceWithFilter(serviceUUID: Int) {
val uuid = ParcelUuid(convertFromInteger(serviceUUID))
val filter = ScanFilter.Builder().setServiceUuid(uuid).build()
val filters = listOf(filter)
val settings = ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build()
checkBTPermissions()
mBtAdapter!!.bluetoothLeScanner.startScan(filters, settings, scanDevicesCallback)
Thread.sleep(3000)
mBtAdapter!!.bluetoothLeScanner.stopScan(scanDevicesCallback)
}
private val scanDevicesCallback = object : ScanCallback() {
override fun onBatchScanResults(results: MutableList<ScanResult>?) {
results?.forEach { result ->
macList.add(result.device.toString())
Log.d(TAG, "device name:${result.device}")
}
}
override fun onScanResult(callbackType: Int, result: ScanResult?) {
result?.let {
if (!macList.contains(result.device.name.toString())) {
deviceList.add(result.device)
macList.add(result.device.name.toString())
adapter.notifyDataSetChanged()
}
Log.d(TAG, "device found:${result.device}")
}
}
override fun onScanFailed(errorCode: Int) {
Log.d(TAG, "Scan failed $errorCode")
}
}
private val gattCallback = object : BluetoothGattCallback() {
/* OK */
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
Log.d(TAG, "Connection State Change: " + status + " -> " + connectionState(newState))
if (status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED) {
/**
* Once successfully connected, we must next discover all the services on the
* device before we can read and write their characteristics.
*/
gatt.discoverServices()
Thread.sleep(500)
} else if (status != BluetoothGatt.GATT_SUCCESS) {
/**
* If there is a failure at any stage, simply disconnect
*/
gatt.disconnect()
}
}
private fun connectionState(status: Int): String {
return when (status) {
BluetoothProfile.STATE_CONNECTED -> "Connected"
BluetoothProfile.STATE_DISCONNECTED -> "Disconnected"
BluetoothProfile.STATE_CONNECTING -> "Connecting"
BluetoothProfile.STATE_DISCONNECTING -> "Disconnecting"
else -> status.toString()
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
mCharacteristic = gatt?.getService(convertFromInteger(LIGHT_SERVICE))?.getCharacteristic(convertFromInteger(PASSWORD_CHARACTERISTIC))
mCharacteristic!!.setValue("0123")
if (gatt!!.writeCharacteristic(mCharacteristic)) {
Log.d(TAG, "Login success")
Thread.sleep(500)
} else
Log.d(TAG, "Login failed")
}
override fun onCharacteristicWrite(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int) {
if (status != BluetoothGatt.GATT_SUCCESS) {
Log.d("onCharacteristicWrite", "Failed write, retrying: $status")
gatt!!.writeCharacteristic(characteristic)
}
super.onCharacteristicWrite(gatt, characteristic, status)
}
}
private val powerCallback = object : BluetoothGattCallback() {
/* OK */
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
Log.d(TAG, "Connection State Change: " + status + " -> " + connectionState(newState))
if (status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED) {
gatt.discoverServices()
Thread.sleep(500)
} else if (status != BluetoothGatt.GATT_SUCCESS) {
gatt.disconnect()
}
}
private fun connectionState(status: Int): String {
return when (status) {
BluetoothProfile.STATE_CONNECTED -> "Connected"
BluetoothProfile.STATE_DISCONNECTED -> "Disconnected"
BluetoothProfile.STATE_CONNECTING -> "Connecting"
BluetoothProfile.STATE_DISCONNECTING -> "Disconnecting"
else -> status.toString()
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
if (on) {
mCharacteristic = gatt!!.getService(convertFromInteger(LIGHT_SERVICE))?.getCharacteristic(convertFromInteger(LIGHT_CHARACTERISTIC))
mCharacteristic!!.setValue("0")
gatt.setCharacteristicNotification(mCharacteristic, true)
if (gatt.writeCharacteristic(mCharacteristic)) {
Log.d(TAG, "Power off success")
Thread.sleep(500)
} else Log.d(TAG, "Power off failed")
} else {
mCharacteristic = gatt!!.getService(convertFromInteger(LIGHT_SERVICE))?.getCharacteristic(convertFromInteger(LIGHT_CHARACTERISTIC))
mCharacteristic!!.setValue("1")
if (gatt.writeCharacteristic(mCharacteristic)) {
Log.d(TAG, "Power on success")
Thread.sleep(500)
} else Log.d(TAG, "Power on failed")
}
}
override fun onCharacteristicWrite(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int) {
if (status != BluetoothGatt.GATT_SUCCESS) {
Log.d("onCharacteristicWrite", "Failed write, retrying: $status")
gatt!!.writeCharacteristic(characteristic)
}
super.onCharacteristicWrite(gatt, characteristic, status);
}
}
private fun isBLESupported(context: Context): Boolean {
return BluetoothAdapter.getDefaultAdapter() != null && context.packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)
}
init {
mBtAdapter = BluetoothAdapter.getDefaultAdapter()
}
private fun checkBTPermissions() {
val permissionCheck = checkSelfPermission("Manifest.permission.ACCESS_FINE_LOCATION")
if (permissionCheck != 0) {
requestPermissions(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), 1001)
}
}
fun convertFromInteger(i: Int): UUID? {
val msb = 0x0000000000001000L
val lsb = -0x7fffff7fa064cb05L
val value = (i and ((-0x1).toLong()).toInt()).toLong()
return UUID(msb or (value shl 32), lsb)
}
companion object {
private const val TAG = "Main Activity"
private const val LIGHT_SERVICE = 0xffb0
private const val LIGHT_CHARACTERISTIC = 0xffb7
private const val PASSWORD_CHARACTERISTIC = 0xffba
private const val REQUEST_ENABLE_BLUETOOTH = 1
}
}
I changed my code, but now when I try to write I get status = 128; GATT_NO_RESOURCES and I have no clue what to do.
I'm not sure if you have understand the flow correctly.
The onCharacteristicWrite is not called "to send data to the BLE device". It is being called after you have called writeCharacteristic, after the remote device has responded.
This callback is called when you are trying to send data using writeCharacteristic(characteristics) and the BLE device responds with some value.
This is not really correct about onCharacteristicChanged. This method is called whenever the local device receives a notification from the remote device. To enable this you must first tell the local Bluetooth stack to forward the notifications to you by calling setCharacteristicNotification first, and write the client characteristic configuration descriptor to the remote device so it will send notifications.
After you have called writeDescriptor, the onDescriptorWrite callback will be called when the remote device responds.
What you should do is to implement the onServicesDiscovered method which will be called as a result of discoverServices, when the service discovery is complete. In this callback you can call writeCharacteristic on the ON/OFF characteristic.
From your description it doesn't seem you need to receive notifications.
I solved my problem. I was sending a string to the device, not a byte value.
mCharacteristic!!.value = byteArrayOf(0x00) fixed my problem.
I have implemented BLE and it works. The flow is: DrawerActivity starts, it sets a fragmentA, which has BLE implementation, because I want active BLE only in fragmentA. So if you switch to fragmentB it should terminate the BLE connection and upair the device.
What happens is that the only time it completely disconnects is, when you close the app, or turn off the bluetooth. If you close the fragmentA and open it again it works from drawerActivity. If you do it again, so this is now the 3rd time, it won't pair to the BLE device. When I investigated further, it won't even find the correct BLE device.. Meaning if you run the fragment the 4th, 5th time it is the same result.
What I want to achieve is when onDestroy in Fragment is called it should disconnect from the BLE and destroy all references. And then if you go into the fragmentA again it should recreate everything again, no matter how many times you open the fragmentA.But now the device isn't found anymore, probably because it didn't disconnect properly and BLE device has old references or something.
This is how I disconnect.
This is onDestroy method:
override fun onDestroy() {
super.onDestroy()
activity?.unregisterReceiver(bluetoothReceiver)
bluetoothManager?.disconnectBluetoothService()
bluetoothManager = null
}
And in bluetoothManager
fun disconnectBluetoothService() {
bluetoothService?.disconnectGattServer()
}
And at the bluetoothService:
fun disconnectGattServer() {
mConnected = false
mBluetoothGatt?.disconnect()
mBluetoothGatt?.close()
mBluetoothGatt = null
}
Here are the all 3 files that are used for BLE.
FragmentA
private var bluetoothManager: MyBluetoothManager? = null
private val bluetoothReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action
if (action == BluetoothAdapter.ACTION_STATE_CHANGED) {
when (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)) {
BluetoothAdapter.STATE_OFF -> {}
BluetoothAdapter.STATE_ON -> {
initBluetoothIfPossible()
bluetoothManager?.scanForBluetoothDevicesIfPossible(true)
}
}
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
listenToBluetoothChanges()
}
override fun onDestroy() {
super.onDestroy()
activity?.unregisterReceiver(bluetoothReceiver)
bluetoothManager?.disconnectBluetoothService()
bluetoothManager = null
}
private fun listenToBluetoothChanges() {
val filter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
carSharingActivity?.registerReceiver(bluetoothReceiver, filter)
}
private fun initBluetoothIfPossible() {
bluetoothToken ?: return
if (bluetoothManager != null) {
bluetoothManager!!.pairDevice()
} else {
bluetoothManager = MyBluetoothManager(activity as Activity,
this,
bluetoothToken!!.token,
bluetoothToken!!.sessionKey,
bluetoothToken!!.uuid)
}
setImageForBluetoothStatus()
}
MyBluetoothManager
class ACCarBluetoothManager(var activity: Activity,
var listener: MyBluetoothListener,
private var token: String,
private var sessionKey: String,
private var accessDeviceUID: String) {
// Bluetooth adapter
private var bluetoothAdapter: BluetoothAdapter?
// Bluetooth service
private var bluetoothService: MyBluetoothService? = null
private var isBluetoothAvailable: Boolean = false
val isBluetoothEnabled: Boolean
get() = bluetoothAdapter?.isEnabled == true
var connectionStatus: Boolean = false
set(value) {
if (field == value) return
field = value
if (value) stopScanning()
else startScanning()
}
private var savedDevice: BluetoothDevice? = null
/**
* Service lifecyle management.
*/
private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(componentName: ComponentName, service: IBinder) {
bluetoothService = (service as MyBluetoothService.LocalBinder).service
bluetoothService?.isConnectedListener = { isConnected ->
listener.isConnected(isConnected)
connectionStatus = isConnected
}
isBluetoothAvailable = bluetoothService?.initialize() == true
}
override fun onServiceDisconnected(componentName: ComponentName) {
bluetoothService = null
connectionStatus = false
}
}
/**
* Broadcast receiver.
*/
private val gattUpdateReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
try {
when (intent.action) {
BluetoothConstants.ACTION_GATT_SERVICES_DISCOVERED -> bluetoothService?.initializeIndications()
BluetoothConstants.ACTION_INDICATIONS_INITIALIZED -> bluetoothService?.startAuthentication(token)
}
} catch (e: Exception) {
Log.e("GattUpdateReciever", e.message)
}
}
}
/**
* Bluetooth device scanning callback. The scanned device is added to the list of available
* devices.
*/
private val bluetoothScanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
super.onScanResult(callbackType, result)
val btDevice = result.device
if (btDevice.name.isNullOrEmpty()) return
if (deviceMatchesUID(btDevice)) {
savedDevice = btDevice
pairDevice()
}
}
}
init {
val gattServiceIntent = Intent(activity, MyBluetoothService::class.java)
activity.bindService(gattServiceIntent, this.serviceConnection, Context.BIND_AUTO_CREATE)
// Setup bluetooth adapter
val bluetoothManager = activity.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
bluetoothAdapter = bluetoothManager.adapter
// If bluetooth is not enabled, request permission, otherwise start scanning process, Not IMPLEMENTED, because it is not needed.
scanForBluetoothDevicesIfPossible()
activity.registerReceiver(gattUpdateReceiver, BluetoothConstants.makeGattUpdateIntentFilter())
}
fun scanForBluetoothDevicesIfPossible(enable: Boolean = isBluetoothEnabled) {
val hasLocationPermission = ContextCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
if (enable) {
if (hasLocationPermission) {
startScanning()
}
//You can request for location permission if he doesn't have permission
} else {
stopScanning()
}
}
fun pairDevice() {
if (isBluetoothAvailable && savedDevice != null) {
bluetoothService?.connect(savedDevice!!)
}
}
fun startScanning() {
bluetoothAdapter?.bluetoothLeScanner?.startScan(bluetoothScanCallback)
}
fun stopScanning() {
bluetoothAdapter?.bluetoothLeScanner?.stopScan(bluetoothScanCallback)
}
fun deviceMatchesUID(device: BluetoothDevice): Boolean {
return device.name.equals(accessDeviceUID, ignoreCase = true)
}
}
MyBluetoothService
class ACCarBluetoothService : Service() {
var isConnectedListener: ((Boolean) -> Unit)? = null
var mConnected = false
set(value) {
field = value
isConnectedListener?.invoke(value)
}
private val mBinder = LocalBinder()
private var mBluetoothManager: BluetoothManager? = null
private var mBluetoothAdapter: BluetoothAdapter? = null
private var mBluetoothGatt: BluetoothGatt? = null
private var mDividedTokenList: MutableList<ByteArray>? = null
// Various callback methods defined by the BLE API.
private val mGattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
if (status == BluetoothGatt.GATT_FAILURE
|| status != BluetoothGatt.GATT_SUCCESS
|| newState == BluetoothProfile.STATE_DISCONNECTED) {
disconnectGattServer()
return
}
if (newState == BluetoothProfile.STATE_CONNECTED) {
gatt.discoverServices()
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
if (status == BluetoothGatt.GATT_SUCCESS) onServiceDiscoveryReady()
}
override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
when {
descriptor.characteristic.uuid == BluetoothConstants.UUID_COMMAND_CHALLENGE -> setCharacteristicNotification(
BluetoothConstants.UUID_DEBUG,
true)
descriptor.characteristic.uuid == BluetoothConstants.UUID_DEBUG -> setCharacteristicNotification(
BluetoothConstants.UUID_STATUS_1,
true)
descriptor.characteristic.uuid == BluetoothConstants.UUID_STATUS_1 -> setCharacteristicNotification(
BluetoothConstants.UUID_STATUS_2,
true)
descriptor.characteristic.uuid == BluetoothConstants.UUID_STATUS_2-> setCharacteristicNotification(
BluetoothConstants.UUID_STATUS_3,
true)
else -> onIndicationsInitialized()
}
}
override fun onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
if (status == BluetoothGatt.GATT_SUCCESS) broadcastUpdate(characteristic)
}
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
if (characteristic.uuid == BluetoothConstants.UUID_COMMAND_CHALLENGE) {
commandChallenge = characteristic.value
} else {
broadcastUpdate(characteristic)
}
}
override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
if (BluetoothConstants.UUID_AUTHORIZE_PHONE == characteristic.uuid) writeNextPartToken()
}
}
override fun onBind(intent: Intent): IBinder? {
return mBinder
}
/**
* Initializes a reference to the local Bluetooth adapter.
*
* #return Return true if the initialization is successful.
*/
fun initialize(): Boolean {
if (mBluetoothManager == null) {
mBluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
if (mBluetoothManager == null) return false
}
mBluetoothAdapter = mBluetoothManager!!.adapter
if (mBluetoothAdapter == null) return false
return true
}
fun initializeIndications() {
setCharacteristicNotification(BluetoothConstants.UUID_COMMAND_CHALLENGE, true)
}
fun startAuthentication(token: String) {
mDividedTokenList = Tools.divideArray(Tools.decodeBase64(token))
writeNextPartToken()
}
fun writeCommand(sessionKey: String, command: ByteArray) {
val safeCommand = Tools.generateSafeCommand(command, commandChallenge, Tools.decodeBase64(sessionKey))
val commandCharacteristic = mBluetoothGatt!!.getService(BluetoothConstants.UUID_CAR_CONTROL_SERVICE)
.getCharacteristic(BluetoothConstants.UUID_COMMAND_PHONE)
commandCharacteristic.value = safeCommand
mBluetoothGatt!!.writeCharacteristic(commandCharacteristic)
}
fun connect(device: BluetoothDevice) {
mBluetoothGatt = device.connectGatt(this, false, this.mGattCallback)
}
fun disconnectGattServer() {
mConnected = false
mBluetoothGatt?.disconnect()
mBluetoothGatt?.close()
mBluetoothGatt = null
}
private fun onIndicationsInitialized() {
val intent = Intent()
intent.action = BluetoothConstants.ACTION_INDICATIONS_INITIALIZED
sendBroadcast(intent)
}
private fun onServiceDiscoveryReady() {
val intent = Intent()
intent.action = BluetoothConstants.ACTION_GATT_SERVICES_DISCOVERED
sendBroadcast(intent)
}
private fun writeNextPartToken() {
if (mDividedTokenList!!.isEmpty()) {
broadcastUpdate(BluetoothConstants.ACTION_INIT_READY)
return
}
writeValue(BluetoothConstants.UUID_AUTHORIZE_PHONE, mDividedTokenList!!.removeAt(0))
}
private fun broadcastUpdate(action: String) {
val intent = Intent(action)
sendBroadcast(intent)
}
private fun writeValue(characteristicUUID: UUID, valueBytes: ByteArray) {
if (mBluetoothAdapter == null || mBluetoothGatt == null) return
val service = mBluetoothGatt!!.getService(BluetoothConstants.UUID_CAR_CONTROL_SERVICE)
val characteristic = service.getCharacteristic(characteristicUUID)
characteristic.value = valueBytes
mBluetoothGatt!!.writeCharacteristic(characteristic)
}
private fun setCharacteristicNotification(characteristicUUID: UUID, enabled: Boolean) {
if (mBluetoothAdapter == null || mBluetoothGatt == null) return
val characteristic = mBluetoothGatt!!
.getService(BluetoothConstants.UUID_CAR_INFORMATION_SERVICE)
.getCharacteristic(characteristicUUID)
mBluetoothGatt!!.setCharacteristicNotification(characteristic, enabled)
characteristic.getDescriptor(CONFIG_DESCRIPTOR)?.let {
it.value = if (enabled) BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
else BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE
mBluetoothGatt!!.writeDescriptor(it)
}
}
private fun broadcastUpdate(characteristic: BluetoothGattCharacteristic) {
val intent = Intent()
if (BluetoothConstants.UUID_STATUS_1 == characteristic.uuid) {
if (!hasDataInBluetooth(characteristic.value)) {
mConnected = true
statusListener?.invoke()
}
intent.action = BluetoothConstants.ACTION_STATUS_1_AVAILABLE
intent.putExtra(BluetoothConstants.EXTRA_DATA, characteristic.value)
}
if (BluetoothConstants.UUID_DEBUG == characteristic.uuid) {
intent.action = BluetoothConstants.ACTION_DEBUG_AVAILABLE
intent.putExtra(BluetoothConstants.EXTRA_DATA, characteristic.value)
}
if (BluetoothConstants.UUID_STATUS_2 == characteristic.uuid) {
intent.action = BluetoothConstants.ACTION_STATUS_1_AVAILABLE
intent.putExtra(BluetoothConstants.EXTRA_DATA, characteristic.value)
}
if (BluetoothConstants.UUID_STATUS_3 == characteristic.uuid) {
intent.action = BluetoothConstants.ACTION_STATUS_1_AVAILABLE
intent.putExtra(BluetoothConstants.EXTRA_DATA, characteristic.value)
}
sendBroadcast(intent)
}
private fun hasDataInBluetooth(byteArray: ByteArray): Boolean {
for (b in byteArray) {
if (b.toInt() != 0) {
return false
}
}
return true
}
inner class LocalBinder : Binder() {
val service: MyBluetoothService
get() = this#MyBluetoothService
}
}
I found the solution.
The problem vas in:
fun pairDevice() {
if (isBluetoothAvailable && savedDevice != null) {
bluetoothService?.connect(savedDevice!!)
}
}
Because it was trying to connect again and again it stopped broadcasting.
I solved it with:
fun pairDevice() {
if (isConnected) return
if (isBluetoothAvailable && savedDevice != null) {
bluetoothService?.connect(savedDevice!!)
isConnected = true
}
}
I want to detect the connection status of a paired Bluetooth headset
to the phone.
In Android 3.0 (API level 11) "BluetoothHeadset" class has
"isAudioConnected()" method.
I don't know how to create (initialize) a "BluetoothHeadset" object.
It seems that I need to use
"getProfileProxy ()" but I need a sample code to find out how I need
to create and pass the parameters.
Thanks,
Hos
You need to implement BluetoothProfile.ServiceListener :
BluetoothProfile.ServiceListener b = new BlueToothListener();
boolean profileProxy = BluetoothAdapter.getDefaultAdapter()
.getProfileProxy(Handler.bot, b, BluetoothProfile.HEADSET);
public class BlueToothListener implements ServiceListener {
public static BluetoothHeadset headset;
public static BluetoothDevice bluetoothDevice;
#Override
public void onServiceDisconnected(int profile) {// dont care
headset = null;
}
#Override
public void onServiceConnected(int profile,
BluetoothProfile proxy) {// dont care
try {
Debugger.test("BluetoothProfile onServiceConnected "+proxy);
if (proxy instanceof BluetoothHeadset)
headset = ((BluetoothHeadset) proxy);
else// getProfileProxy(Handler.bot, b, BluetoothProfile.HEADSET);
return;// ^^ => NEVER
List<BluetoothDevice> connectedDevices = proxy
.getConnectedDevices();
for (BluetoothDevice device : connectedDevices) {
Debugger.log("BluetoothDevice found :" + device);
bluetoothDevice = device;
int connectionState = headset.getConnectionState(bluetoothDevice);
Debugger.log("BluetoothHeadset connectionState "+connectionState);//2 == OK
boolean startVoiceRecognition = headset
.startVoiceRecognition(device);
if (startVoiceRecognition) {
Debugger
.log("BluetoothHeadset init Listener OK");
return;
}
else
Notify.popup("Bluetooth headset can't start speech recognition");
}
} catch (Exception e) {
// }
}
}
}
`
Monitoring of the BT status can be done indeed by polling. Here's how I did it (full sample here) . Note that it's just a sample and you should manage the polling better:
manifest
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
gradle
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'com.google.android.material:material:1.7.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation "androidx.work:work-runtime-ktx:2.7.1"
MainActivityViewModel.kt
#UiThread
class MainActivityViewModel(application: Application) : BaseViewModel(application) {
private val bluetoothAdapter: BluetoothAdapter =
context.getSystemService<BluetoothManager>()!!.adapter
private var bluetoothHeadsetProfile: BluetoothProfile? = null
val connectedDevicesLiveData =
DistinctLiveDataWrapper(MutableLiveData<ConnectedDevicesState>(ConnectedDevicesState.Idle))
val bluetoothTurnedOnLiveData = DistinctLiveDataWrapper(MutableLiveData<Boolean?>(null))
val isConnectedToBtHeadsetLiveData = DistinctLiveDataWrapper(MutableLiveData<Boolean?>(null))
private val pollingBtStateRunnable: Runnable
init {
updateBtStates()
pollingBtStateRunnable = object : Runnable {
override fun run() {
updateBtStates()
handler.postDelayed(this, POLLING_TIME_IN_MS)
}
}
// Establish connection to the proxy.
val serviceListener = object : BluetoothProfile.ServiceListener {
override fun onServiceConnected(profile: Int, bluetoothProfile: BluetoothProfile) {
this#MainActivityViewModel.bluetoothHeadsetProfile = bluetoothProfile
handler.removeCallbacks(pollingBtStateRunnable)
pollingBtStateRunnable.run()
}
override fun onServiceDisconnected(profile: Int) {
handler.removeCallbacks(pollingBtStateRunnable)
updateBtStates()
}
}
bluetoothAdapter.getProfileProxy(context, serviceListener, BluetoothProfile.HEADSET)
onClearedListeners.add {
this.bluetoothHeadsetProfile?.let { bluetoothProfile ->
bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothProfile)
}
handler.removeCallbacks(pollingBtStateRunnable)
}
}
fun initWithLifecycle(lifecycle: Lifecycle) {
lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)
pollingBtStateRunnable.run()
}
override fun onPause(owner: LifecycleOwner) {
super.onPause(owner)
handler.removeCallbacks(pollingBtStateRunnable)
}
})
}
#UiThread
private fun updateBtStates() {
// Log.d("AppLog", "updateBtStates")
val isBlueToothTurnedOn = bluetoothAdapter.state == BluetoothAdapter.STATE_ON
bluetoothTurnedOnLiveData.value = isBlueToothTurnedOn
if (!isBlueToothTurnedOn) {
connectedDevicesLiveData.value = ConnectedDevicesState.BluetoothIsTurnedOff
isConnectedToBtHeadsetLiveData.value = false
return
}
val isConnectedToBtHeadset = try {
bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET) == BluetoothAdapter.STATE_CONNECTED
} catch (e: SecurityException) {
null
}
isConnectedToBtHeadsetLiveData.value = isConnectedToBtHeadset
val bluetoothProfile = bluetoothHeadsetProfile
if (bluetoothProfile != null) {
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) {
val connectedDevicesSet = bluetoothProfile.connectedDevices.toHashSet()
val previousConnectedDevices =
(connectedDevicesLiveData.value as? ConnectedDevicesState.GotResult)?.connectedDevices
if (previousConnectedDevices == null || previousConnectedDevices != connectedDevicesSet)
connectedDevicesLiveData.value =
ConnectedDevicesState.GotResult(connectedDevicesSet)
} else {
connectedDevicesLiveData.value =
ConnectedDevicesState.NeedBlueToothConnectPermission
}
} else {
connectedDevicesLiveData.value = ConnectedDevicesState.Idle
}
}
companion object {
private const val POLLING_TIME_IN_MS = 500L
}
}
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainActivityViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProvider(this)[MainActivityViewModel::class.java]
viewModel.initWithLifecycle(lifecycle)
viewModel.bluetoothTurnedOnLiveData.observe(this) {
Log.d("AppLog", "MainActivity bluetoothTurnedOnLiveData BT turned on? $it")
}
viewModel.isConnectedToBtHeadsetLiveData.observe(this) {
Log.d("AppLog", "MainActivity isConnectedToBtHeadsetLiveData BT headset connected? $it")
}
viewModel.connectedDevicesLiveData.observe(this) {
Log.d("AppLog", "MainActivity connectedDevicesLiveData devices: $it")
}
findViewById<View>(R.id.grantBtPermission).setOnClickListener {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
requestPermissions(arrayOf(Manifest.permission.BLUETOOTH_CONNECT), 1)
}
}
}
}
ConnectedDevicesState.kt
sealed class ConnectedDevicesState {
object Idle : ConnectedDevicesState() {
override fun toString(): String {
if (BuildConfig.DEBUG)
return "Idle"
return super.toString()
}
}
class GotResult(#Suppress("MemberVisibilityCanBePrivate") val connectedDevices: Set<BluetoothDevice>) : ConnectedDevicesState() {
override fun toString(): String {
if (BuildConfig.DEBUG) {
return "GotResult: connectedDevices:${
connectedDevices.map {
try {
it.name
} catch (e: SecurityException) {
it.address
}
}
}"
}
return super.toString()
}
}
object BluetoothIsTurnedOff : ConnectedDevicesState() {
override fun toString(): String {
if (BuildConfig.DEBUG)
return "BluetoothIsTurnedOff"
return super.toString()
}
}
object NeedBlueToothConnectPermission : ConnectedDevicesState() {
override fun toString(): String {
if (BuildConfig.DEBUG)
return "NeedBlueToothConnectPermission"
return super.toString()
}
}
}
DistinctLiveDataWrapper.kt
class DistinctLiveDataWrapper<T>(#Suppress("MemberVisibilityCanBePrivate") val mutableLiveData: MutableLiveData<T>) {
#Suppress("MemberVisibilityCanBePrivate")
val distinctLiveData = Transformations.distinctUntilChanged(mutableLiveData)
var value: T?
#UiThread
set(value) {
mutableLiveData.value = value
}
get() {
return mutableLiveData.value
}
#AnyThread
fun postValue(value: T) {
mutableLiveData.postValue(value)
}
fun observe(lifecycleOwner: LifecycleOwner, observer: Observer<in T>) {
distinctLiveData.observe(lifecycleOwner, observer)
}
}
BaseViewModel.kt
/**usage: class MyViewModel(application: Application) : BaseViewModel(application)
* getting instance: private lateinit var viewModel: MyViewModel
* viewModel=ViewModelProvider(this).get(MyViewModel::class.java)*/
abstract class BaseViewModel(application: Application) : AndroidViewModel(application) {
#Suppress("MemberVisibilityCanBePrivate")
var isCleared = false
#Suppress("MemberVisibilityCanBePrivate")
val onClearedListeners = ArrayList<Runnable>()
#Suppress("unused")
#SuppressLint("StaticFieldLeak")
val context: Context = application.applicationContext
#Suppress("unused")
val handler = Handler(Looper.getMainLooper())
override fun onCleared() {
super.onCleared()
isCleared = true
onClearedListeners.forEach { it.run() }
}
}