From aaa5c1cf04b57773610640b84ae34c95cbb5e337 Mon Sep 17 00:00:00 2001 From: andrekir Date: Thu, 28 Apr 2022 21:40:34 -0300 Subject: [PATCH 1/4] move hasCompanionDeviceApi out of BluetoothInterface --- .../mesh/android/ContextServices.kt | 11 +++- .../repository/radio/BluetoothInterface.kt | 63 ------------------- .../geeksville/mesh/ui/SettingsFragment.kt | 8 +-- 3 files changed, 11 insertions(+), 71 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/android/ContextServices.kt b/app/src/main/java/com/geeksville/mesh/android/ContextServices.kt index 11b9e9f8..ab7bdf2b 100644 --- a/app/src/main/java/com/geeksville/mesh/android/ContextServices.kt +++ b/app/src/main/java/com/geeksville/mesh/android/ContextServices.kt @@ -8,7 +8,6 @@ import android.content.pm.PackageManager import android.hardware.usb.UsbManager import android.os.Build import androidx.core.content.ContextCompat -import com.geeksville.mesh.repository.radio.BluetoothInterface /** * @return null on platforms without a BlueTooth driver (i.e. the emulator) @@ -19,6 +18,14 @@ val Context.usbManager: UsbManager get() = requireNotNull(getSystemService(Conte val Context.notificationManager: NotificationManager get() = requireNotNull(getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager?) +/** + * @return true if CompanionDeviceManager API is present + */ +fun Context.hasCompanionDeviceApi(): Boolean = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + packageManager.hasSystemFeature(PackageManager.FEATURE_COMPANION_DEVICE_SETUP) + else false + /** * return a list of the permissions we don't have */ @@ -62,7 +69,7 @@ fun Context.getScanPermissions(): List { perms.add(Manifest.permission.BLUETOOTH_ADMIN) } */ - if (!BluetoothInterface.hasCompanionDeviceApi(this)) { + if (!hasCompanionDeviceApi()) { perms.add(Manifest.permission.ACCESS_FINE_LOCATION) perms.add(Manifest.permission.BLUETOOTH_ADMIN) } diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterface.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterface.kt index f4861f78..0bcc0a9a 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterface.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterface.kt @@ -118,12 +118,6 @@ class BluetoothInterface(val service: RadioInterfaceService, val address: String usbRepository: UsbRepository, // Temporary until dependency injection transition is completed rest: String ): Boolean { - /* val allPaired = if (hasCompanionDeviceApi(context)) { - val deviceManager: CompanionDeviceManager by lazy { - context.getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager - } - deviceManager.associations.map { it }.toSet() - } else { */ val allPaired = getBluetoothAdapter(context)?.bondedDevices.orEmpty() .map { it.address }.toSet() return if (!allPaired.contains(rest)) { @@ -133,63 +127,6 @@ class BluetoothInterface(val service: RadioInterfaceService, val address: String true } - - /// Return the device we are configured to use, or null for none - /* - @SuppressLint("NewApi") - fun getBondedDeviceAddress(context: Context): String? = - if (hasCompanionDeviceApi(context)) { - // Use new companion API - - val deviceManager = context.getSystemService(CompanionDeviceManager::class.java) - val associations = deviceManager.associations - val result = associations.firstOrNull() - debug("reading bonded devices: $result") - result - } else { - // Use classic API and a preferences string - - val allPaired = - getBluetoothAdapter(context)?.bondedDevices.orEmpty().map { it.address }.toSet() - - // If the user has unpaired our device, treat things as if we don't have one - val address = InterfaceService.getPrefs(context).getString(DEVADDR_KEY, null) - - if (address != null && !allPaired.contains(address)) { - warn("Ignoring stale bond to ${address.anonymize}") - null - } else - address - } -*/ - - /// Can we use the modern BLE scan API? - fun hasCompanionDeviceApi(context: Context): Boolean = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val res = - context.packageManager.hasSystemFeature(PackageManager.FEATURE_COMPANION_DEVICE_SETUP) - debug("CompanionDevice API available=$res") - res - } else { - warn("CompanionDevice API not available, falling back to classic scan") - false - } - - /** FIXME - when adding companion device support back in, use this code to set companion device from setBondedDevice - * if (BluetoothInterface.hasCompanionDeviceApi(this)) { - // We only keep an association to one device at a time... - if (addr != null) { - val deviceManager = getSystemService(CompanionDeviceManager::class.java) - - deviceManager.associations.forEach { old -> - if (addr != old) { - BluetoothInterface.debug("Forgetting old BLE association $old") - deviceManager.disassociate(old) - } - } - } - */ - /** * this is created in onCreate() * We do an ugly hack of keeping it in the singleton so we can share it for the rare software update case diff --git a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt index 02a96931..25684cc6 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt @@ -37,7 +37,6 @@ import com.geeksville.mesh.android.* import com.geeksville.mesh.databinding.SettingsFragmentBinding import com.geeksville.mesh.model.BluetoothViewModel import com.geeksville.mesh.model.UIViewModel -import com.geeksville.mesh.repository.radio.BluetoothInterface import com.geeksville.mesh.repository.radio.MockInterface import com.geeksville.mesh.repository.radio.RadioInterfaceService import com.geeksville.mesh.repository.radio.SerialInterface @@ -154,6 +153,7 @@ class BTScanModel @Inject constructor( } val bluetoothAdapter = context.bluetoothManager?.adapter + val hasCompanionDeviceApi get() = context.hasCompanionDeviceApi() private val usbManager get() = context.usbManager var selectedAddress: String? = null @@ -466,10 +466,6 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { @Inject internal lateinit var usbRepository: UsbRepository - private val hasCompanionDeviceApi: Boolean by lazy { - BluetoothInterface.hasCompanionDeviceApi(requireContext()) - } - private val deviceManager: CompanionDeviceManager by lazy { requireContext().getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager } @@ -958,7 +954,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { super.onViewCreated(view, savedInstanceState) initCommonUI() - if (hasCompanionDeviceApi) + if (scanModel.hasCompanionDeviceApi) initModernScan() else initClassicScan() From 0950e12bd094f5e8cefa6c914f5d3e797b696b1a Mon Sep 17 00:00:00 2001 From: andrekir Date: Thu, 28 Apr 2022 23:09:06 -0300 Subject: [PATCH 2/4] add BLE associations to devices list --- .../mesh/android/ContextServices.kt | 7 ++++++ .../geeksville/mesh/ui/SettingsFragment.kt | 25 ++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/geeksville/mesh/android/ContextServices.kt b/app/src/main/java/com/geeksville/mesh/android/ContextServices.kt index ab7bdf2b..f3bd9ee5 100644 --- a/app/src/main/java/com/geeksville/mesh/android/ContextServices.kt +++ b/app/src/main/java/com/geeksville/mesh/android/ContextServices.kt @@ -1,8 +1,10 @@ package com.geeksville.mesh.android import android.Manifest +import android.annotation.SuppressLint import android.app.NotificationManager import android.bluetooth.BluetoothManager +import android.companion.CompanionDeviceManager import android.content.Context import android.content.pm.PackageManager import android.hardware.usb.UsbManager @@ -14,6 +16,11 @@ import androidx.core.content.ContextCompat */ val Context.bluetoothManager: BluetoothManager? get() = getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager? +val Context.deviceManager: CompanionDeviceManager? + @SuppressLint("InlinedApi") + get() = if (hasCompanionDeviceApi()) getSystemService(Context.COMPANION_DEVICE_SERVICE) as? CompanionDeviceManager? + else null + val Context.usbManager: UsbManager get() = requireNotNull(getSystemService(Context.USB_SERVICE) as? UsbManager?) { "USB_SERVICE is not available"} val Context.notificationManager: NotificationManager get() = requireNotNull(getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager?) diff --git a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt index 25684cc6..8ece3085 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt @@ -134,7 +134,7 @@ class BTScanModel @Inject constructor( null override fun toString(): String { - return "DeviceListEntry(name=${name.anonymize}, addr=${address.anonymize})" + return "DeviceListEntry(name=${name.anonymize}, addr=${address.anonymize}, bonded=$bonded)" } val isBluetooth: Boolean get() = address[0] == 'x' @@ -153,7 +153,9 @@ class BTScanModel @Inject constructor( } val bluetoothAdapter = context.bluetoothManager?.adapter + private val deviceManager get() = context.deviceManager val hasCompanionDeviceApi get() = context.hasCompanionDeviceApi() + private val hasConnectPermission get() = context.hasConnectPermission() private val usbManager get() = context.usbManager var selectedAddress: String? = null @@ -230,6 +232,8 @@ class BTScanModel @Inject constructor( } } + + private fun addDevice(entry: DeviceListEntry) { val oldDevs = devices.value!! oldDevs[entry.address] = entry // Add/replace entry @@ -298,6 +302,9 @@ class BTScanModel @Inject constructor( // Include a placeholder for "None" addDevice(DeviceListEntry(context.getString(R.string.none), "n", true)) + if (hasCompanionDeviceApi && hasConnectPermission) + addAssociations() + serialDevices.forEach { (_, d) -> addDevice(USBDeviceListEntry(usbManager, d)) } @@ -334,6 +341,22 @@ class BTScanModel @Inject constructor( } } + @SuppressLint("MissingPermission", "NewApi") + private fun addAssociations() { + deviceManager?.associations?.forEach { bleAddress -> + bluetoothAdapter?.getRemoteDevice(bleAddress)?.let { device -> + if (device.name.startsWith("Mesh")) { + val entry = DeviceListEntry( + device.name, + "x${device.address}", // full address with the bluetooth prefix added + device.bondState == BOND_BONDED + ) + addDevice(entry) + } + } + } + } + val devices = object : MutableLiveData>(mutableMapOf()) { /** From b6410dd1627e3cd5fe975b7312d386478361480e Mon Sep 17 00:00:00 2001 From: andrekir Date: Fri, 29 Apr 2022 23:34:03 -0300 Subject: [PATCH 3/4] disassociate devices when not bonded --- .../geeksville/mesh/ui/SettingsFragment.kt | 74 ++++++++++--------- 1 file changed, 40 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt index 8ece3085..5710464f 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt @@ -152,7 +152,7 @@ class BTScanModel @Inject constructor( debug("BTScanModel cleared") } - val bluetoothAdapter = context.bluetoothManager?.adapter + private val bluetoothAdapter = context.bluetoothManager?.adapter private val deviceManager get() = context.deviceManager val hasCompanionDeviceApi get() = context.hasCompanionDeviceApi() private val hasConnectPermission get() = context.hasConnectPermission() @@ -232,8 +232,6 @@ class BTScanModel @Inject constructor( } } - - private fun addDevice(entry: DeviceListEntry) { val oldDevs = devices.value!! oldDevs[entry.address] = entry // Add/replace entry @@ -302,8 +300,8 @@ class BTScanModel @Inject constructor( // Include a placeholder for "None" addDevice(DeviceListEntry(context.getString(R.string.none), "n", true)) - if (hasCompanionDeviceApi && hasConnectPermission) - addAssociations() + // Include CompanionDeviceManager valid associations + addDeviceAssociations() serialDevices.forEach { (_, d) -> addDevice(USBDeviceListEntry(usbManager, d)) @@ -341,18 +339,33 @@ class BTScanModel @Inject constructor( } } - @SuppressLint("MissingPermission", "NewApi") - private fun addAssociations() { - deviceManager?.associations?.forEach { bleAddress -> - bluetoothAdapter?.getRemoteDevice(bleAddress)?.let { device -> - if (device.name.startsWith("Mesh")) { - val entry = DeviceListEntry( - device.name, - "x${device.address}", // full address with the bluetooth prefix added - device.bondState == BOND_BONDED - ) - addDevice(entry) - } + /** + * @return DeviceListEntry from Bluetooth Address. + * Only valid if name begins with "Meshtastic"... + */ + @SuppressLint("MissingPermission") + fun bleDeviceFrom(bleAddress: String): DeviceListEntry { + val device = + if (hasConnectPermission) bluetoothAdapter?.getRemoteDevice(bleAddress) else null + + return if (device != null && device.name != null) { + DeviceListEntry( + device.name, + "x${device.address}", // full address with the bluetooth prefix added + device.bondState == BOND_BONDED + ) + } else DeviceListEntry("", "", false) + } + + @SuppressLint("NewApi") + private fun addDeviceAssociations() { + if (hasCompanionDeviceApi) deviceManager?.associations?.forEach { bleAddress -> + val bleDevice = bleDeviceFrom(bleAddress) + if (!bleDevice.bonded) { // Clean up associations after pairing is removed + debug("Forgetting old BLE association ${bleAddress.anonymize}") + deviceManager?.disassociate(bleAddress) + } else if (bleDevice.name.startsWith("Mesh")) { + addDevice(bleDevice) } } } @@ -372,7 +385,7 @@ class BTScanModel @Inject constructor( */ override fun onInactive() { super.onInactive() - // stopScan() + if (!hasCompanionDeviceApi) stopScan() } } @@ -789,8 +802,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { b.text = device.name b.id = View.generateViewId() b.isEnabled = enabled - b.isChecked = - device.address == scanModel.selectedNotNull && device.bonded // Only show checkbox if device is still paired + b.isChecked = device.address == scanModel.selectedNotNull binding.deviceRadioGroup.addView(b) b.setOnClickListener { @@ -799,21 +811,15 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { b.isChecked = scanModel.onSelected(myActivity, device) - - if (!b.isSelected) { - binding.scanStatusText.text = getString(R.string.please_pair) - } } } - @SuppressLint("MissingPermission") private fun updateDevicesButtons(devices: MutableMap?) { // Remove the old radio buttons and repopulate binding.deviceRadioGroup.removeAllViews() if (devices == null) return - val adapter = scanModel.bluetoothAdapter var hasShownOurDevice = false devices.values.forEach { device -> if (device.address == scanModel.selectedNotNull) @@ -829,14 +835,14 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { // and before use val bleAddr = scanModel.selectedBluetooth - if (bleAddr != null && adapter != null && myActivity.hasConnectPermission()) { - val bDevice = - adapter.getRemoteDevice(bleAddr) - if (bDevice.name != null) { // ignore nodes that node have a name, that means we've lost them since they appeared + if (bleAddr != null) { + debug("bleAddr= $bleAddr selected= ${scanModel.selectedAddress}") + val bleDevice = scanModel.bleDeviceFrom(bleAddr) + if (bleDevice.name.startsWith("Mesh")) { // ignore nodes that node have a name, that means we've lost them since they appeared val curDevice = BTScanModel.DeviceListEntry( - bDevice.name, - scanModel.selectedAddress!!, - bDevice.bondState == BOND_BONDED + bleDevice.name, + bleDevice.address, + bleDevice.bonded ) addDeviceButton( curDevice, @@ -1087,7 +1093,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { val hasUSB = usbRepository.serialDevicesWithDrivers.value.isNotEmpty() if (!hasUSB) { // Warn user if BLE is disabled - if (scanModel.bluetoothAdapter?.isEnabled != true) { + if (bluetoothViewModel.enabled.value == false) { showSnackbar(getString(R.string.error_bluetooth)) } else { if (binding.provideLocationCheckbox.isChecked) From 0294da844b381fb2d1a4ca689fd3fb64003c7e0f Mon Sep 17 00:00:00 2001 From: andrekir Date: Sat, 30 Apr 2022 00:06:49 -0300 Subject: [PATCH 4/4] update UI when started with BLE disabled --- .../main/java/com/geeksville/mesh/ui/SettingsFragment.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt index 5710464f..5b0e62b9 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt @@ -681,9 +681,12 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { regionAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) spinner.adapter = regionAdapter - bluetoothViewModel.enabled.observe(viewLifecycleOwner) { - if (it) binding.changeRadioButton.show() - else binding.changeRadioButton.hide() + bluetoothViewModel.enabled.observe(viewLifecycleOwner) { enabled -> + if (enabled) { + binding.changeRadioButton.show() + if (scanModel.devices.value.isNullOrEmpty()) scanModel.setupScan() + if (binding.scanStatusText.text == getString(R.string.requires_bluetooth)) updateNodeInfo() + } else binding.changeRadioButton.hide() } model.ownerName.observe(viewLifecycleOwner) { name ->