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..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,24 +1,38 @@ 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 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) */ 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?) +/** + * @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 +76,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..5b0e62b9 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 @@ -135,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 +152,10 @@ 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() private val usbManager get() = context.usbManager var selectedAddress: String? = null @@ -298,6 +300,9 @@ class BTScanModel @Inject constructor( // Include a placeholder for "None" addDevice(DeviceListEntry(context.getString(R.string.none), "n", true)) + // Include CompanionDeviceManager valid associations + addDeviceAssociations() + serialDevices.forEach { (_, d) -> addDevice(USBDeviceListEntry(usbManager, d)) } @@ -334,6 +339,37 @@ class BTScanModel @Inject constructor( } } + /** + * @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) + } + } + } + val devices = object : MutableLiveData>(mutableMapOf()) { /** @@ -349,7 +385,7 @@ class BTScanModel @Inject constructor( */ override fun onInactive() { super.onInactive() - // stopScan() + if (!hasCompanionDeviceApi) stopScan() } } @@ -466,10 +502,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 } @@ -649,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 -> @@ -770,8 +805,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 { @@ -780,21 +814,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) @@ -810,14 +838,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, @@ -958,7 +986,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { super.onViewCreated(view, savedInstanceState) initCommonUI() - if (hasCompanionDeviceApi) + if (scanModel.hasCompanionDeviceApi) initModernScan() else initClassicScan() @@ -1068,7 +1096,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)