From cde3f2dec267a52a11030b3b8905aac7a92d9c8c Mon Sep 17 00:00:00 2001 From: geeksville Date: Sun, 19 Apr 2020 19:23:20 -0700 Subject: [PATCH] WIP - doesn't yet build, but changing to set the device addresses the correct way --- .../com/geeksville/mesh/IMeshService.aidl | 6 + .../mesh/IRadioInterfaceService.aidl | 5 +- .../java/com/geeksville/mesh/MainActivity.kt | 8 +- .../geeksville/mesh/service/MeshService.kt | 55 ++++----- .../mesh/service/RadioInterfaceService.kt | 104 +++++++++--------- .../geeksville/mesh/ui/SettingsFragment.kt | 24 ++-- 6 files changed, 111 insertions(+), 91 deletions(-) diff --git a/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl b/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl index 877242298..54ebd964f 100644 --- a/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl +++ b/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl @@ -57,8 +57,14 @@ interface IMeshService { */ String connectionState(); + /// If a macaddress we will try to talk to our device, if null we will be idle. + /// Users should not call this directly, only used internally by the MeshUtil activity + void setDeviceAddress(String deviceAddr); + // see com.geeksville.com.geeksville.mesh broadcast intents // RECEIVED_OPAQUE for data received from other nodes. payload will contain a DataPacket // NODE_CHANGE for new IDs appearing or disappearing // CONNECTION_CHANGED for losing/gaining connection to the packet radio + + } diff --git a/app/src/main/aidl/com/geeksville/mesh/IRadioInterfaceService.aidl b/app/src/main/aidl/com/geeksville/mesh/IRadioInterfaceService.aidl index b7aa04e01..55e051343 100644 --- a/app/src/main/aidl/com/geeksville/mesh/IRadioInterfaceService.aidl +++ b/app/src/main/aidl/com/geeksville/mesh/IRadioInterfaceService.aidl @@ -22,6 +22,7 @@ interface IRadioInterfaceService { byte []readOwner(); void writeOwner(in byte [] owner); - /// If true we will try to talk to our device, if false we will shutdown. Useful during software update. - void enableLink(boolean enable); + /// If a macaddress we will try to talk to our device, if null we will be idle. + /// Users should not call this directly, called only by MeshService + void setDeviceAddress(String deviceAddr); } diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index 70867809b..cbbd27ec7 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -436,7 +436,9 @@ class MainActivity : AppCompatActivity(), Logging, } // ... Continue interacting with the paired device. - RadioInterfaceService.setBondedDeviceAddress(this, device.address) + model.meshService?.let { service -> + service.setDeviceAddress(device.address) + } } else -> @@ -636,7 +638,7 @@ class MainActivity : AppCompatActivity(), Logging, } } - fun bindMeshService() { + private fun bindMeshService() { debug("Binding to mesh service!") // we bind using the well known name, to make sure 3rd party apps could also if (model.meshService != null) @@ -648,7 +650,7 @@ class MainActivity : AppCompatActivity(), Logging, } } - fun unbindMeshService() { + private fun unbindMeshService() { // If we have received the service, and hence registered with // it, then now is the time to unregister. // if we never connected, do nothing diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt index 7ec6e8e0d..946a2a332 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -37,7 +37,7 @@ import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext -class RadioNotConnectedException() : Exception("Not connected to radio") +class RadioNotConnectedException(message: String = "Not connected to radio") : Exception(message) private val errorHandler = CoroutineExceptionHandler { _, exception -> @@ -73,35 +73,30 @@ class MeshService : Service(), Logging { /// Helper function to start running our service, returns the intent used to reach it /// or null if the service could not be started (no bluetooth or no bonded device set) fun startService(context: Context): Intent? { - if (RadioInterfaceService.getBondedDeviceAddress(context) == null) { - warn("No mesh radio is bonded, not starting service") - return null - } else { - // bind to our service using the same mechanism an external client would use (for testing coverage) - // The following would work for us, but not external users - //val intent = Intent(this, MeshService::class.java) - //intent.action = IMeshService::class.java.name - val intent = Intent() - intent.setClassName( - "com.geeksville.mesh", - "com.geeksville.mesh.service.MeshService" - ) + // bind to our service using the same mechanism an external client would use (for testing coverage) + // The following would work for us, but not external users + //val intent = Intent(this, MeshService::class.java) + //intent.action = IMeshService::class.java.name + val intent = Intent() + intent.setClassName( + "com.geeksville.mesh", + "com.geeksville.mesh.service.MeshService" + ) - // Before binding we want to explicitly create - so the service stays alive forever (so it can keep - // listening for the bluetooth packets arriving from the radio. And when they arrive forward them - // to Signal or whatever. + // Before binding we want to explicitly create - so the service stays alive forever (so it can keep + // listening for the bluetooth packets arriving from the radio. And when they arrive forward them + // to Signal or whatever. - logAssert( - (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // we have some samsung devices failing with https://issuetracker.google.com/issues/76112072#comment56 not sure what the fix is yet - context.startForegroundService(intent) - } else { - context.startService(intent) - }) != null - ) + logAssert( + (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // we have some samsung devices failing with https://issuetracker.google.com/issues/76112072#comment56 not sure what the fix is yet + context.startForegroundService(intent) + } else { + context.startService(intent) + }) != null + ) - return intent - } + return intent } } @@ -1069,6 +1064,12 @@ class MeshService : Service(), Logging { } private val binder = object : IMeshService.Stub() { + + override fun setDeviceAddress(deviceAddr: String?) { + debug("Passing through device change to radio service: $deviceAddr") + connectedRadio.setDeviceAddress(deviceAddr) + } + // Note: bound methods don't get properly exception caught/logged, so do that with a wrapper // per https://blog.classycode.com/dealing-with-exceptions-in-aidl-9ba904c6d63 override fun subscribeReceiver(packageName: String, receiverName: String) = diff --git a/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt b/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt index eefb83e3f..ca480ee99 100644 --- a/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.app.Service import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattService import android.bluetooth.BluetoothManager import android.companion.CompanionDeviceManager import android.content.Context @@ -184,52 +185,6 @@ class RadioInterfaceService : Service(), Logging { } - @SuppressLint("NewApi") - fun setBondedDeviceAddress(context: Context, addr: String?) { - // Record that this use has configured a radio - GeeksvilleApplication.analytics.track( - "mesh_bond" - ) - - debug("Setting bonded device to $addr") - if (hasCompanionDeviceApi((context))) { - // We only keep an association to one device at a time... - if (addr != null) { - val deviceManager = context.getSystemService(CompanionDeviceManager::class.java) - - deviceManager.associations.forEach { old -> - if (addr != old) { - debug("Forgetting old BLE association $old") - deviceManager.disassociate(old) - } - } - } - } else { - getPrefs(context).edit(commit = true) { - if (addr == null) - this.remove(DEVADDR_KEY) - else - putString(DEVADDR_KEY, addr) - } - } - - // Force the service to reconnect - val s = runningService - if (s != null) { - // Ignore any errors that happen while closing old device - ignoreException { - info("shutting down old service") - s.setEnabled(false) // nasty, needed to force the next setEnabled call to reconnect - } - info("Setting enable on the running radio service") - s.setEnabled(addr != null) - } - if (addr != null) { - info("We have a device addr now, starting mesh service") - MeshService.startService(context) - } - } - /// Can we use the modern BLE scan API? fun hasCompanionDeviceApi(context: Context): Boolean = false /* ALAS - not ready for production yet if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -250,8 +205,11 @@ class RadioInterfaceService : Service(), Logging { /// Our BLE device val device get() = safe!!.gatt!! - /// Our service - val service get() = device.getService(BTM_SERVICE_UUID) + /// Our service - note - it is possible to get back a null response for getService if the device services haven't yet been found + val service + get(): BluetoothGattService = device.getService(BTM_SERVICE_UUID) + ?: throw RadioNotConnectedException("BLE service not found") + //.services.find { it.uuid == BTM_SERVICE_UUID }!! private lateinit var fromNum: BluetoothGattCharacteristic @@ -328,6 +286,49 @@ class RadioInterfaceService : Service(), Logging { } + @SuppressLint("NewApi") + fun setBondedDeviceAddress(addr: String?) { + // Record that this use has configured a radio + GeeksvilleApplication.analytics.track( + "mesh_bond" + ) + + // Ignore any errors that happen while closing old device + ignoreException { + Companion.info("shutting down old service") + setEnabled(false) // nasty, needed to force the next setEnabled call to reconnect + } + + debug("Setting bonded device to $addr") + if (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) { + Companion.debug("Forgetting old BLE association $old") + deviceManager.disassociate(old) + } + } + } + } else { + getPrefs(this).edit(commit = true) { + if (addr == null) + this.remove(DEVADDR_KEY) + else + putString(DEVADDR_KEY, addr) + } + } + + // Force the service to reconnect + if (addr != null) { + info("Setting enable on the running radio service") + setEnabled(true) + } + } + + private fun onDisconnect() { broadcastConnectionChanged(false) isConnected = false @@ -418,7 +419,7 @@ class RadioInterfaceService : Service(), Logging { } else { val address = getBondedDeviceAddress(this) if (address == null) - errormsg("No bonded mesh radio, can't create service") + errormsg("No bonded mesh radio, can't start service") else { // Note: this call does no comms, it just creates the device object (even if the // device is off/not connected) @@ -518,8 +519,9 @@ class RadioInterfaceService : Service(), Logging { } private val binder = object : IRadioInterfaceService.Stub() { - override fun enableLink(enable: Boolean) = toRemoteExceptions { - setEnabled(enable) + + override fun setDeviceAddress(deviceAddr: String?) = toRemoteExceptions { + setBondedDeviceAddress(deviceAddr) } // A write of any size to nodeinfo means restart reading 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 1746b22d0..0cf1c521a 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt @@ -36,15 +36,18 @@ object SLogging : Logging {} /// Change to a new macaddr selection, updating GUI and radio fun changeDeviceSelection(context: MainActivity, newAddr: String?) { - RadioInterfaceService.setBondedDeviceAddress(context, newAddr) + model.meshService?.let { service -> + service.setDeviceAddress(context, newAddr) - // Super ugly hack. we force the activity to reconnect FIXME, find a cleaner way - context.unbindMeshService() - context.bindMeshService() + } } /// Show the UI asking the user to bond with a device, call changeSelection() if/when bonding completes -private fun requestBonding(activity: MainActivity, device: BluetoothDevice, onSuccess: () -> Unit) { +private fun requestBonding( + activity: MainActivity, + device: BluetoothDevice, + onSuccess: () -> Unit +) { SLogging.info("Starting bonding for $device") // We need this receiver to get informed when the bond attempt finished @@ -132,7 +135,10 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging { // If nothing was selected, by default select the first thing we see if (selectedMacAddr == null && entry.bonded) - changeScanSelection(GeeksvilleApplication.currentActivity as MainActivity, addr) + changeScanSelection( + GeeksvilleApplication.currentActivity as MainActivity, + addr + ) devices.value = oldDevs + Pair(addr, entry) // trigger gui updates } @@ -347,14 +353,16 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { if (!device.bonded) scanStatusText.setText(R.string.starting_pairing) - b.isSelected = scanModel.onSelected(requireActivity() as MainActivity, device) + b.isSelected = + scanModel.onSelected(requireActivity() as MainActivity, device) if (!b.isSelected) scanStatusText.setText(R.string.pairing_failed) } } - val hasBonded = RadioInterfaceService.getBondedDeviceAddress(requireContext()) != null + val hasBonded = + RadioInterfaceService.getBondedDeviceAddress(requireContext()) != null // get rid of the warning text once at least one device is paired warningNotPaired.visibility = if (hasBonded) View.GONE else View.VISIBLE