Using CompanionDeviceManager to read the devices information - android

I am using BluetoothLeScanner to scan for BLE devices and get a list of objects representing the devices to show inside my app (not connecting to any of them).
I am interested in doing the same, but using the CompanionDeviceManager now. Its callback CompanionDeviceManager.Callback.onDeviceFound(chooserLauncher: IntentSender?) unfortunately does not return any human readable form of found devices... the closest it gets is the IntentSender.writeToParcel method, but I am not sure how to use it in this situation.
I am not constrained to use the CompanionDeviceManager but I wanted to follow the OS version specific guidelines, we are supposed to use CompanionDeviceManager for Bluetooth devices scanning starting from API 26, but it seems useless in my case... so is there any way to get devices data from that callback, or should I just ditch it and stay with BluetoothLeScanner for all OS versions?

Late answer but it might help someone else. You can create a bluetooth device picker in combination with ActivityResultContracts.StartIntentSenderForResult() in order to get the BluetoothDevice. From there you will have access to all the device info that you need. Recent changes added some Android 12 permissions like android.permission.BLUETOOTH_CONNECT. Your mileage may vary.
val context = LocalContext.current
// Get the device manager instance
val deviceManager: CompanionDeviceManager by lazy {
ContextCompat.getSystemService(
context,
CompanionDeviceManager::class.java
) as CompanionDeviceManager
}
// Create a filter of your choice. Here I just look for specific device names
val deviceFilter: BluetoothDeviceFilter by lazy {
BluetoothDeviceFilter.Builder()
.setNamePattern(Pattern.compile(supportedDevices))
.build()
}
// Create a pairing request with your filter from the last step
val pairingRequest: AssociationRequest = AssociationRequest.Builder()
.addDeviceFilter(deviceFilter)
.build()
// Create a picker for discovered bluetooth devices
val bluetoothDevicePicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartIntentSenderForResult(),
onResult = {
val device: BluetoothDevice? =
it.data?.getParcelableExtra(CompanionDeviceManager.EXTRA_DEVICE)
try {
// Now that you have the desired device, do what you need to with it
device?.apply {
when {
name?.matches(Regex(firstDevicePattern)) == true -> {
Log.i(TAG, "${this.name} connected")
onFirstDeviceDiscovered(device)
}
name?.matches(Regex(secondDevicePattern)) == true -> {
Log.i(TAG, "${this.name} connected")
onSecondDeviceDiscovered(device)
}
}
}
} catch (e: SecurityException) {
e.printStackTrace()
//TODO: handle the security exception (this is possibly a bug)
// https://issuetracker.google.com/issues/198986283
}
}
)
// A utility function to centralize calling associate (optional)
val associateDevice: (AssociationRequest) -> Unit = { request ->
// Attempt to associate device(s)
deviceManager.associate(
request,
object : CompanionDeviceManager.Callback() {
override fun onDeviceFound(chooserLauncher: IntentSender) {
val sender = IntentSenderRequest.Builder(chooserLauncher)
.build()
bluetoothDevicePicker.launch(sender)
}
override fun onFailure(error: CharSequence?) {
//TODO: handle association failure
}
}, null
)
}

Related

USB host bulkTransfer returns valid amount of data but I don't get anything back

I have an external camera that's connected via a USB C dongle to my Android tablet. My goal is to have a constant stream of data from the camera into my phone, showing it to the user and allowing him to record it and save it to the local storage.
I am following the official docs from the following link -
https://developer.android.com/guide/topics/connectivity/usb/host#working-d
And I have spent the last couple of hours trying to figure out how things work, mapping the interfaces and endpoints, eventually finding an interface that has an endpoint that when I call bulkTransfer() on, does not return a failed value (-1).
I currently am facing 2 issues:
I have indeed got a valid response from the bulkTransfer() function, but my ByteArray does not fill with relevant information - when trying to print out the values they are all 0's. I though it may be a wrong endpoint as suggested in the official docs, but I have tried all combinations of interfaces and endpoints until I get an indexOutOfBoundException. That combination of interface + endpoint that I used is the only one that produced a valid bulk response. What am I missing?
I am looking for a stream of data that doesn't stop, but it seems like when calling bulkTransfer() it's one a one time oppression, unlike CameraX library for example that I get a constant callback each time a new chunck of data is available.
Here is the code on my main screen -
LaunchedEffect(key1 = true) {
val usbManager = context.getSystemService(Context.USB_SERVICE) as UsbManager
val filter = IntentFilter(ACTION_USB_PERMISSION)
registerReceiver(context, UsbBroadcastReceiver(), filter, RECEIVER_NOT_EXPORTED)
val hdCamera = usbManager.deviceList.values.find { device ->
val name = device.productName ?: return#LaunchedEffect
name.contains("HD camera")
} ?: return#LaunchedEffect
val permissionIntent = PendingIntent.getBroadcast(
context,
0, Intent(ACTION_USB_PERMISSION),
0
)
usbManager.requestPermission(hdCamera, permissionIntent)
}
And here is my BroadcastReceiver -
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action != ACTION_USB_PERMISSION) return
synchronized(this) {
val usbManager = context?.getSystemService(Context.USB_SERVICE) as UsbManager
val device: UsbDevice? = intent.getParcelable(UsbManager.EXTRA_DEVICE)
val usbInterface = device?.getInterface(0)
val endpoint = usbInterface?.getEndpoint(1) ?: return#synchronized
usbManager.openDevice(device)?.apply {
val array = ByteArray(endpoint.maxPacketSize)
claimInterface(usbInterface, true)
val bulkTransfer = bulkTransfer(endpoint, array, array.size, 0)
Log.d("defaultAppDebuger", "bulk array: $bulkTransfer") //prints a valid number - 512
array.forEach {
Log.d("defaultAppDebuger", "bulk array: $it") //the array values are empty
}
}
}
}
edit:
I have tried to move the BroadcastReceiver code to an async coroutine thinking that the loading of the information is related to the fact that I am in the wrong thread. Still didn't work, I get a valid result from the bulkTransfer and the byteArray is not filled -
fun BroadcastReceiver.goAsync(
context: CoroutineContext = Dispatchers.IO,
block: suspend CoroutineScope.() -> Unit
) {
val pendingResult = goAsync()
CoroutineScope(SupervisorJob()).launch(context) {
try {
block()
} finally {
pendingResult.finish()
}
}
}
override fun onReceive(context: Context?, intent: Intent?) = goAsync { .... }
Thanks!
After carefully researching I was not able to get an answer and ditched that mini project that I worked on. I followed this comment on the following thread -
https://stackoverflow.com/a/68120774/8943516
That, combined with a 2.5 days of deep researched of both USB Host protocol which was not able to connect to my camera and Camera2API which couldn't recognize my external camera brought me to a dead end.

Send data from my App to Stm32 bluetooth Device - Kotlin

i have an application, and my application can connect to a bluetooth device.
After that, i want to send message (Int) to my Blutooth Low Energy device.
I have this code, but i can't figure it out what is the problem.
If you want i have : Characteristic UUID, Service UUID.
Really, i need your help...
I've edited the question :
My code :
val filter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
lateinit var bluetoothAdapter: BluetoothAdapter
val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
bluetoothAdapter = bluetoothManager.adapter
settingViewModel.bluetooth(bluetoothAdapter = bluetoothAdapter)
val mReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent) {
val action = intent.action
if (action == BluetoothAdapter.ACTION_STATE_CHANGED) {
val state = intent.getIntExtra(
BluetoothAdapter.EXTRA_STATE,
BluetoothAdapter.ERROR
)
when (state) {
BluetoothAdapter.STATE_OFF -> {
settingViewModel.setIsConnected(false)
//settingViewModel.stopScan()
settingViewModel.setListDevices(null)
}
BluetoothAdapter.STATE_ON -> {
settingViewModel.setIsConnected(true)
//scan()
settingViewModel.setListDevices(bluetoothAdapter.bondedDevices)
context!!.unregisterReceiver(this)
}
}
}
}
}
context.registerReceiver(mReceiver, filter)
val SERVICE_UUID = "00000000-0001-11e1-9ab4-0002a5d5c51c"
val ConfigCharacteristic = descriptorOf(
service = SERVICE_UUID,
characteristic = "00E00000-0001-11e1-ac36-0002a5d5c51b",
descriptor = "00000000-0000-0000-0000-000000000000",
)
Button(
onClick = {
if (settingViewModel.isConnected.value == true) {
coroutine.launch(Dispatchers.IO) {
try {
settingViewModel.peripheral.write(ConfigCharacteristic, byteArrayOf(1))
} catch (e: Exception) {
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
}
}
}
// try {
// val Service =
// settingViewModel.deviceSocket.value.get .getService(UUID.fromString("0000ffe0-0000-1000-8000-00805f9b34fb"))
// val charac: BluetoothGattCharacteristic =
// Service.getCharacteristic(UUID.fromString("00E00000-0001-11e1-ac36-0002a5d5c51b"))
// settingViewModel.deviceSocket.value!!.outputStream.write("1".toByteArray())
// } catch (e: Exception) {
// Toast.makeText(context, e.message.toString(), Toast.LENGTH_LONG).show()
// }
}
) {
Text(text = "HelloWorld")
}
I Already have the mac adress, the caracteristic and the service UUID of the device i want to connect to.
Again, i really need your help
First of all:
When developing an app for a BLE device it is best to first use a generic BLE scanner app to test the connection and to find out which commands need to be sent. If you confirm that the BLE device works as expected you can continue with your own custom app. I would recommend nRF Connect for this task.
Regarding your problem:
There are still many things missing from your sourcecode. You said you can connect to the device but have problems sending a message. Your code does not contain anything related to a BLE connection so I can only assume that you connected to the device using the Bluetooth settings of your phone. This would be correct for Bluetooth Classic but BLE requires you to connect through your own custom app.
The Ultimate Guide to Android Bluetooth Low Energy explains all steps necessary for a successful BLE connection. These steps are:
Setting the correct permissions
Scan for nearby BLE devices
Connect to a BLE device of your choosing
Scan for Services
Read and Write a characteristic of your choosing
All these steps are explained in the Guide using Kotlin as programming language.

Combine WiFi Specifier and Suggestion APIs on Android Q

I am building an app where the connection to a 2nd device is the essence. Therefore, I used the WifiNetworkSpecifier API. However, the application must be able to automatically reconnect to the target network once the users leave and return to the Wi-Fi perimeter. Thus, I used the WifiNetworkSuggestion API. However, I am experiencing several issues there:
Once I get connected to the SSID using the specifier API and I confirm the push notification generated by the suggestion API, the suggestion API does not seem to work until I manually disconnect from the SSID (unregister network callback previously assigned to the specifier request) or kill the application.
If there is another network present in the perimeter which the user previously connected to by using the OS Wi-Fi manager (a hotspot, for instance), Android will prioritize this network, hence the suggestion API for my application would never auto-reconnect to the wanted and accessible SSID.
From my experience and understanding (which might be wrong) so far, it seems like we have to manually unregister the network callback previously assigned to the specifier request, or kill the application, and let the suggestion API to do its thing until it can work properly. This might be problematic if there are other networks (which the user previously connected to by using the OS Wi-Fi manager) present in the perimeter. In this case, we'd never auto-reconnect to the SSID defined by the application and the suggestion API would never work.
The question is: how to combine those two APIs to be able to connect to an SSID, yet auto-reconnect, without doing such ugly hacks as manually disconnecting the user, or killing the application, which also doesn't give us any guarantees?
In my opinion, this whole new implementation with the new network APIs is not done well, it's creating a lot of issues and restrictions for developers, or at least it's poorly documented.
Here's the code used for making the requests. Note that the device I'm connecting to does not have actual internet access, it's just used as a p2p network.
#RequiresApi(api = Build.VERSION_CODES.Q)
private fun connectToWiFiOnQ(wifiCredentials: WifiCredentials, onUnavailable: () -> Unit) {
val request = NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.setNetworkSpecifier(createWifiNetworkSpecifier(wifiCredentials))
.build()
networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
super.onAvailable(network)
connectivityManager.bindProcessToNetwork(network)
}
override fun onUnavailable() {
super.onUnavailable()
onUnavailable.invoke()
}
}
networkCallback?.let {
addNetworkSuggestion(wifiCredentials)
connectivityManager.requestNetwork(request, it)
}
}
#RequiresApi(api = Build.VERSION_CODES.Q)
private fun addNetworkSuggestion(wifiCredentials: WifiCredentials) {
wifiManager.addNetworkSuggestions(listOf(createWifiNetworkSuggestion(wifiCredentials))).apply {
if (this != WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS) {
if (this == WifiManager.STATUS_NETWORK_SUGGESTIONS_ERROR_ADD_EXCEEDS_MAX_PER_APP) {
wifiManager.removeNetworkSuggestions(emptyList())
addNetworkSuggestion(wifiCredentials)
}
}
}
suggestionBroadcastReceiver?.let { context.unregisterReceiver(it) }
suggestionBroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action != WifiManager.ACTION_WIFI_NETWORK_SUGGESTION_POST_CONNECTION)
return
// Post connection processing..
}
}
context.registerReceiver(
suggestionBroadcastReceiver, IntentFilter(WifiManager.ACTION_WIFI_NETWORK_SUGGESTION_POST_CONNECTION)
)
}
#RequiresApi(api = Build.VERSION_CODES.Q)
private fun createWifiNetworkSpecifier(wifiCredentials: WifiCredentials): WifiNetworkSpecifier {
return when (wifiCredentials.authenticationType.toLowerCase()) {
WifiCipherType.NOPASS.name.toLowerCase() -> WifiNetworkSpecifier.Builder()
.setSsid(wifiCredentials.networkSSID)
.setIsHiddenSsid(wifiCredentials.isSSIDHidden)
.build()
WifiCipherType.WPA.name.toLowerCase() -> WifiNetworkSpecifier.Builder()
.setSsid(wifiCredentials.networkSSID)
.setWpa2Passphrase(wifiCredentials.password)
.setIsHiddenSsid(wifiCredentials.isSSIDHidden)
.build()
else -> WifiNetworkSpecifier.Builder()
.setSsid(wifiCredentials.networkSSID)
.setIsHiddenSsid(wifiCredentials.isSSIDHidden)
.build()
}
}
#RequiresApi(api = Build.VERSION_CODES.Q)
private fun createWifiNetworkSuggestion(wifiCredentials: WifiCredentials): WifiNetworkSuggestion {
return when (wifiCredentials.authenticationType.toLowerCase()) {
WifiCipherType.NOPASS.name.toLowerCase() -> WifiNetworkSuggestion.Builder()
.setSsid(wifiCredentials.networkSSID)
.setIsHiddenSsid(wifiCredentials.isSSIDHidden)
.build()
WifiCipherType.WPA.name.toLowerCase() -> WifiNetworkSuggestion.Builder()
.setSsid(wifiCredentials.networkSSID)
.setWpa2Passphrase(wifiCredentials.password)
.setIsHiddenSsid(wifiCredentials.isSSIDHidden)
.build()
else -> WifiNetworkSuggestion.Builder()
.setSsid(wifiCredentials.networkSSID)
.setIsHiddenSsid(wifiCredentials.isSSIDHidden)
.build()
}
}
Calling the suggestion API in onAvailable works for me. That way the user doesn't see two popups at the same time either.
val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
connectivityManager.bindProcessToNetwork(network)
addNetworkSuggestion(wifiCredentials)
}
}

Any way to monitor discovered BLE peripherals without connecting?

Is there any way of being notified if a discovered BLE peripheral moves out of range or otherwise drops out of sight? I'm using rxBleClient.scanBleDevices() to build a list of devices in the area that are advertising, but before shipping this list to the main application I'd like to be sure that all the devices are still reachable. What's the best way of doing this?
The vanilla Android Scan API allows for scanning BLE devices with callback types of:
/**
* A result callback is only triggered for the first advertisement packet received that matches
* the filter criteria.
*/
public static final int CALLBACK_TYPE_FIRST_MATCH = 2;
/**
* Receive a callback when advertisements are no longer received from a device that has been
* previously reported by a first match callback.
*/
public static final int CALLBACK_TYPE_MATCH_LOST = 4;
The same API is available via RxBleClient.scanBleDevices(ScanSettings, ScanFilter...)
The CALLBACK_TYPE_FIRST_MATCH and CALLBACK_TYPE_MATCH_LOST are flags that can be put into ScanSettings.
The timeout after which the CALLBACK_TYPE_MATCH_LOST is triggered is somewhere around 10 seconds. This may be an indication that a particular device is no longer in range/available.
You can create a Transformer that will collect scanned devices and emit a list, that is kept up to date, depending on how long ago the device was recently seen.
Robert, that may not be exactly what you expect but treat it as an example. My Transformer is emitting a list of items whenever it has been changed, either because an update from the scanner or the eviction happened (checked every second).
class RollingPairableDeviceReducer(
private val systemTime: SystemTime,
private val evictionTimeSeconds: Long,
private val pairableDeviceFactory: PairableDeviceFactory
) : Observable.Transformer<ScannedDevice, List<PairableDevice>> {
override fun call(source: Observable<ScannedDevice>): Observable<List<PairableDevice>> {
val accumulator: MutableSet<PairableDevice> = Collections.synchronizedSet(mutableSetOf())
return source
.map { createPairableDevice(it) }
.map { pairableDevice ->
val added = updateOrAddDevice(accumulator, pairableDevice)
val removed = removeOldDevices(accumulator)
added || removed
}
.mergeWith(checkEvictionEverySecond(accumulator))
.filter { addedOrRemoved -> addedOrRemoved == true }
.map { accumulator.toList() }
}
private fun createPairableDevice(scannedDevice: ScannedDevice)
= pairableDeviceFactory.create(scannedDevice)
private fun updateOrAddDevice(accumulator: MutableSet<PairableDevice>, emittedItem: PairableDevice): Boolean {
val existingPairableDevice = accumulator.find { it.deviceIdentifier.hardwareId == emittedItem.deviceIdentifier.hardwareId }
return if (existingPairableDevice != null) {
accumulator.remove(existingPairableDevice)
existingPairableDevice.updateWith(emittedItem)
accumulator.add(existingPairableDevice)
false
} else {
accumulator.add(emittedItem)
true
}
}
private fun checkEvictionEverySecond(collector: MutableSet<PairableDevice>): Observable<Boolean>
= Observable.interval(1, TimeUnit.SECONDS)
.map { removeOldDevices(collector) }
private fun removeOldDevices(accumulator: MutableSet<PairableDevice>): Boolean {
val currentTimeInMillis = systemTime.currentTimeInMillis()
val evictionTimeMillis = TimeUnit.SECONDS.toMillis(evictionTimeSeconds)
return accumulator.removeAll { (currentTimeInMillis - it.lastSeenTime) >= evictionTimeMillis }
}
}

How to direct users for enabling accessibility service for my app

I know It's impossible to enable the Accessibility service for apps programmatically, so I'd like to direct users to this screen:
System settings --> Accessibility --> app name --> enable/disable screen.
Is that possible ?
You can get them to the Accessibility screen on most devices using ACTION_ACCESSIBILITY_SETTINGS. However:
that may not work on all devices, so you will want to just send them to Settings as a fallback, if you get an ActivityNotFoundException
there is no way to get them straight to any given app, let alone the enable/disable screen
You can at least make it reach the app, making the app item blink. It should work for most devices, or at least those that are like of Pixel devices:
fun <T : AccessibilityService> getRequestAccessibilityPermissionIntents(context: Context, accessibilityService: Class<T>): Array<Intent> {
var intent = Intent("com.samsung.accessibility.installed_service")
if (intent.resolveActivity(context.packageManager) == null) {
intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
}
val extraFragmentArgKey = ":settings:fragment_args_key"
val extraShowFragmentArguments = ":settings:show_fragment_args"
val bundle = Bundle()
val showArgs = "${context.packageName}/${accessibilityService.canonicalName!!}"
bundle.putString(extraFragmentArgKey, showArgs)
intent.putExtra(extraFragmentArgKey, showArgs)
intent.putExtra(extraShowFragmentArguments, bundle)
return arrayOf(intent, Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY))
}
Usage:
private fun requestAccessibilityPermission() {
getRequestAccessibilityPermissionIntents(this, MyAccessibilityService::class.java).forEach { intent ->
try {
startActivity(intent)
return
} catch (e: Exception) {
}
}
//TODO do something here in case it failed
}

Categories

Resources