I'm quite new to Kotlin and to Android development, but I have got a proper connection between my phone and a HM-10 bluetooth module. This bluetooth module is a BLE device.
I only want to send a simple "1" to the Bluetooth module, and I am attempting to do so with this function.
fun writeCharacteristic(){
service = mBluetoothGatt!!.getService(
UUID.fromString("00001800-0000-1000-8000-00805f9b34fb"))
val characteristic : BluetoothGattCharacteristic =
service.getCharacteristic(UUID.fromString("00002a03-0000-1000-8000-00805f9b34fb"))
mBluetoothGatt?.let { gatt ->
characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
characteristic.value = "1".toByteArray()
gatt.writeCharacteristic(characteristic)
} ?: error("No established connection")
}
I apologize if there is something very obvious I am missing, but I have done my best to try and stick to the official Android documentation about this and they do a poor job of actually explaining how this specific protocol works. I tried the same thing a few months earlier with Bluetooth classic and it worked like a breeze :D
My actual issue is that this doesn't actually do anything. I get a null pointer exception on the bluetooth adapter.
EDIT (as per request by user emil):
class WakeupReceiver : BroadcastReceiver() {
#RequiresApi(Build.VERSION_CODES.M)
override fun onReceive(context: Context?, intent: Intent?) {
BluetoothLeService().writeCharacteristic()
Log.i("Confirmation", "Message sent")
}
}
This is how I call the function. Right now I can't get a reference to the specific error I get, but it is a NullPointerException.
Related
I am making an android app that monitors sensors from my Arduino Mega 2560 and using Bluetooth HC-05 Module
to connect the app and the Arduino. Right now I have the bluetooth connection and write done but I am stuck on read, I tried calling readBluetoothData in another activity but it freezes the activity and crashes, I would like to know how to pass "readMessage" to another Activity so I can display the sensor values and to stop it from crashing. Really would like to ask advice on this since I am relatively new to both kotlin and Bluetooth coding.
private fun readBluetoothData() {
val bluetoothSocketInputStream = m_bluetoothSocket!!.inputStream
val buffer = ByteArray(1024)
var bytes: Int
//Loop to listen for received bluetooth messages
while (true) {
try {
bytes = bluetoothSocketInputStream.read(buffer)
val readMessage = String(buffer, 0, bytes)
} catch (e: IOException) {
e.printStackTrace()
break
}
}
}
can use aplication class it is upstairs then activity
class MainApp: Application() {
you bluetooth conection object
}
manifest:
<application
android:name=".MainApp"
in you activity
context?.applicationContext as MainApp).(you bluetooth conection object)
or use single activity app and fragments
I think the better solution is using Android service for bluetooth work and exchange data with app using broadcast receiver or something similar
Don't forget to use another thread or process for preventing app to stuck in a loop
I use AltBeacon library for simple ble scanning. Scanning for about 7 or 8 seconds then I stop it. tapping button for rescanning. the problem that I have had from the previous version of this library , when I scan then stop it, and disconnect beacon power and I rescan , rangenotifier or observer(another method to watch beacons) could see disconnected beacon! for first time after disconnected it is happened. after that works correctly and if I do the whole process again it is happens.
in the previous library version I had to bind and unbind each time.(not good approach but I had to do) .but in newer version there are no unbind or bind methods. most of methods and functions are deprecated.
I use scanner in fragment. even it is not matter if switch to another fragment. when I come back to scanning fragment again it finds disconnected beacon for fist time after beacon power disconnected. I'm not sure if this library is suitable for a simple bacon scanning. But it is very powerful and simplified some complex thing.
class ScanningFragment() : androidx.fragment.app.Fragment(){
lateinit var beaconManager:BeaconManager
lateinit var region:Region
val rangeNotifier =object:RangeNotifier{
override fun didRangeBeaconsInRegion(beacons: MutableCollection<Beacon>?, region: Region?) {
Log.d(TAG,"in didRangeBeacon")
if (beacons!!.size > 0) {
Log.d(TAG, "didRangeBeaconsInRegion called count: " + beacons.size + beacons.iterator().next().id1)
val firstBeacon = beacons.iterator().next()
}
override fun onCreate(savedInstanceState: Bundle?)
{
Log.d("lifecycl","it is oncreate ")
super.onCreate(savedInstanceState)
BeaconManager.setDebug(true)
beaconManager=BeaconManager.getInstanceForApplication(requireContext()).apply {
foregroundScanPeriod=7000L
foregroundBetweenScanPeriod=5000L
updateScanPeriods()
beaconParsers.clear()
beaconParsers.add(BeaconParser().setBeaconLayout("m:2-3=0215,i:4-8,i:4-19,i:20-21,i:22-23,p:24-24"))
region = Region("prefixRegion", Identifier.parse("0x0000000000"), null, null)
}
setupPermissions()
}
fun rangingButtonTapped() {
if (beaconManager.rangedRegions.size == 0) {
beaconManager.addRangeNotifier(rangeNotifier)
beaconManager.startRangingBeacons(region)
binding.insideviewmodel?.isScanning?.value = true
}
else {
beaconManager.stopRangingBeacons(region)
binding.BTNScan.run {
Handler(Looper.getMainLooper()).postDelayed({ stopAnimation() }, 1000)
Handler(Looper.getMainLooper()).postDelayed({ revertAnimation() }, 2000)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.insideviewmodel?.isScanning?.observe(viewLifecycleOwner,Observer{
currentStatusScan->
if(currentStatusScan)
{
object : CountDownTimer(8500, 1000) {
override fun onTick(p0: Long) {
}
override fun onFinish() {
beaconManager.stopRangingBeacons(region)
beaconManager.removeRangeNotifier(rangeNotifier)
binding.insideviewmodel?.isScanning?.value = false
}
}.start()
}
})
}
}
}
it is a debug log for first time scanning.
https://jpst.it/2LVY4
it is a debug log for rescanning after disconnecting beacon power.
https://jpst.it/2LVZs
Profiler:
The second log line "after disconnecting beacon power" shows that the Android OS BLE scanner does indeed deliver an iBeacon detection at 12:53:31:
2022-02-24 12:53:31.117 23528-23528/ D/CycledLeScannerForLollipop: got record ... Processing pdu type FF: 0201041aff4c000215....
The library source code shows that this log line is issued immediately upon a callback from the operating system about a BLE advertisement detection. See here.
Clearly it is not possible for a Bluetooth scanner to detect an advertisement from a powered-off BLE device so there must be an alternate explanation. A few possibilities:
The BLE transmitter is not really powered off at (or slightly before) 12:53:31.117
The detected advertisement comes from a different transmitter
The callback from the Android OS is delayed, perhaps because the main thread on which is delivered was blocked by lots of CPU usage in the app.
Some flaw in the bluetooth stack or UI thread handling for the phone in question is delaying delivery of detections.
In order to figure out the cause I would suggest the following:
To eliminate a bluetooth stack flaw, test the same code on a different Android phone, preferably by a different manufacturer.
To eliminate the possibility of the UI thread being blocked, run this in the Android Studio profiler, or simply cut out as much code as possible that executes before the delay is seen.
To verify the transmitter is really off and that there are no other transmitters around, use a second phone with an off the shelf beacon scanner to monitor what devices are actually transmitting. Only perform your test when you confirm with a second device there are no other visible transmitters.
I'm developing a frame exchange sequence between an nRF52840 and an Android smartphone using the BLE protocol.
The first time I connect, everything works fine.
I activate the listening of BLE notifications by the Android smartphone with this method:
fun enableBleNotificationsOnCentral(currentBluetoothGatt: BluetoothGatt, serviceUUID: UUID, characteristicUUID: UUID) {
getMainDeviceService(currentBluetoothGatt, serviceUUID)?.let { service ->
val notificationConfiguration = service.getCharacteristic(characteristicUUID)
val result = currentBluetoothGatt.setCharacteristicNotification(notificationConfiguration, true)
println(result)
}
}
And I enable sending BLE notifications on the nRF52840 with this method:
fun enableBleNotificationsOnPeripheral(currentBluetoothGatt: BluetoothGatt, serviceUUID: UUID, characteristicUUID: UUID, descriptorUUID: UUID) {
getMainDeviceService(currentBluetoothGatt, serviceUUID)?.let { service ->
val descriptorConfiguration = service.getCharacteristic(characteristicUUID).getDescriptor(
descriptorUUID).apply {
value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
}
val result = currentBluetoothGatt.writeDescriptor(descriptorConfiguration)
println(result)
}
}
These methods are called each time my smartphone is connected to the nRF52840.
But if I disconnect and connect a second time, I receive each of the notifications in duplicate.
In addition, if I disconnect and connect a 3rd time, I receive each notification 3 times, and one more each time I reconnect.
I checked my code on the nRF52840 and it does not duplicate notifications.
Here is the method I call when I request a disconnection:
private fun disconnectFromCurrentDevice() {
currentBluetoothGatt?.disconnect()
BLECallbackManager.currentDevice = null
setUiMode(false)
}
I guess my problem is related to the fact that I don't disable the receipt of BLE notifications by my Android application when I disconnect but I'm not sure. And if that's where the problem comes from, when should I do it in the disconnect method? Can you help me?
I guess you're creating a new BluetoothGatt object for every new connection attempt, but you not destroy the previous one.
Try change disconnect() to close().
Background
I've noticed that in WifiManager class there is a function called addNetwork, that might be useful if I want to restore or save networks information (network name AKA SSID, together with the password and the type), so that I could also connect to it.
The problem
I can't find much information about how to do such a thing. I've seen various examples on StackOverflow, and if I target Android API 28 (or below), I indeed succeed to make it add a network and even connect to it.
When targeting Android 29 (Android Q), however, it fails to add the network.
What I've found
Since I'm trying on Pixel 2 with Android Q beta 4, I think that maybe it's because addNetwork is deprecated, so the docs even say so, and that if I target Android Q, it won't work, and indeed it doesn't work:
Compatibility Note: For applications targeting Build.VERSION_CODES.Q
or above, this API will always return -1.
The way it seems it should work up till Android Q (excluding), is by preparing WifiConfiguration and adding it. Later I can also connect to it if I wish. On Android Q, it seems it was replaced by WifiNetworkSuggestion, but it doesn't seem like it's about adding a network at all:
The Network Suggestion object is used to provide a Wi-Fi network for
consideration when auto-connecting to networks. Apps cannot directly
create this object, they must use
WifiNetworkSuggestion.Builder#build() to obtain an instance of this
object.
Apps can provide a list of such networks to the platform using
WifiManager#addNetworkSuggestions(List).
Here's my current code, for pre-Android-Q
#WorkerThread
fun addNetwork(context: Context, networkName: String, networkPassword: String? = null, keyMgmt: Int = WifiConfiguration.KeyMgmt.NONE) {
val wifiManager = context.getSystemService(Context.WIFI_SERVICE) as WifiManager
val conf = WifiConfiguration()
conf.SSID = "\"$networkName\""
conf.preSharedKey = if (networkPassword.isNullOrEmpty()) "" else "\"$networkPassword\""
conf.allowedKeyManagement.set(keyMgmt)
when (keyMgmt) {
WifiConfiguration.KeyMgmt.WPA_PSK -> {
//WPA/WPA2
}
WifiConfiguration.KeyMgmt.IEEE8021X -> {
}
WifiConfiguration.KeyMgmt.WPA_EAP -> {
}
WifiConfiguration.KeyMgmt.NONE -> {
if (networkPassword.isNullOrEmpty()) {
//open network
conf.wepKeys[0] = "\"\""
} else {
//wep
conf.wepKeys[0] = "\"" + networkPassword + "\""
conf.wepTxKeyIndex = 0
conf.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP40)
}
}
}
if (networkPassword.isNullOrEmpty()) {
//open network
conf.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE)
} else {
}
wifiManager.isWifiEnabled = true
while (!wifiManager.pingSupplicant()) {
Log.d("AppLog", "waiting to be able to add network")
}
val networkId = wifiManager.addNetwork(conf)
if (networkId == -1)
Log.d("AppLog", "failed to add network")
else {
wifiManager.enableNetwork(networkId, false)
Log.d("AppLog", "success to add network")
}
}
Seems it requires only these permissions:
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
But in any case, this works as long as you don't target Android Q (API 29) and above. When you target it, I indeed always get "-1" as a result, meaning it fails.
I've also found an issue on the issue tracker (here and I wrote about it here), telling about someone that needs the API back, but I'm not sure it's about adding a network.
Looking at WifiNetworkSuggestion, I don't see that it has as many things to set as WifiConfiguration via its builder, so this is another reason for why I suspect it's not about adding a network.
But I tried anyway. Here's the code I've tried, for example, to add a normal WPA network:
#WorkerThread
fun addNetworkAndroidQ(context: Context, networkName: String, networkPassword: String? = null) {
val wifiManager = context.getSystemService(Context.WIFI_SERVICE) as WifiManager
val list = ArrayList<WifiNetworkSuggestion>()
val builder = WifiNetworkSuggestion.Builder().setSsid(networkName)
if (!networkPassword.isNullOrEmpty())
builder.setWpa2Passphrase(networkPassword)
list.add(builder.build())
val result = wifiManager.addNetworkSuggestions(list)
if (result == WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS)
Log.d("AppLog", "success")
else Log.d("AppLog", "failed")
}
When running (I gave it my Wifi network details, after making the OS forget about it), it says it succeeded, but nothing occurred on the OS's Wifi settings. The network doesn't exist there with the password I've added. So I really don't get what it did...
After a few long seconds, I've noticed a notification asking me if it's ok to connect to the suggested networks made by the app:
But still when I chose that I accept, it didn't do anything, as before.
I tried to make another POC, thinking I might have done it incorrectly, but then it didn't even show the notification. Since I think this whole behavior is a bug, I've reported about it here.
Not only that, but I've found out that if indeed it is supposed to add a network one way or another, it still has some serious restrictions, such as max added networks (here) and being removed upon uninstall of the app (here)
The questions
How should Android Q be handled exactly? Is there really no API anymore to add a network?
If WifiNetworkSuggestion is not about adding a network, what is it really used for exactly?
Since I'm not familiar enough with the tidbits of adding a network, is my code correct about all possible ways to add a network? I ask this because someone wrote here that people should enable Wifi and make sure pingSupplicant returns true. Is it true? Or would it be enough to just call addNetwork ?
If it's now impossible to add a network using the normal API, is there maybe a solution by using a rooted device instead? Maybe some adb command?
EDIT: Not sure how to do it officially, but using adb, you might be able to add Wifi-networks on Android 11 . Need to check adb shell cmd wifi help .
I stuck with same issue, but somehow I reached a reproducible state for connecting a desired network and I want to share my findings it may helps.
As a summary:
You have to disable all auto connection before applying WifiNetworkSuggestion logic
For more details, Please read the following:
I used the following code (Similar to what you use):
private fun connectUsingNetworkSuggestion(ssid: String, password: String) {
val wifiNetworkSuggestion = WifiNetworkSuggestion.Builder()
.setSsid(ssid)
.setWpa2Passphrase(password)
.build()
// Optional (Wait for post connection broadcast to one of your suggestions)
val intentFilter =
IntentFilter(WifiManager.ACTION_WIFI_NETWORK_SUGGESTION_POST_CONNECTION);
val broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (!intent.action.equals(WifiManager.ACTION_WIFI_NETWORK_SUGGESTION_POST_CONNECTION)) {
return
}
showToast("Connection Suggestion Succeeded")
// do post connect processing here
}
}
registerReceiver(broadcastReceiver, intentFilter)
lastSuggestedNetwork?.let {
val status = wifiManager.removeNetworkSuggestions(listOf(it))
Log.i("WifiNetworkSuggestion", "Removing Network suggestions status is $status")
}
val suggestionsList = listOf(wifiNetworkSuggestion)
var status = wifiManager.addNetworkSuggestions(suggestionsList)
Log.i("WifiNetworkSuggestion", "Adding Network suggestions status is $status")
if (status == WifiManager.STATUS_NETWORK_SUGGESTIONS_ERROR_ADD_DUPLICATE) {
showToast("Suggestion Update Needed")
status = wifiManager.removeNetworkSuggestions(suggestionsList)
Log.i("WifiNetworkSuggestion", "Removing Network suggestions status is $status")
status = wifiManager.addNetworkSuggestions(suggestionsList)
}
if (status == WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS) {
lastSuggestedNetwork = wifiNetworkSuggestion
lastSuggestedNetworkSSID = ssid
showToast("Suggestion Added")
}
}
So here are the steps:
Install fresh version / Or remove all suggestion you added before
Make sure that you forgot all surrounding networks so your device won't auto-connect
Add wifi network suggestions list
Go to Wifi Settings to scan networks Or wait until next scan is running
A notification prompt will appear :
6. When you Press "Yes" the system will auto-connect with it via your app and internet will work normally. See the following:
Please note the following:
If you disconnect the network from Wifi Settings (i.e press disconnect bin icon in the following image) your network will be blocked for 24 hours from auto-connect even if you removed the suggested network using wifiManager.removeNetworkSuggestions(listOf(it)) and add it again. And even if you uninstall and install your app again
Unfortunately, this is limitation added by Android System as described here:
If the user uses the Wi-Fi picker to explicitly disconnect from one of the network suggestions when connected to it, then that network is blacklisted for 24 hours. During the blacklist period, that network will not be considered for auto-connection, even if the app removes and re-adds the network suggestion corresponding to the network.
If you uninstall the application while connected to suggested WiFi, the system will close the connection automatically.
In case you have multiple suggestion you can priorities them by using WifiNetworkSuggestion.Builder().setPriority(<Priority Integer>) as mentioned here:
Specify the priority of this network among other network suggestions provided by the same app (priorities have no impact on suggestions by different apps). The higher the number, the higher the priority (i.e value of 0 = lowest priority).
In case you pressed "No" in notification prompt, you can change it from (Settings > Apps & notifications > Special App access > Wi-Fi Control > App name) as described here:
A user declining the network suggestion notification removes the CHANGE_WIFI_STATE permission from the app. The user can grant this approval later by going into the Wi-Fi control menu (Settings > Apps & notifications > Special App access > Wi-Fi Control > App name).
I wish I had answers to all of your questions because I'm currently struggling with similar issues.
After many hours I was finally able to connect to the desired network using this approach:
val wifiNetworkSpecifier = WifiNetworkSpecifier.Builder()
.setSsid(ssid)
.setWpa2Passphrase(passphrase)
.setBssid(mac)
.build()
val networkRequest = NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.setNetworkSpecifier(wifiNetworkSpecifier)
.build()
val connectivityManager = applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?
connectivityManager?.requestNetwork(networkRequest, ConnectivityManager.NetworkCallback())
You can receive a whole host of events through the ConnectivityManager.NetworkCallback().
Looks like they've added support in Android 11(API 30) for adding network configuration that persists outside of the application scope and is saved as a system network configuration just like it was done with the deprecated WiFiManager method addNetwork. All you need to do is to use ACTION_WIFI_ADD_NETWORKS to show a system dialog that asks a user if he wants to proceed with adding a new Wifi suggestion to the system. This is how we start that dialog:
// used imports
import android.provider.Settings.ACTION_WIFI_ADD_NETWORKS
import android.provider.Settings.EXTRA_WIFI_NETWORK_LIST
import android.app.Activity
import android.content.Intent
import android.net.wifi.WifiNetworkSuggestion
// show system dialog for adding new network configuration
val wifiSuggestionBuilder = WifiNetworkSuggestion.Builder()
.setSsid("network SSID")
.build()
val suggestionsList = arraylistOf(wifiSuggestionBuilder)
val intent = new Intent(ACTION_WIFI_ADD_NETWORKS)
intent.putParcelableArrayListExtra(EXTRA_WIFI_NETWORK_LIST, suggestionsList);
activity.startActivityForResult(intent, 1000)
The dialog looks like this:
And then we just need to handle a result in onActivityResult method like this:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == 1000) {
if (resultCode == Activity.RESULT_OK) {
// network succesfully added - User pressed Save
} else if (resultCode == Activity.RESULT_CANCELED) {
// failed attempt of adding network to system - User pressed Cancel
}
}
}
But as I've tested this code on Android devices that have older Android versions(lower then API30) installed I've got a crash every time I want it to show that dialog for adding a new network configuration. This is the crash:
java.lang.RuntimeException: Unable to start activity: android.content.ActivityNotFoundException: No Activity found to handle Intent { act=android.settings.WIFI_ADD_NETWORKS (has extras) }
Looks like the new way is not back-supported out of the box. So, for API30 we can use a new Intent action, for API 28 and below we can still use the old way of adding Networks, but for API29 we have some kind of gray area where I was not able to find a good solution yet. If anyone has an idea what else to do please share it with me. ;)
#Sebastian Helzer's answer works for me. I use java in my application. This may help java users...
WifiNetworkSpecifier wifiNetworkSpecifier = new WifiNetworkSpecifier.Builder()
.setSsid(ssid)
.setWpa2Passphrase(password)
.build();
NetworkRequest networkRequest = new NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.setNetworkSpecifier(wifiNetworkSpecifier)
.build();
ConnectivityManager connectivityManager = (ConnectivityManager)this.getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
connectivityManager.requestNetwork(networkRequest, new ConnectivityManager.NetworkCallback());
I'm trying to build an app which gets battery level of currently connected Bluetooth headset. This app can be used on phones which don't have this functionality built-in.
While searching on stackoverflow, I found How to get Bluetooth Headset battery status in android this question. I got the currently connected Bluetooth headset using BluetoothProfile.HEADSET profile.
But in the device object of type BluetoothDevice I don't see any method or property to get battery level of Bluetooth Headset.
I can get the device name and isAudioConnected.
If question is about Bluetooth HFP feature: HF indicators feature is optional for the both sides. If the both sides support it, BluetoothHeadset will broadcast BluetoothHeadset.ACTION_HF_INDICATORS_VALUE_CHANGED with BluetoothHeadset.EXTRA_HF_INDICATORS_IND_ID equal 2 (Battery Level) and BluetoothHeadset.EXTRA_HF_INDICATORS_IND_VALUE with scope 0..100. Do not remember Android version were it was implemented, you should check it.
Also battery level can be implemented in device using vendor specific HFP AT commands (especially for old handsfree devices) and maybe BLE.
I found a solution, but it only works on android 8 and above
I took this code from here
Kotlin
fun getBatteryLevel(pairedDevice: BluetoothDevice?): Int {
return pairedDevice?.let { bluetoothDevice ->
(bluetoothDevice.javaClass.getMethod("getBatteryLevel"))
.invoke(pairedDevice) as Int
} ?: -1
}
The first thing to register BroadcastReciver by "android.bluetooth.device.action.BATTERY_LEVEL_CHANGED"
and you can receive this action by the broadcast receiver then get extra data by "android.bluetooth.device.extra.BATTERY_LEVEL"
and if you want to trigger this action, you need to reconnect your Bluetooth device or Bluetooth device battery level happened to change.
Good luck for you.
Connected AirPods Pro to OnePlus 5T with Android 9.
None of those registered events happen:
"android.bluetooth.device.action.BATTERY_LEVEL_CHANGED"
"android.bluetooth.headset.profile.action.AUDIO_STATE_CHANGED"
"android.bluetooth.headset.action.HF_INDICATORS_VALUE_CHANGED"
I am Able to achieve the handset battery Level in Java
try {
BluetoothDevice device = bluetoothAdapter.getRemoteDevice("Connected device ID");
java.lang.reflect.Method method;
method = device.getClass().getMethod("getBatteryLevel");
int value = (int) method.invoke(device);
result.success(value);
} catch (Exception ex) {
result.error("invalid_argument", "'deviceId' argument is required to be string", null);
break;
}
This is #Kirill Martyuk answer as an Extension variable
val BluetoothDevice.batteryLevel
get() = this.let { device ->
val method = device.javaClass.getMethod("getBatteryLevel")
method.invoke(device) as Int?
} ?: -1
Usage would be something like
val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager?
val adapter = manager?.adapter
val devices = adapter?.bondedDevices.orEmpty()
devices.forEach { device ->
Log.d("DEVICE_NAME", device.name)
Log.d("CHARGE_LEVEL", device.batteryLevel.toString())
}