From 7cfcda2a30c459940b6ef08349000043f44e1975 Mon Sep 17 00:00:00 2001 From: geeksville Date: Sun, 16 Feb 2020 14:22:24 -0800 Subject: [PATCH] shitty version of the android gps code is in --- TODO.md | 2 + app/build.gradle | 3 + .../java/com/geeksville/mesh/MainActivity.kt | 50 +++++ .../geeksville/mesh/service/MeshService.kt | 174 +++++++++++++++--- .../com/geeksville/mesh/ui/NodeInfoCard.kt | 12 +- 5 files changed, 210 insertions(+), 31 deletions(-) diff --git a/TODO.md b/TODO.md index 7f2cb9ee0..329915820 100644 --- a/TODO.md +++ b/TODO.md @@ -55,6 +55,8 @@ Do this "Signal app compatible" release relatively soon after the alpha release # Medium priority Things for the betaish period. +* only publish gps positions once every 5 mins while we are connected to our radio _and_ someone else is in the mesh +* Do PRIORITY_BALANCED_POWER_ACCURACY for our gps updates when no one in the mesh is nearer than 200 meters * fix slow rendering warnings in play console * use google signin to get user name * use Firebase Test Lab diff --git a/app/build.gradle b/app/build.gradle index e1ffd777b..f44edde97 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -99,6 +99,9 @@ dependencies { androidTestImplementation("androidx.ui:ui-platform:$compose_version") androidTestImplementation("androidx.ui:ui-test:$compose_version") + // location services + implementation 'com.google.android.gms:play-services-location:17.0.0' + // For Google Sign-In (owner name accesss) implementation 'com.google.android.gms:play-services-auth:17.0.0' diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index 695c40bd6..181a62da5 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -30,6 +30,55 @@ import java.nio.charset.Charset import java.util.* +/* +UI design + +material setup instructions: https://material.io/develop/android/docs/getting-started/ +dark theme (or use system eventually) https://material.io/develop/android/theming/dark/ + +NavDrawer is a standard draw which can be dragged in from the left or the menu icon inside the app +title. + +Fragments: + +SettingsFragment shows + username + shortname + bluetooth pairing list + (eventually misc device settings that are not channel related) + +Channel fragment + qr code, copy link button + ch number + misc other settings + (eventually a way of choosing between past channels) + +ChatFragment + a text box to enter new texts + a scrolling list of rows. each row is a text and a sender info layout + +NodeListFragment + a node info row for every node + +ViewModels: + + BTScanModel starts/stops bt scan and provides list of devices (manages entire scan lifecycle) + + MeshModel contains: (manages entire service relationship) + current received texts + current radio macaddr + current node infos (updated dynamically) + +eventually use bottom navigation bar to switch between, Members, Chat, Channel, Settings. https://material.io/develop/android/components/bottom-navigation-view/ + use numbers of # chat messages and # of members in the badges. + +(per this recommendation to not use top tabs: https://ux.stackexchange.com/questions/102439/android-ux-when-to-use-bottom-navigation-and-when-to-use-tabs ) + + +eventually: + make a custom theme: https://github.com/material-components/material-components-android/tree/master/material-theme-builder +*/ + class MainActivity : AppCompatActivity(), Logging, ActivityCompat.OnRequestPermissionsResultCallback { @@ -51,6 +100,7 @@ class MainActivity : AppCompatActivity(), Logging, debug("Checking permissions") val perms = mutableListOf( + Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.BLUETOOTH, Manifest.permission.BLUETOOTH_ADMIN, 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 f5263fc7a..e0da3b92f 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -1,5 +1,6 @@ package com.geeksville.mesh.service +import android.annotation.SuppressLint import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager @@ -16,8 +17,11 @@ import com.geeksville.mesh.* import com.geeksville.mesh.MeshProtos.MeshPacket import com.geeksville.mesh.MeshProtos.ToRadio import com.geeksville.util.exceptionReporter +import com.geeksville.util.reportException import com.geeksville.util.toOneLineString import com.geeksville.util.toRemoteExceptions +import com.google.android.gms.common.api.ResolvableApiException +import com.google.android.gms.location.* import com.google.protobuf.ByteString import java.nio.charset.Charset @@ -44,12 +48,6 @@ class MeshService : Service(), Logging { class NodeNumNotFoundException(id: Int) : Exception("NodeNum not found $id") class NotInMeshException() : Exception("We are not yet in a mesh") - /// If we haven't yet received a node number from the radio - private const val NODE_NUM_UNKNOWN = -2 - - /// If the radio hasn't yet joined a mesh (i.e. no nodenum assigned) - private const val NODE_NUM_NO_MESH = -1 - /// 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? { @@ -106,6 +104,85 @@ class MeshService : Service(), Logging { } } + private val locationCallback = object : LocationCallback() { + override fun onLocationResult(locationResult: LocationResult) { + super.onLocationResult(locationResult) + var l = locationResult.lastLocation + + // Docs say lastLocation should always be !null if there are any locations, but that's not the case + if (l == null) { + // try to only look at the accurate locations + val locs = + locationResult.locations.filter { !it.hasAccuracy() || it.accuracy < 200 } + l = locs.lastOrNull() + } + if (l != null) { + info("got location $l") + if (l.hasAccuracy() && l.accuracy >= 200) // if more than 200 meters off we won't use it + warn("accuracy ${l.accuracy} is too poor to use") + else { + sendPosition(l.latitude, l.longitude, l.altitude.toInt()) + } + } + + } + } + + private var fusedLocationClient: FusedLocationProviderClient? = null + + /** + * start our location requests + * + * per https://developer.android.com/training/location/change-location-settings + */ + @SuppressLint("MissingPermission") + private fun startLocationRequests() { + val request = LocationRequest.create().apply { + interval = + 60 * 1000 // FIXME, do more like once every 5 mins while we are connected to our radio _and_ someone else is in the mesh + + priority = LocationRequest.PRIORITY_HIGH_ACCURACY + } + val builder = LocationSettingsRequest.Builder().addLocationRequest(request) + val locationClient = LocationServices.getSettingsClient(this) + val locationSettingsResponse = locationClient.checkLocationSettings(builder.build()) + + locationSettingsResponse.addOnSuccessListener { + debug("We are now successfully listening to the GPS") + } + + locationSettingsResponse.addOnFailureListener { exception -> + error("Failed to listen to GPS") + if (exception is ResolvableApiException) { + exceptionReporter { + // Location settings are not satisfied, but this can be fixed + // by showing the user a dialog. + + // FIXME + // Show the dialog by calling startResolutionForResult(), + // and check the result in onActivityResult(). + /* exception.startResolutionForResult( + this@MainActivity, + REQUEST_CHECK_SETTINGS + ) */ + } + } else + reportException(exception) + } + + val client = LocationServices.getFusedLocationProviderClient(this) + + + // FIXME - should we use Looper.myLooper() in the third param per https://github.com/android/location-samples/blob/432d3b72b8c058f220416958b444274ddd186abd/LocationUpdatesForegroundService/app/src/main/java/com/google/android/gms/location/sample/locationupdatesforegroundservice/LocationUpdatesService.java + client.requestLocationUpdates(request, locationCallback, null) + + fusedLocationClient = client + } + + private fun stopLocationRequests() { + fusedLocationClient?.removeLocationUpdates(locationCallback) + fusedLocationClient = null + } /** * The RECEIVED_OPAQUE: @@ -265,13 +342,17 @@ class MeshService : Service(), Logging { /// BEGINNING OF MODEL - FIXME, move elsewhere /// + /// special broadcast address + val NODENUM_BROADCAST = 255 + + // MyNodeInfo sent via special protobuf from radio + data class MyNodeInfo(val myNodeNum: Int, val hasGPS: Boolean) + + var myNodeInfo: MyNodeInfo? = null + /// Is our radio connected to the phone? private var isConnected = false - /// We learn this from the node db sent by the device - it is stable for the entire session - private var ourNodeNum = - NODE_NUM_UNKNOWN - // The database of active nodes, index is the node number private val nodeDBbyNodeNum = mutableMapOf() @@ -324,13 +405,10 @@ class MeshService : Service(), Logging { /// Generate a new mesh packet builder with our node as the sender, and the specified node num private fun newMeshPacketTo(idNum: Int) = MeshPacket.newBuilder().apply { - from = ourNodeNum - - if (from == NODE_NUM_NO_MESH) - throw NotInMeshException() - else if (from == NODE_NUM_UNKNOWN) + if (myNodeInfo == null) throw RadioNotConnectedException() + from = myNodeInfo!!.myNodeNum to = idNum } @@ -397,6 +475,17 @@ class MeshService : Service(), Logging { } } + /// Update our DB of users based on someone sending out a Position subpacket + private fun handleReceivedPosition(fromNum: Int, p: MeshProtos.Position) { + updateNodeInfo(fromNum) { + it.position = Position( + p.latitude, + p.longitude, + p.altitude + ) + } + } + /// Update our model and resend as needed for a MeshPacket we just received from the radio private fun handleReceivedMeshPacket(packet: MeshPacket) { val fromNum = packet.from @@ -416,13 +505,8 @@ class MeshService : Service(), Logging { when (p.variantCase.number) { MeshProtos.SubPacket.POSITION_FIELD_NUMBER -> - updateNodeInfo(fromNum) { - it.position = Position( - p.position.latitude, - p.position.longitude, - p.position.altitude - ) - } + handleReceivedPosition(fromNum, p.position) + MeshProtos.SubPacket.DATA_FIELD_NUMBER -> handleReceivedData(fromNum, p.data) @@ -447,7 +531,10 @@ class MeshService : Service(), Logging { val myInfo = MeshProtos.MyNodeInfo.parseFrom( connectedRadio.readMyNode() ) - ourNodeNum = myInfo.myNodeNum + + + val mynodeinfo = MyNodeInfo(myInfo.myNodeNum, myInfo.hasGps) + myNodeInfo = mynodeinfo // Ask for the current node DB connectedRadio.restartNodeInfo() @@ -482,6 +569,15 @@ class MeshService : Service(), Logging { // advance to next infoBytes = connectedRadio.readNodeInfo() } + + // we don't ask for GPS locations from android if our device has a built in GPS + if (!mynodeinfo.hasGPS) + startLocationRequests() + else + debug("Our radio has a built in GPS, so not reading GPS in phone") + } else { + // lost radio connection, therefore no need to keep listening to GPS + stopLocationRequests() } } @@ -527,6 +623,34 @@ class MeshService : Service(), Logging { } } + /// Send a position (typically from our built in GPS) into the mesh + private fun sendPosition(lat: Double, lon: Double, alt: Int) { + debug("Sending our position into mesh lat=$lat, lon=$lon, alt=$alt") + + val destNum = NODENUM_BROADCAST + + val position = MeshProtos.Position.newBuilder().also { + it.latitude = lat + it.longitude = lon + it.altitude = alt + }.build() + + // encapsulate our payload in the proper protobufs and fire it off + val packet = newMeshPacketTo(destNum) + + packet.payload = MeshProtos.SubPacket.newBuilder().also { + it.position = position + }.build() + + // Also update our own map for our nodenum, by handling the packet just like packets from other users + handleReceivedPosition(myNodeInfo!!.myNodeNum, position) + + // send the packet into the mesh + sendToRadio(ToRadio.newBuilder().apply { + this.packet = packet.build() + }) + } + private val binder = object : IMeshService.Stub() { // 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 @@ -547,8 +671,8 @@ class MeshService : Service(), Logging { }.build() // Also update our own map for our nodenum, by handling the packet just like packets from other users - if (ourNodeNum >= 0) { - handleReceivedUser(ourNodeNum, user) + if (myNodeInfo != null) { + handleReceivedUser(myNodeInfo!!.myNodeNum, user) } // set my owner info diff --git a/app/src/main/java/com/geeksville/mesh/ui/NodeInfoCard.kt b/app/src/main/java/com/geeksville/mesh/ui/NodeInfoCard.kt index b5f32dad3..ff0597fd4 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/NodeInfoCard.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/NodeInfoCard.kt @@ -1,7 +1,6 @@ package com.geeksville.mesh.ui import androidx.compose.Composable -import androidx.ui.core.Modifier import androidx.ui.core.Text import androidx.ui.layout.* import androidx.ui.material.EmphasisLevels @@ -11,34 +10,35 @@ import androidx.ui.tooling.preview.Preview import androidx.ui.unit.dp import com.geeksville.mesh.NodeInfo import com.geeksville.mesh.R +import androidx.ui.core.Modifier as Modifier1 @Composable -fun NodeIcon(modifier: Modifier = Modifier.None, node: NodeInfo) { +fun NodeIcon(modifier: Modifier1 = Modifier1.None, node: NodeInfo) { Column { Container(modifier = modifier + LayoutSize(40.dp, 40.dp)) { VectorImage(id = if (node.user?.shortName != null) R.drawable.person else R.drawable.help) } // Show our shortname if possible - node.user?.shortName?.let { + /* node.user?.shortName?.let { Text(it) - } + } */ } } @Composable -fun CompassHeading(modifier: Modifier = Modifier.None, node: NodeInfo) { +fun CompassHeading(modifier: Modifier1 = Modifier1.None, node: NodeInfo) { Column { if (node.position != null) { Container(modifier = modifier + LayoutSize(40.dp, 40.dp)) { VectorImage(id = R.drawable.navigation) } - Text("2.3 km") } else Container(modifier = modifier + LayoutSize(40.dp, 40.dp)) { VectorImage(id = R.drawable.help) } + Text("2.3 km") // always reserve space for the distance even if we aren't showing it } }