Android: How to detect Bluetooth connection status - android

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() }
}
}

Related

how do i get the set notification in BLE nordic

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")
}
}
`

How to connect BT barcode reader correctly?

I have two type of BT reader on of them is a Symbol CS3070 and the other one is a Datalogic DBT6400.
For the DBT6400 device I found an Android SDK with which the connection is very simple. But for the first one there is no supported SDK.
So I decided to connect to these device directly without SDK. Everything works fine,until the device disconnected because of sleep mode or because of I switch from one to another device.
In this case I get a lot of exception inside the while loop, because of the socket is closed. I have a tip for this why it's happen, maybe because of the coroutine, if it is possible.
UPDATE: So I found the problem, a put the catch block this line of code: reader=0, and there is only 1 error, so it's seems good. But I still don't know, is it a good solution?
My CustomReader class
class CustomReader(
private val deviceName: String,
onBarcodeRead: (barcode: String?) -> Unit
) : BarcodeReader(onBarcodeRead), CoroutineScope {
private val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
private var pairedDevices: Set<BluetoothDevice>? = null
private var device: BluetoothDevice? = null
private lateinit var uuid: UUID
private var socket: BluetoothSocket? = null
private var connected: Boolean = false
private val localJob: Job = Job()
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + localJob
override fun init(context: Context) {
pairedDevices = bluetoothAdapter.bondedDevices
}
override fun connect(address: String) {
try {
bluetoothAdapter.startDiscovery()
uuid = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")
device = pairedDevices?.firstOrNull { x -> x.name.startsWith(deviceName) }
if(device == null){
Timber.d("CustomReader: $deviceName: Device not found")
return
}
initSocket()
launch {
run()
}
} catch (e: Exception) {
Timber.e(e)
}
}
override fun disconnect() {
try {
socket?.close()
socket = null
} catch (e: IOException) {
Timber.d("CustomReader: $deviceName: Socket closed")
}
localJob.cancel()
}
override fun destroy() {
try {
socket?.close()
socket = null
} catch (e: IOException) {
Timber.d("CustomReader: $deviceName: Socket closed")
}
localJob.cancel()
}
override fun getAddress(): String? {
return device?.address
}
override fun isConnected(): Boolean {
return connected
}
private suspend fun run() {
try {
socket?.let { socket ->
socket.connect()
connected = true
val buffer = ByteArray(1024)
var read = 0
do {
try {
read = socket.inputStream.read(buffer)
val data = String(buffer, 0, read)
onBarcodeRead.invoke(data)
} catch (e: Exception) {
Timber.e(e)
}
} while (read > 0)
CoroutineScope()
}
}catch (e: Exception){
Timber.d("CustomReader: $deviceName Disconnected")
connected = false
}
}
fun initSocket(){
if(socket != null)
return
socket = device?.createRfcommSocketToServiceRecord(uuid)
bluetoothAdapter.cancelDiscovery()
}
fun closeSocket(){
try {
socket?.close()
}catch (e: Exception){
Timber.d("CustomReader: $deviceName: Socket closed")
}
}
}
**BletoothHandlar class:**
object BluetoothHandler {
#JvmStatic
private var currentReader: BarcodeReader? = null
#JvmStatic
private val barcodeLiveData = MutableLiveData<EventWrapper<String?>>()
#JvmStatic
fun getData(): LiveData<EventWrapper<String?>> = barcodeLiveData
init {
fixedRateTimer(
name = "blueooth-reconnect",
daemon = true,
initialDelay = 0L,
period = 5000
) {
if (currentReader == null) {
Timber.d("No reader device connected")
return#fixedRateTimer
}
if (currentReader?.isConnected() == true) {
Timber.d("reader connected, doing nothing")
} else if (currentReader?.isConnected() == false) {
Timber.d("reader not connected, reconnecting")
currentReader?.connect(currentReader!!.getAddress()!!)
}
}
}
#JvmStatic
fun connectToDevice(device: BluetoothDeviceCustom, context: Context): BarcodeReader? {
if (device.address == currentReader?.getAddress() && currentReader?.isConnected() == true) {
Timber.d("device already connected")
return currentReader
}
//ha változott a kiválasztott olvasó
if (device.address != currentReader?.getAddress()) {
currentReader?.disconnect()
}
val reader = createReader(device)
if (reader == null) {
currentReader?.disconnect()
} else {
reader.init(context)
reader.connect(device.address)
}
currentReader = reader
return reader
}
#JvmStatic
fun disconnect() {
currentReader?.disconnect()
currentReader?.destroy()
currentReader = null
}
#JvmStatic
fun getConnectedDevice(): BarcodeReader? {
return currentReader
}
#JvmStatic
private fun createReader(bluetoothDeviceCustom: BluetoothDeviceCustom): BarcodeReader? {
return CustomReader(bluetoothDeviceCustom.name) {
barcodeLiveData.postValue(EventWrapper(it))
}
/*return if (bluetoothDeviceCustom.name.startsWith("DBT6400")) {
DatalogicDBT6400 {
barcodeLiveData.postValue(EventWrapper(it))
}
} else if (bluetoothDeviceCustom.name.startsWith("CS3070")) {
CustomReader(bluetoothDeviceCustom.name) {
barcodeLiveData.postValue(EventWrapper(it))
}
} else {
null
}*/
}
}
So, here it is my solution, I don't think it is the best, but it works.
const val UUID_STRING = "00001101-0000-1000-8000-00805F9B34FB"
class CustomReader(
onBarcodeRead: (barcode: String?) -> Unit
) : BarcodeReader(onBarcodeRead), CoroutineScope {
private val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
private var pairedDevices: Set<BluetoothDevice>? = null
private var device: BluetoothDevice? = null
private var socket: BluetoothSocket? = null
private val localJob: Job = Job()
private var deviceName = ""
private val uuid = UUID.fromString(UUID_STRING)
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + localJob
override fun init(context: Context) {
pairedDevices = bluetoothAdapter.bondedDevices
}
private val singleExecutor: ExecutorCoroutineDispatcher =
Executors.newFixedThreadPool(1).asCoroutineDispatcher()
override fun connect(address: String) {
try {
device = pairedDevices?.firstOrNull { x -> x.address == address }
if(device == null){
Timber.d("CustomReader: $deviceName: Device not found")
return
}
deviceName = device?.name ?: "N/A"
initSocket()
launch {
withContext(singleExecutor){
run()
}
}
} catch (e: Exception) {
Timber.e(e)
}
}
override fun disconnect() {
closeSocket()
localJob.cancel()
}
override fun getAddress(): String? {
return device?.address
}
override fun isConnected(): Boolean {
return socket?.isConnected ?: false
}
private suspend fun run() {
try {
socket?.let { socket ->
socket.connect()
val buffer = ByteArray(1024)
var read = 0
do {
try {
read = socket.inputStream.read(buffer)
val data = String(buffer, 0, read)
onBarcodeRead.invoke(data)
} catch (e: Exception) {
Timber.d("CustomReader: $deviceName Socket disconnected")
read = -1
closeSocket()
}
} while (read > 0)
}
}catch (e: Exception){
Timber.d("CustomReader : $deviceName Disconnected")
}
}
private fun closeSocket(){
try {
socket?.close()
}catch (e: IOException){
Timber.d("CustomReader: $deviceName Socket closed")
}
socket = null
}
private fun initSocket(){
if(socket != null)
return
socket = device?.createRfcommSocketToServiceRecord(uuid)
}
}

BLE - Bluetooth GATT service can't close connection

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
}
}

Android NsdManager not able to discover services

I'm running into a problem with Androids NsdManager when following their tutorial Using Network Service Discovery.
I have a few zeroconf/bonjour hardware devices on my network. From my mac I can discover all of them as expected from my terminal with the following
dns-sd -Z _my-mesh._tcp.
From my Android app's first run I can flawlessly discover these services using NsdManager. However if I restart the application and try again none of the services are found. onDiscoveryStarted gets called successfully but then nothing else after. While waiting I can confirm from my mac that the services are still successfully there.
I can then turn on my Zeroconf app (on Android) and it will show the services like my mac. When I return to my app I see it immediately receive all the callbacks I expected previously. So I believe something is wrong with my approach, however I'm not sure what. Below is the code I use to discover and resolve services. The view is a giant textview (in a scroll view) I keep writing text to for debugging easier.
import android.annotation.SuppressLint
import android.content.Context
import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.widget.TextView
class MainActivity : AppCompatActivity(),
NsdManager.DiscoveryListener {
private var nsdManager: NsdManager? = null
private var text: TextView? = null
private var isResolving = false
private val services = ArrayList<ServiceWrapper>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
this.text = findViewById(R.id.text)
this.nsdManager = application.getSystemService(Context.NSD_SERVICE) as NsdManager
}
override fun onResume() {
super.onResume()
this.nsdManager?.discoverServices("_my-mesh._tcp.", NsdManager.PROTOCOL_DNS_SD, this)
write("Resume Discovering Services")
}
override fun onPause() {
super.onPause()
this.nsdManager?.stopServiceDiscovery(this)
write("Pause Discovering Services")
}
override fun onServiceFound(serviceInfo: NsdServiceInfo?) {
write("onServiceFound(serviceInfo = $serviceInfo))")
if (serviceInfo == null) {
return
}
add(serviceInfo)
}
override fun onStopDiscoveryFailed(serviceType: String?, errorCode: Int) {
write("onStopDiscoveryFailed(serviceType = $serviceType, errorCode = $errorCode)")
}
override fun onStartDiscoveryFailed(serviceType: String?, errorCode: Int) {
write("onStartDiscoveryFailed(serviceType = $serviceType, errorCode = $errorCode)")
}
override fun onDiscoveryStarted(serviceType: String?) {
write("onDiscoveryStarted(serviceType = $serviceType)")
}
override fun onDiscoveryStopped(serviceType: String?) {
write("onDiscoveryStopped(serviceType = $serviceType)")
}
override fun onServiceLost(serviceInfo: NsdServiceInfo?) {
write("onServiceLost(serviceInfo = $serviceInfo)")
}
private fun createResolveListener(): NsdManager.ResolveListener {
return object : NsdManager.ResolveListener {
override fun onResolveFailed(serviceInfo: NsdServiceInfo?, errorCode: Int) {
write("onResolveFailed(serviceInfo = $serviceInfo, errorCode = $errorCode)")
isResolving = false
resolveNext()
}
override fun onServiceResolved(serviceInfo: NsdServiceInfo?) {
write("onServiceResolved(serviceInfo = $serviceInfo)")
if (serviceInfo == null) {
return
}
for (servicewrapper in services) {
if (servicewrapper.serviceInfo.serviceName == serviceInfo.serviceName) {
servicewrapper.resolve(serviceInfo)
}
}
isResolving = false
resolveNext()
}
}
}
#SuppressLint("SetTextI18n")
private fun write(text: String?) {
this.text?.let {
it.post({
it.text = it.text.toString() + "\n" + text + "\n"
})
}
}
fun add(serviceInfo: NsdServiceInfo) {
for (servicewrapper in services) {
if (servicewrapper.serviceInfo.serviceName == serviceInfo.serviceName) {
return
}
}
services.add(ServiceWrapper(serviceInfo))
resolveNext()
}
#Synchronized
fun resolveNext() {
if (isResolving) {
return
}
isResolving = true
for (servicewrapper in services) {
if (servicewrapper.isResolved) {
continue
}
write("resolving")
this.nsdManager?.resolveService(servicewrapper.serviceInfo, createResolveListener())
return
}
isResolving = false
}
inner class ServiceWrapper(var serviceInfo: NsdServiceInfo) {
var isResolved = false
fun resolve(serviceInfo: NsdServiceInfo) {
isResolved = true
this.serviceInfo = serviceInfo
}
}
}
Better late than never. Did not realize other people were having this issue too until now.
What we discovered was some routers were blocking or not correctly forwarding the packets back and forth. Our solution to this was using wire shark to detect what other popular apps were doing to get around the issue. Androids NsdManager has limited customizability so it required manually transmitting the packet over a MulticastSocket.
interface NsdDiscovery {
suspend fun startDiscovery()
suspend fun stopDiscovery()
fun setListener(listener: Listener?)
fun isDiscovering(): Boolean
interface Listener {
fun onServiceFound(ip:String, local:String)
fun onServiceLost(event: ServiceEvent)
}
}
#Singleton
class ManualNsdDiscovery #Inject constructor()
: NsdDiscovery {
//region Fields
private val isDiscovering = AtomicBoolean(false)
private var socketManager: SocketManager? = null
private var listener: WeakReference<NsdDiscovery.Listener> = WeakReference<NsdDiscovery.Listener>(null)
//endregion
//region NsdDiscovery
override suspend fun startDiscovery() = withContext(Dispatchers.IO) {
if (isDiscovering()) return#withContext
this#ManualNsdDiscovery.isDiscovering.set(true)
val socketManager = SocketManager()
socketManager.start()
this#ManualNsdDiscovery.socketManager = socketManager
}
override suspend fun stopDiscovery() = withContext(Dispatchers.IO) {
if (!isDiscovering()) return#withContext
this#ManualNsdDiscovery.socketManager?.stop()
this#ManualNsdDiscovery.socketManager = null
this#ManualNsdDiscovery.isDiscovering.set(false)
}
override fun setListener(listener: NsdDiscovery.Listener?) {
this.listener = WeakReference<NsdDiscovery.Listener>(listener)
}
#Synchronized
override fun isDiscovering(): Boolean {
return this.isDiscovering.get()
}
//endregion
private inner class SocketManager {
//region Fields
private val group = InetAddress.getByName("224.0.0.251")
?: throw IllegalStateException("Can't setup group")
private val incomingNsd = IncomingNsd()
private val outgoingNsd = OutgoingNsd()
//endregion
//region Constructors
//endregion
//region Methods
suspend fun start() {
this.incomingNsd.startListening()
this.outgoingNsd.send()
}
fun stop() {
this.incomingNsd.stopListening()
}
//endregion
private inner class OutgoingNsd {
//region Fields
private val socketMutex = Mutex()
private var socket = MulticastSocket(5353)
suspend fun setUpSocket() {
this.socketMutex.withLock {
try {
this.socket = MulticastSocket(5353)
this.socket.reuseAddress = true
this.socket.joinGroup(group)
} catch (e: SocketException) {
return
}
}
}
suspend fun tearDownSocket() {
this.socketMutex.withLock {
this#OutgoingNsd.socket.close()
}
}
//ugly code but here is the packet
private val bytes = byteArrayOf(171.toByte(), 205.toByte(), 1.toByte(), 32.toByte(),
0.toByte(), 1.toByte(), 0.toByte(), 0.toByte(),
0.toByte(), 0.toByte(), 0.toByte(), 0.toByte(),
9.toByte(), 95.toByte(), 101.toByte(), 118.toByte(),
97.toByte(), 45.toByte(), 109.toByte(), 101.toByte(),
115.toByte(), 104.toByte(), 4.toByte(), 95.toByte(),
116.toByte(), 99.toByte(), 112.toByte(), 5.toByte(),
108.toByte(), 111.toByte(), 99.toByte(), 97.toByte(),
108.toByte(), 0.toByte(), 0.toByte(), 12.toByte(),
0.toByte(), 1.toByte())
private val outPacket = DatagramPacket(bytes,
bytes.size,
this#SocketManager.group,
5353)
//endregion
//region Methods
#Synchronized
suspend fun send() {
withContext(Dispatchers.Default) {
setUpSocket()
try {
this#OutgoingNsd.socket.send(this#OutgoingNsd.outPacket)
delay(1500L)
tearDownSocket()
} catch (e: Exception) {
}
}
}
//endregion
}
private inner class IncomingNsd {
//region Fields
private val isRunning = AtomicBoolean(false)
private var socket = MulticastSocket(5353)
//endregion
//region Any
fun setUpSocket() {
try {
this.socket = MulticastSocket(5353)
this.socket.reuseAddress = true
this.socket.joinGroup(group)
} catch (e: SocketException) {
} catch (e: BindException) {
}
}
fun run() {
GlobalScope.launch(Dispatchers.Default) {
setUpSocket()
try {
while (this#IncomingNsd.isRunning.get()) {
val bytes = ByteArray(4096)
val inPacket = DatagramPacket(bytes, bytes.size)
this#IncomingNsd.socket.receive(inPacket)
val incoming = DNSIncoming(inPacket)
for (answer in incoming.allAnswers) {
if (answer.key.contains("_my_mesh._tcp")) {
this#ManualNsdDiscovery.listener.get()?.onServiceFound(answer.recordSource.hostAddress, answer.name)
return#launch
}
}
}
this#IncomingNsd.socket.close()
} catch (e: Exception) {
}
}
}
//endregion
//region Methods
#Synchronized
fun startListening() {
if (this.isRunning.get()) {
return
}
this.isRunning.set(true)
run()
}
#Synchronized
fun stopListening() {
if (!this.isRunning.get()) {
return
}
this.isRunning.set(false)
}
//endregion
}
}
}

how to set color of RecyclerView.ViewHolder in BroadcastReceiver.onReceive?

i am creating an app that scans for and pairs to Bluetooth devices. i am displaying the devices in a RecyclerView and indicating bond state by coloring the ViewHolder for that device. my problem is that the color of the ViewHolder is only changed after scanning for devices again and i want it to immediately update the color on pair or unpair. i am attempting to do this through use of a broadcast receiver but i am unable to get a reference to the correct ViewHolder. how can i achieve this? i am including my code below for my RecyclerView.Adapter and my BluetoothUtils file containing the broadcast receiver. thanks in advance. my adapter:
class DeviceAdapter(val mContext : Context) : RecyclerView.Adapter<DeviceAdapter.DeviceHolder>() {
companion object {
val TAG = "Device Adapter"
fun DeviceHolder.setColor(bonded: Boolean):Unit{
val background = if (bonded)Color.CYAN else Color.TRANSPARENT
this.itemView.setBackgroundColor(background)
}
}
val mDevices = ArrayList<BluetoothDevice>()
fun updateItems(list: ArrayList<BluetoothDevice>) {
mDevices.clear()
mDevices.addAll(list)
Log.d(TAG, "updating items : $mDevices")
notifyDataSetChanged()
}
fun ViewGroup.inflate(#LayoutRes res: Int, attachToRoot: Boolean = false): View {
return LayoutInflater.from(mContext).inflate(res, this, attachToRoot)
}
override fun onBindViewHolder(holder: DeviceHolder, position: Int) {
Log.d(TAG, "onBindViewHolder called!")
holder.bindItems(mDevices.get(position))
if (mDevices.get(position).bondState==BluetoothDevice.BOND_BONDED) {
holder.itemView.setBackgroundColor(CYAN)
} else {
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
}
}
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): DeviceAdapter.DeviceHolder {
Log.d(TAG, "onCreateViewHolder called!")
val v = parent!!.inflate(R.layout.device_item, false)
return DeviceHolder(v)
}
override fun getItemCount(): Int {
return mDevices.size
}
inner class DeviceHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val nameView = itemView.findViewById(R.id.nameView) as TextView
val addrView = itemView.findViewById(R.id.addressView) as TextView
var dialog: AlertDialog? = null;
fun bindItems(btDevice: BluetoothDevice) {
Log.d(TAG, "holder created!")
nameView.text = btDevice.name ?: "Unknown"
addrView.text = btDevice.address
itemView.setOnClickListener {
dialog = AlertDialog.Builder(it.context)
.setTitle("Options")
.setView(R.layout.options_dialog_layout)
.setNegativeButton("Cancel", DialogInterface.OnClickListener { _, which -> })
.create()
dialog!!.show()
val ops = listOf(
dialog!!.findViewById(R.id.statOp),
dialog!!.findViewById(R.id.pairOp),
dialog!!.findViewById(R.id.connectOp),
dialog!!.findViewById(R.id.sendOp),
dialog!!.findViewById(R.id.unPairOp)
)
ops.forEach { it.setOnClickListener {
Toast.makeText(it.context, it.id.toString(), Toast.LENGTH_SHORT).show()
when(it.id){
R.id.statOp -> {}
R.id.connectOp -> {
Log.d(TAG, "connectOp reached")
BluetoothReflection.connectDevice(btDevice)
dialog!!.dismiss()
}// BluetoothUtils.connect(BluetoothAdapter.getDefaultAdapter(), btDevice)
R.id.pairOp -> {
Log.d(TAG, "pairOp reached")
BluetoothUtils.startPair(BluetoothAdapter.getDefaultAdapter(), btDevice)
if (btDevice.bondState==BluetoothDevice.BOND_BONDED){
this#DeviceHolder.itemView.setBackgroundColor(CYAN) //doesn't work
}
Log.d(TAG, "start pair complete")
dialog!!.dismiss()
}//
R.id.unPairOp -> {//no executable code found here
Log.d(TAG, "unPairOp reached")
BluetoothUtils.unPair(btDevice)
if (btDevice.bondState==BluetoothDevice.BOND_NONE){
this#DeviceHolder.itemView.setBackgroundColor(Color.TRANSPARENT) //doesn't work
}
Log.d(TAG, "unpair complete")
dialog!!.dismiss()
}
R.id.sendOp -> {}
}
} }
}
}
}
}
and my BluetoothUtils:
class BluetoothUtils {
companion object {
var listener: ListenThread? = null
val _UUID = UUID.fromString("a0e7e4c7-0e4e-43b7-9d18-659192512164")
val TAG = "BluetoothUtils"
val receiver = MainBTStatusReceiver()
fun initPairingServer(adapter: BluetoothAdapter){
var mmServerSocket: BluetoothServerSocket?
try {
var tmp = adapter.listenUsingRfcommWithServiceRecord(TAG, _UUID)
mmServerSocket = tmp
listener = ListenThread(mmServerSocket)
listener!!.start()
}catch (ioe: IOException){
Log.e(TAG, "Error initializing Bluetooth", ioe)
}
}
fun cancelListener() = listener!!.cancel()
fun connect(adapter: BluetoothAdapter, device: BluetoothDevice){
var btSocket: BluetoothSocket?
try {
adapter.cancelDiscovery()
btSocket = device.createRfcommSocketToServiceRecord(_UUID)
PairingThread(btSocket).start()
}catch (ioe: IOException){
Log.e(TAG, "error connecting", ioe)
}
}
fun startPair(adapter: BluetoothAdapter, device: BluetoothDevice): Unit{
adapter.cancelDiscovery()
Log.d(TAG, device.bondState.toString())
device.createBond()
}
fun unPair(device: BluetoothDevice): Any = device::class.java.getMethod("removeBond").invoke(device)
}
}
class ListenThread(val btServSock: BluetoothServerSocket) : Thread(){
companion object {
val TAG = "ListenThread"
}
var btSocket: BluetoothSocket? = null
override fun run() {
super.run()
while (true){
try {
Log.d(TAG, "listening . . . ")
btSocket = btServSock.accept()
}catch (ioe: IOException){
Log.e(TAG, "Error", ioe) // SHOULD HANDLE FAILURE OF LISTENER INSTANTIATION
break
}
//manage connection here
//with either BluetoothUtils function
//or BluetoothSocket extension
}
}
fun cancel() = btServSock.close()
}
class PairingThread(val btSocket: BluetoothSocket) : Thread(){
companion object {
val TAG = "Pairing Thread"
}
override fun run() {
super.run()
try {
Log.d(TAG, "attempting to connect")
btSocket.connect()
}catch (ioe: IOException){
Log.e(TAG, "error connecting", ioe)
btSocket.close()
}
}
}
class MainBTStatusReceiver(): BroadcastReceiver(){
val TAG = "MainBTStatusReceiver"
var mAdapter: DeviceAdapter? = null
fun setAdapter(adapter: DeviceAdapter){
mAdapter = adapter
}
override fun onReceive(context: Context?, intent: Intent) {
val action = intent.action
val devExtra = intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE) as BluetoothDevice
when(action){
BluetoothDevice.ACTION_BOND_STATE_CHANGED -> {
val device = intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
when(device.bondState){
BluetoothDevice.BOND_BONDED -> {
Log.d(TAG, "BONDED")
}
BluetoothDevice.BOND_BONDING -> {Log.d(TAG, "BONDING")}
BluetoothDevice.BOND_NONE -> {Log.d(TAG, "NONE")}
}
}
}
}
Add boolean or int to BluetoothDevice model for managing view.
For example,
BluetoothDevice: added isOn state. (Sorry, it's Java)
class BluetoothDevice {
boolean isOn;
public boolean isOn() {
return isOn;
}
public void setOn(boolean isOn) {
this.isOn = isOn;
}
}
DeviceHolder: changed color of view
fun bindItems(btDevice: BluetoothDevice) {
stateView.textColor = btDevice.isOn() ? Color.RED : Color.GREEN
}
DeviceAdapter: added getItems
fun getItems() {
return mDevices
}
If you want to change isOn state, change model and notify it.
adapter.getItems().get(i).setOn(true);
adapter.notifyDataSetChanged();
I like the above answer as well but the way that i got this done was to pass the BroadcastReceiver into the DeviceAdapter :
class DeviceAdapter(val mContext:Context, val mReceiver:MainBTStatusReceiver) : RecyclerView.Adapter<DeviceAdapter.DeviceHolder>()
and then made a ViewHolder a member of the BroadcastReceiver and a setter function for the ViewHolder named setFocus. before calling any functions from the BluetoothUtils class i called the setFocus function and then the broadcast receiver modifies the color of the view that focus is currently set too. I do have some concern that this might not be reliable as the most accurate method to modify the correct ViewHolder every time. if you see any problem with this please comment to let me know.
my updated BroadcastReceiver:
class MainBTStatusReceiver(): BroadcastReceiver() {
val TAG = "MainBTStatusReceiver"
var holder: DeviceAdapter.DeviceHolder? = null
fun setFocus(holder: DeviceAdapter.DeviceHolder) {
this.holder = holder
}
override fun onReceive(context: Context?, intent: Intent) {
val action = intent.action
when (action) {
BluetoothDevice.ACTION_BOND_STATE_CHANGED -> {
val device = intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
when (device.bondState) {
BluetoothDevice.BOND_BONDED -> {
holder!!.itemView.setBackgroundColor(Color.CYAN)
Log.d(TAG, "BONDED")
}
BluetoothDevice.BOND_BONDING -> {
Log.d(TAG, "BONDING")
}
BluetoothDevice.BOND_NONE -> {
holder!!.itemView.setBackgroundColor(Color.TRANSPARENT)
Log.d(TAG, "NONE")
}
}
}
}
}
}
and the two statements in my when expression that call setFocus():
R.id.pairOp -> {
Log.d(TAG, "pairOp reached")
mReceiver.setFocus(this#DeviceHolder)
BluetoothUtils.startPair(BluetoothAdapter.getDefaultAdapter(), btDevice)
Log.d(TAG, "start pair complete")
dialog!!.dismiss()
}
R.id.unPairOp -> {
Log.d(TAG, "unPairOp reached")
mReceiver.setFocus(this#DeviceHolder)
BluetoothUtils.unPair(btDevice)
Log.d(TAG, "unpair complete")
dialog!!.dismiss()
}

Categories

Resources