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
|
||||
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
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<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
|
||||
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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue