diff --git a/TODO.md b/TODO.md index 23000fb6..e143a294 100644 --- a/TODO.md +++ b/TODO.md @@ -80,7 +80,6 @@ rules at the BluetoothDevice level. Either make SafeBluetooth lock at the devic * register app link for our URLs https://developer.android.com/studio/write/app-link-indexing.html * let user change radio params and share radio join info via QR code or text message (use an encoded app specific URL - to autoprompt for app installation as needed) * test with an oldish android release using real hardware -* if necessary restart entire BT adapter with this tip from Michael https://stackoverflow.com/questions/35103701/ble-android-onconnectionstatechange-not-being-called * stop using a foreground service * change info() log strings to debug() * use platform theme (dark or light) @@ -152,4 +151,5 @@ Don't leave device discoverable. Don't let unpaired users do things with device * tell Compose geeks * call crashlytics from exceptionReporter!!! currently not logging failures caught there * do setOwner every time we connect to the radio, use our settings, radio should ignore if unchanged -* send location data for devices that don't have a GPS - https://developer.android.com/training/location/change-location-settings \ No newline at end of file +* send location data for devices that don't have a GPS - https://developer.android.com/training/location/change-location-settings +* if necessary restart entire BT adapter with this tip from Michael https://stackoverflow.com/questions/35103701/ble-android-onconnectionstatechange-not-being-called 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 80ca3b3b..8a86a5f1 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -16,6 +16,8 @@ import android.os.RemoteException import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat.PRIORITY_MIN +import com.geeksville.analytics.DataPair +import com.geeksville.android.GeeksvilleApplication import com.geeksville.android.Logging import com.geeksville.android.ServiceClient import com.geeksville.mesh.* @@ -163,6 +165,8 @@ class MeshService : Service(), Logging { @SuppressLint("MissingPermission") private fun startLocationRequests() { if (fusedLocationClient == null) { + GeeksvilleApplication.analytics.track("location_start") // Figure out how many users needed to use the phone GPS + val request = LocationRequest.create().apply { interval = 5 * 60 * 1000 // FIXME, do more like once every 5 mins while we are connected to our radio _and_ someone else is in the mesh @@ -209,6 +213,7 @@ class MeshService : Service(), Logging { private fun stopLocationRequests() { if (fusedLocationClient != null) { debug("Stopping location requests") + GeeksvilleApplication.analytics.track("location_stop") fusedLocationClient?.removeLocationUpdates(locationCallback) fusedLocationClient = null } @@ -231,7 +236,7 @@ class MeshService : Service(), Logging { private fun broadcastNodeChange(info: NodeInfo) { debug("Broadcasting node change $info") val intent = Intent(ACTION_NODE_CHANGE) - + intent.putExtra(EXTRA_NODEINFO, info) explicitBroadcast(intent) } @@ -388,6 +393,8 @@ class MeshService : Service(), Logging { ) + private val numNodes get() = nodeDBbyNodeNum.size + /** * How many nodes are currently online (including our local node) */ @@ -623,6 +630,12 @@ class MeshService : Service(), Logging { // Do our startup init try { reinitFromRadio() + + GeeksvilleApplication.analytics.track( + "mesh_connect", + DataPair("num_nodes", numNodes), + DataPair("num_online", numOnlineNodes) + ) } catch (ex: RemoteException) { // It seems that when the ESP32 goes offline it can briefly come back for a 100ms ish which // causes the phone to try and reconnect. If we fail downloading our initial radio state we don't want to @@ -633,6 +646,12 @@ class MeshService : Service(), Logging { } else { // lost radio connection, therefore no need to keep listening to GPS stopLocationRequests() + + GeeksvilleApplication.analytics.track( + "mesh_disconnect", + DataPair("num_nodes", numNodes), + DataPair("num_online", numOnlineNodes) + ) } } 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 a5afe1e6..7b410c89 100644 --- a/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt @@ -224,7 +224,8 @@ class RadioInterfaceService : Service(), Logging { else { val fromRadio = service.getCharacteristic(BTM_FROMRADIO_CHARACTER) safe!!.asyncReadCharacteristic(fromRadio) { - val b = it.getOrThrow().value + val b = it.getOrThrow() + .value.clone() // We clone the array just in case, I'm not sure if they keep reusing the array if (b.isNotEmpty()) { debug("Received ${b.size} bytes from radio") @@ -380,13 +381,14 @@ class RadioInterfaceService : Service(), Logging { // Note: we generate a new characteristic each time, because we are about to // change the data and we want the data stored in the closure val toRadio = service.getCharacteristic(uuid) - var a = safe!!.readCharacteristic(toRadio).value + var a = safe!!.readCharacteristic(toRadio) + .value.clone() // we copy the bluetooth array because it might still be in use debug("Read of $uuid got ${a.size} bytes") if (a.isEmpty()) // An empty bluetooth response is converted to a null response for our clients - a = null - - a + null + else + a } } diff --git a/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt b/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt index f96176b7..fc866755 100644 --- a/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt +++ b/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt @@ -5,6 +5,8 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.os.Handler +import com.geeksville.android.GeeksvilleApplication import com.geeksville.android.Logging import com.geeksville.concurrent.CallbackContinuation import com.geeksville.concurrent.Continuation @@ -65,7 +67,8 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD debug("We were not connected, so ignoring bluetooth shutdown") } BluetoothAdapter.STATE_ON -> { - warn("FIXME - requeue a connect anytime bluetooth is reenabled?") + warn("requeue a connect anytime bluetooth is reenabled") + reconnect() } } } @@ -100,6 +103,35 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD } } + /** + * skanky hack to restart BLE if it says it is hosed + * https://stackoverflow.com/questions/35103701/ble-android-onconnectionstatechange-not-being-called + */ + var mHandler: Handler = Handler() + + fun restartBle() { + GeeksvilleApplication.analytics.track("ble_restart") // record # of times we needed to use this nasty hack + error("Doing emergency BLE restart") + val mgr = + context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager + val adp = mgr.adapter + if (null != adp) { + if (adp.isEnabled) { + adp.disable() + // TODO: display some kind of UI about restarting BLE + mHandler.postDelayed(object : Runnable { + override fun run() { + if (!adp.isEnabled) { + adp.enable() + } else { + mHandler.postDelayed(this, 2500) + } + } + }, 2500) + } + } + } + private val gattCallback = object : BluetoothGattCallback() { override fun onConnectionStateChange( @@ -153,6 +185,11 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD debug("No connectionCallback registered") } } + + if (status == 257) { // mystery error code when phone is hung + //throw Exception("Mystery bluetooth failure - debug me") + restartBle() + } } } } @@ -350,6 +387,13 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD queueConnect(autoConnect, CallbackContinuation(cb)) } + /// Restart any previous connect attempts + private fun reconnect() { + connectionCallback?.let { cb -> + queueConnect(true, CallbackContinuation(cb)) + } + } + fun connect(autoConnect: Boolean = false) = makeSync { queueConnect(autoConnect, it) } private fun queueReadCharacteristic( diff --git a/app/src/main/java/com/geeksville/mesh/ui/BTScanScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/BTScanScreen.kt index bca938f8..c4d20e7f 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/BTScanScreen.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/BTScanScreen.kt @@ -43,8 +43,7 @@ object ScanState : Logging { debug("stopping scan") scanner!!.stopScan(callback) callback = null - } else - debug("not stopping bt scanner") + } } }