kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
shitty version of the android gps code is in
rodzic
9756c5c5e3
commit
7cfcda2a30
2
TODO.md
2
TODO.md
|
@ -55,6 +55,8 @@ Do this "Signal app compatible" release relatively soon after the alpha release
|
||||||
# Medium priority
|
# Medium priority
|
||||||
Things for the betaish period.
|
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
|
* fix slow rendering warnings in play console
|
||||||
* use google signin to get user name
|
* use google signin to get user name
|
||||||
* use Firebase Test Lab
|
* use Firebase Test Lab
|
||||||
|
|
|
@ -99,6 +99,9 @@ dependencies {
|
||||||
androidTestImplementation("androidx.ui:ui-platform:$compose_version")
|
androidTestImplementation("androidx.ui:ui-platform:$compose_version")
|
||||||
androidTestImplementation("androidx.ui:ui-test:$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)
|
// For Google Sign-In (owner name accesss)
|
||||||
implementation 'com.google.android.gms:play-services-auth:17.0.0'
|
implementation 'com.google.android.gms:play-services-auth:17.0.0'
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,55 @@ import java.nio.charset.Charset
|
||||||
import java.util.*
|
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,
|
class MainActivity : AppCompatActivity(), Logging,
|
||||||
ActivityCompat.OnRequestPermissionsResultCallback {
|
ActivityCompat.OnRequestPermissionsResultCallback {
|
||||||
|
|
||||||
|
@ -51,6 +100,7 @@ class MainActivity : AppCompatActivity(), Logging,
|
||||||
debug("Checking permissions")
|
debug("Checking permissions")
|
||||||
|
|
||||||
val perms = mutableListOf(
|
val perms = mutableListOf(
|
||||||
|
Manifest.permission.ACCESS_COARSE_LOCATION,
|
||||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||||
Manifest.permission.BLUETOOTH,
|
Manifest.permission.BLUETOOTH,
|
||||||
Manifest.permission.BLUETOOTH_ADMIN,
|
Manifest.permission.BLUETOOTH_ADMIN,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package com.geeksville.mesh.service
|
package com.geeksville.mesh.service
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
|
@ -16,8 +17,11 @@ import com.geeksville.mesh.*
|
||||||
import com.geeksville.mesh.MeshProtos.MeshPacket
|
import com.geeksville.mesh.MeshProtos.MeshPacket
|
||||||
import com.geeksville.mesh.MeshProtos.ToRadio
|
import com.geeksville.mesh.MeshProtos.ToRadio
|
||||||
import com.geeksville.util.exceptionReporter
|
import com.geeksville.util.exceptionReporter
|
||||||
|
import com.geeksville.util.reportException
|
||||||
import com.geeksville.util.toOneLineString
|
import com.geeksville.util.toOneLineString
|
||||||
import com.geeksville.util.toRemoteExceptions
|
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 com.google.protobuf.ByteString
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
|
|
||||||
|
@ -44,12 +48,6 @@ class MeshService : Service(), Logging {
|
||||||
class NodeNumNotFoundException(id: Int) : Exception("NodeNum not found $id")
|
class NodeNumNotFoundException(id: Int) : Exception("NodeNum not found $id")
|
||||||
class NotInMeshException() : Exception("We are not yet in a mesh")
|
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
|
/// 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)
|
/// or null if the service could not be started (no bluetooth or no bonded device set)
|
||||||
fun startService(context: Context): Intent? {
|
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:
|
* The RECEIVED_OPAQUE:
|
||||||
|
@ -265,13 +342,17 @@ class MeshService : Service(), Logging {
|
||||||
/// BEGINNING OF MODEL - FIXME, move elsewhere
|
/// 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?
|
/// Is our radio connected to the phone?
|
||||||
private var isConnected = false
|
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
|
// The database of active nodes, index is the node number
|
||||||
private val nodeDBbyNodeNum = mutableMapOf<Int, NodeInfo>()
|
private val nodeDBbyNodeNum = mutableMapOf<Int, NodeInfo>()
|
||||||
|
|
||||||
|
@ -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
|
/// 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 {
|
private fun newMeshPacketTo(idNum: Int) = MeshPacket.newBuilder().apply {
|
||||||
from = ourNodeNum
|
if (myNodeInfo == null)
|
||||||
|
|
||||||
if (from == NODE_NUM_NO_MESH)
|
|
||||||
throw NotInMeshException()
|
|
||||||
else if (from == NODE_NUM_UNKNOWN)
|
|
||||||
throw RadioNotConnectedException()
|
throw RadioNotConnectedException()
|
||||||
|
|
||||||
|
from = myNodeInfo!!.myNodeNum
|
||||||
to = idNum
|
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
|
/// Update our model and resend as needed for a MeshPacket we just received from the radio
|
||||||
private fun handleReceivedMeshPacket(packet: MeshPacket) {
|
private fun handleReceivedMeshPacket(packet: MeshPacket) {
|
||||||
val fromNum = packet.from
|
val fromNum = packet.from
|
||||||
|
@ -416,13 +505,8 @@ class MeshService : Service(), Logging {
|
||||||
|
|
||||||
when (p.variantCase.number) {
|
when (p.variantCase.number) {
|
||||||
MeshProtos.SubPacket.POSITION_FIELD_NUMBER ->
|
MeshProtos.SubPacket.POSITION_FIELD_NUMBER ->
|
||||||
updateNodeInfo(fromNum) {
|
handleReceivedPosition(fromNum, p.position)
|
||||||
it.position = Position(
|
|
||||||
p.position.latitude,
|
|
||||||
p.position.longitude,
|
|
||||||
p.position.altitude
|
|
||||||
)
|
|
||||||
}
|
|
||||||
MeshProtos.SubPacket.DATA_FIELD_NUMBER ->
|
MeshProtos.SubPacket.DATA_FIELD_NUMBER ->
|
||||||
handleReceivedData(fromNum, p.data)
|
handleReceivedData(fromNum, p.data)
|
||||||
|
|
||||||
|
@ -447,7 +531,10 @@ class MeshService : Service(), Logging {
|
||||||
val myInfo = MeshProtos.MyNodeInfo.parseFrom(
|
val myInfo = MeshProtos.MyNodeInfo.parseFrom(
|
||||||
connectedRadio.readMyNode()
|
connectedRadio.readMyNode()
|
||||||
)
|
)
|
||||||
ourNodeNum = myInfo.myNodeNum
|
|
||||||
|
|
||||||
|
val mynodeinfo = MyNodeInfo(myInfo.myNodeNum, myInfo.hasGps)
|
||||||
|
myNodeInfo = mynodeinfo
|
||||||
|
|
||||||
// Ask for the current node DB
|
// Ask for the current node DB
|
||||||
connectedRadio.restartNodeInfo()
|
connectedRadio.restartNodeInfo()
|
||||||
|
@ -482,6 +569,15 @@ class MeshService : Service(), Logging {
|
||||||
// advance to next
|
// advance to next
|
||||||
infoBytes = connectedRadio.readNodeInfo()
|
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() {
|
private val binder = object : IMeshService.Stub() {
|
||||||
// Note: bound methods don't get properly exception caught/logged, so do that with a wrapper
|
// 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
|
// per https://blog.classycode.com/dealing-with-exceptions-in-aidl-9ba904c6d63
|
||||||
|
@ -547,8 +671,8 @@ class MeshService : Service(), Logging {
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
// Also update our own map for our nodenum, by handling the packet just like packets from other users
|
// Also update our own map for our nodenum, by handling the packet just like packets from other users
|
||||||
if (ourNodeNum >= 0) {
|
if (myNodeInfo != null) {
|
||||||
handleReceivedUser(ourNodeNum, user)
|
handleReceivedUser(myNodeInfo!!.myNodeNum, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
// set my owner info
|
// set my owner info
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package com.geeksville.mesh.ui
|
package com.geeksville.mesh.ui
|
||||||
|
|
||||||
import androidx.compose.Composable
|
import androidx.compose.Composable
|
||||||
import androidx.ui.core.Modifier
|
|
||||||
import androidx.ui.core.Text
|
import androidx.ui.core.Text
|
||||||
import androidx.ui.layout.*
|
import androidx.ui.layout.*
|
||||||
import androidx.ui.material.EmphasisLevels
|
import androidx.ui.material.EmphasisLevels
|
||||||
|
@ -11,34 +10,35 @@ import androidx.ui.tooling.preview.Preview
|
||||||
import androidx.ui.unit.dp
|
import androidx.ui.unit.dp
|
||||||
import com.geeksville.mesh.NodeInfo
|
import com.geeksville.mesh.NodeInfo
|
||||||
import com.geeksville.mesh.R
|
import com.geeksville.mesh.R
|
||||||
|
import androidx.ui.core.Modifier as Modifier1
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NodeIcon(modifier: Modifier = Modifier.None, node: NodeInfo) {
|
fun NodeIcon(modifier: Modifier1 = Modifier1.None, node: NodeInfo) {
|
||||||
Column {
|
Column {
|
||||||
Container(modifier = modifier + LayoutSize(40.dp, 40.dp)) {
|
Container(modifier = modifier + LayoutSize(40.dp, 40.dp)) {
|
||||||
VectorImage(id = if (node.user?.shortName != null) R.drawable.person else R.drawable.help)
|
VectorImage(id = if (node.user?.shortName != null) R.drawable.person else R.drawable.help)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show our shortname if possible
|
// Show our shortname if possible
|
||||||
node.user?.shortName?.let {
|
/* node.user?.shortName?.let {
|
||||||
Text(it)
|
Text(it)
|
||||||
}
|
} */
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CompassHeading(modifier: Modifier = Modifier.None, node: NodeInfo) {
|
fun CompassHeading(modifier: Modifier1 = Modifier1.None, node: NodeInfo) {
|
||||||
Column {
|
Column {
|
||||||
if (node.position != null) {
|
if (node.position != null) {
|
||||||
Container(modifier = modifier + LayoutSize(40.dp, 40.dp)) {
|
Container(modifier = modifier + LayoutSize(40.dp, 40.dp)) {
|
||||||
VectorImage(id = R.drawable.navigation)
|
VectorImage(id = R.drawable.navigation)
|
||||||
}
|
}
|
||||||
Text("2.3 km")
|
|
||||||
} else Container(modifier = modifier + LayoutSize(40.dp, 40.dp)) {
|
} else Container(modifier = modifier + LayoutSize(40.dp, 40.dp)) {
|
||||||
VectorImage(id = R.drawable.help)
|
VectorImage(id = R.drawable.help)
|
||||||
}
|
}
|
||||||
|
Text("2.3 km") // always reserve space for the distance even if we aren't showing it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Ładowanie…
Reference in New Issue