mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
refactored MeshService
- moved MeshService start-related code to `MeshServiceStarter` - moved `LocationCallback` to `MeshServiceLocationCallback` - coroutine scope is now handled in `MeshService` positio ncallback. - refactored `onLocationResult()` to be easier to read - created `MeshServiceNotifications` for creating and updating of notifications - moved `SavedSettings` to `MeshServiceSettingsData`
This commit is contained in:
parent
f9e39b66a4
commit
3610f0b53e
9 changed files with 434 additions and 347 deletions
|
|
@ -845,7 +845,7 @@ class MainActivity : AppCompatActivity(), Logging,
|
|||
}
|
||||
|
||||
// ALSO bind so we can use the api
|
||||
mesh.connect(this, MeshService.intent, Context.BIND_AUTO_CREATE + Context.BIND_ABOVE_CLIENT)
|
||||
mesh.connect(this, MeshService.createIntent(), Context.BIND_AUTO_CREATE + Context.BIND_ABOVE_CLIENT)
|
||||
}
|
||||
|
||||
private fun unbindMeshService() {
|
||||
|
|
@ -942,5 +942,3 @@ class MainActivity : AppCompatActivity(), Logging,
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.geeksville.mesh.android
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.bluetooth.BluetoothManager
|
||||
import android.content.Context
|
||||
import android.hardware.usb.UsbManager
|
||||
|
|
@ -10,3 +11,5 @@ import android.hardware.usb.UsbManager
|
|||
val Context.bluetoothManager: BluetoothManager? get() = getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager?
|
||||
|
||||
val Context.usbManager: UsbManager get() = requireNotNull(getSystemService(Context.USB_SERVICE) as? UsbManager?) { "USB_SERVICE is not available"}
|
||||
|
||||
val Context.notificationManager: NotificationManager get() = requireNotNull(getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager?)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,6 @@ import com.geeksville.android.Logging
|
|||
class BootCompleteReceiver : BroadcastReceiver(), Logging {
|
||||
override fun onReceive(mContext: Context, intent: Intent) {
|
||||
// start listening for bluetooth messages from our device
|
||||
MeshService.startLater(mContext)
|
||||
MeshService.startServiceLater(mContext)
|
||||
}
|
||||
}
|
||||
|
|
@ -6,18 +6,11 @@ import android.content.BroadcastReceiver
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.Parcelable
|
||||
import android.os.RemoteException
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.annotation.UiThread
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationCompat.PRIORITY_MIN
|
||||
import androidx.core.content.edit
|
||||
import androidx.work.*
|
||||
import com.geeksville.analytics.DataPair
|
||||
import com.geeksville.android.GeeksvilleApplication
|
||||
import com.geeksville.android.Logging
|
||||
|
|
@ -38,18 +31,16 @@ import com.google.android.gms.location.*
|
|||
import com.google.protobuf.ByteString
|
||||
import com.google.protobuf.InvalidProtocolBufferException
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
|
||||
/**
|
||||
* Handles all the communication with android apps. Also keeps an internal model
|
||||
* of the network state.
|
||||
*
|
||||
* Note: this service will go away once all clients are unbound from it.
|
||||
* Warning: do not override toString, it causes infinite recursion on some androids (because contextWrapper.getResources calls to string
|
||||
*/
|
||||
class MeshService : Service(), Logging {
|
||||
|
||||
|
|
@ -66,23 +57,6 @@ class MeshService : Service(), Logging {
|
|||
|
||||
class IdNotFoundException(id: String) : Exception("ID not found $id")
|
||||
class NodeNumNotFoundException(id: Int) : Exception("NodeNum not found $id")
|
||||
// class NotInMeshException() : Exception("We are not yet in a mesh")
|
||||
|
||||
/** A little helper that just calls startService
|
||||
*/
|
||||
class ServiceStarter(appContext: Context, workerParams: WorkerParameters) :
|
||||
Worker(appContext, workerParams) {
|
||||
|
||||
override fun doWork(): Result = try {
|
||||
startService(this.applicationContext)
|
||||
|
||||
// Indicate whether the task finished successfully with the Result
|
||||
Result.success()
|
||||
} catch (ex: Exception) {
|
||||
errormsg("failure starting service, will retry", ex)
|
||||
Result.retry()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Talk to our running service and try to set a new device address. And then immediately
|
||||
|
|
@ -93,53 +67,10 @@ class MeshService : Service(), Logging {
|
|||
startService(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Just after boot the android OS is super busy, so if we call startForegroundService then, our
|
||||
* thread might be stalled long enough to expose this google/samsung bug:
|
||||
* https://issuetracker.google.com/issues/76112072#comment56
|
||||
*/
|
||||
fun startLater(context: Context) {
|
||||
// No point in even starting the service if the user doesn't have a device bonded
|
||||
info("Received boot complete announcement, starting mesh service in two minutes")
|
||||
val delayRequest = OneTimeWorkRequestBuilder<ServiceStarter>()
|
||||
.setInitialDelay(2, TimeUnit.MINUTES)
|
||||
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 2, TimeUnit.MINUTES)
|
||||
.addTag("startLater")
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance(context).enqueue(delayRequest)
|
||||
}
|
||||
|
||||
val intent = Intent().apply {
|
||||
setClassName(
|
||||
"com.geeksville.mesh",
|
||||
"com.geeksville.mesh.service.MeshService"
|
||||
)
|
||||
}
|
||||
|
||||
/// Helper function to start running our service
|
||||
fun startService(context: Context) {
|
||||
// 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
|
||||
|
||||
|
||||
// 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.
|
||||
|
||||
info("Trying to start service")
|
||||
val compName =
|
||||
(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(intent)
|
||||
} else {
|
||||
context.startService(intent)
|
||||
})
|
||||
|
||||
if (compName == null)
|
||||
throw Exception("Failed to start service")
|
||||
}
|
||||
fun createIntent() = Intent().setClassName(
|
||||
"com.geeksville.mesh",
|
||||
"com.geeksville.mesh.service.MeshService"
|
||||
)
|
||||
}
|
||||
|
||||
enum class ConnectionState {
|
||||
|
|
@ -150,88 +81,50 @@ class MeshService : Service(), Logging {
|
|||
|
||||
/// A mapping of receiver class name to package name - used for explicit broadcasts
|
||||
private val clientPackages = mutableMapOf<String, String>()
|
||||
|
||||
val radio = ServiceClient {
|
||||
val stub = IRadioInterfaceService.Stub.asInterface(it)
|
||||
|
||||
// Now that we are connected to the radio service, tell it to connect to the radio
|
||||
stub.connect()
|
||||
|
||||
stub
|
||||
}
|
||||
|
||||
private val serviceNotifications = MeshServiceNotifications(this)
|
||||
private val serviceBroadcasts = MeshServiceBroadcasts(this, clientPackages) { connectionState }
|
||||
private val serviceJob = Job()
|
||||
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
|
||||
|
||||
/// The current state of our connection
|
||||
private var connectionState = ConnectionState.DISCONNECTED
|
||||
|
||||
private var packetRepo: PacketRepository? = null
|
||||
|
||||
/*
|
||||
see com.geeksville.mesh broadcast intents
|
||||
// RECEIVED_OPAQUE for data received from other nodes
|
||||
// NODE_CHANGE for new IDs appearing or disappearing
|
||||
// ACTION_MESH_CONNECTED for losing/gaining connection to the packet radio (note, this is not
|
||||
the same as RadioInterfaceService.RADIO_CONNECTED_ACTION, because it implies we have assembled a valid
|
||||
node db.
|
||||
*/
|
||||
|
||||
private fun explicitBroadcast(intent: Intent) {
|
||||
sendBroadcast(intent) // We also do a regular (not explicit broadcast) so any context-registered rceivers will work
|
||||
clientPackages.forEach {
|
||||
intent.setClassName(it.value, it.key)
|
||||
sendBroadcast(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private val locationCallback = object : LocationCallback() {
|
||||
private var lastSendMsec = 0L
|
||||
|
||||
override fun onLocationResult(locationResult: LocationResult) {
|
||||
serviceScope.handledLaunch {
|
||||
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 phone location")
|
||||
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 {
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
// we limit our sends onto the lora net to a max one once every FIXME
|
||||
val sendLora = (now - lastSendMsec >= 30 * 1000)
|
||||
if (sendLora)
|
||||
lastSendMsec = now
|
||||
try {
|
||||
sendPosition(
|
||||
l.latitude, l.longitude, l.altitude.toInt(),
|
||||
destNum = if (sendLora) NODENUM_BROADCAST else myNodeNum,
|
||||
wantResponse = sendLora
|
||||
)
|
||||
} catch (ex: RemoteException) { // Really a RadioNotConnected exception, but it has changed into this type via remoting
|
||||
warn("Lost connection to radio, stopping location requests")
|
||||
onConnectionChanged(ConnectionState.DEVICE_SLEEP)
|
||||
} catch (ex: BLEException) { // Really a RadioNotConnected exception, but it has changed into this type via remoting
|
||||
warn("BLE exception, stopping location requests $ex")
|
||||
onConnectionChanged(ConnectionState.DEVICE_SLEEP)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var fusedLocationClient: FusedLocationProviderClient? = null
|
||||
|
||||
val radio = ServiceClient {
|
||||
IRadioInterfaceService.Stub.asInterface(it).apply {
|
||||
// Now that we are connected to the radio service, tell it to connect to the radio
|
||||
connect()
|
||||
}
|
||||
}
|
||||
|
||||
private val locationCallback = MeshServiceLocationCallback(
|
||||
::sendPositionScoped,
|
||||
onSendPositionFailed = { onConnectionChanged(ConnectionState.DEVICE_SLEEP) },
|
||||
getNodeNum = { myNodeNum }
|
||||
)
|
||||
|
||||
private fun getSenderName(): String {
|
||||
val recentFrom = recentReceivedTextPacket?.from // safe, immutable copy
|
||||
return if (recentFrom != null) {
|
||||
nodeDBbyID[recentFrom]?.user?.longName
|
||||
?: recentFrom
|
||||
} else {
|
||||
getString(R.string.unknown_username)
|
||||
}
|
||||
}
|
||||
|
||||
/// A text message that has a arrived since the last notification update
|
||||
private var recentReceivedTextPacket: DataPacket? = null
|
||||
|
||||
private val notificationSummary
|
||||
get() = when (connectionState) {
|
||||
ConnectionState.CONNECTED -> getString(R.string.connected_count).format(
|
||||
numOnlineNodes,
|
||||
numNodes
|
||||
)
|
||||
ConnectionState.DISCONNECTED -> getString(R.string.disconnected)
|
||||
ConnectionState.DEVICE_SLEEP -> getString(R.string.device_sleeping)
|
||||
}
|
||||
|
||||
private fun warnUserAboutLocation() {
|
||||
Toast.makeText(
|
||||
this,
|
||||
|
|
@ -319,37 +212,6 @@ class MeshService : Service(), Logging {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The RECEIVED_OPAQUE:
|
||||
* Payload will be a DataPacket
|
||||
*/
|
||||
private fun broadcastReceivedData(payload: DataPacket) {
|
||||
val intent = Intent(ACTION_RECEIVED_DATA)
|
||||
intent.putExtra(EXTRA_PAYLOAD, payload)
|
||||
explicitBroadcast(intent)
|
||||
}
|
||||
|
||||
private fun broadcastNodeChange(info: NodeInfo) {
|
||||
debug("Broadcasting node change $info")
|
||||
val intent = Intent(ACTION_NODE_CHANGE)
|
||||
|
||||
intent.putExtra(EXTRA_NODEINFO, info)
|
||||
explicitBroadcast(intent)
|
||||
}
|
||||
|
||||
private fun broadcastMessageStatus(p: DataPacket) {
|
||||
if (p.id == 0) {
|
||||
debug("Ignoring anonymous packet status")
|
||||
} else {
|
||||
debug("Broadcasting message status $p")
|
||||
val intent = Intent(ACTION_MESSAGE_STATUS)
|
||||
|
||||
intent.putExtra(EXTRA_PACKET_ID, p.id)
|
||||
intent.putExtra(EXTRA_STATUS, p.status as Parcelable)
|
||||
explicitBroadcast(intent)
|
||||
}
|
||||
}
|
||||
|
||||
/// Safely access the radio service, if not connected an exception will be thrown
|
||||
private val connectedRadio: IRadioInterfaceService
|
||||
get() = (if (connectionState == ConnectionState.CONNECTED) radio.serviceP else null)
|
||||
|
|
@ -372,98 +234,7 @@ class MeshService : Service(), Logging {
|
|||
})
|
||||
}
|
||||
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun createNotificationChannel(): String {
|
||||
val channelId = "my_service"
|
||||
val channelName = getString(R.string.meshtastic_service_notifications)
|
||||
val chan = NotificationChannel(
|
||||
channelId,
|
||||
channelName, NotificationManager.IMPORTANCE_HIGH
|
||||
)
|
||||
chan.lightColor = Color.BLUE
|
||||
chan.importance = NotificationManager.IMPORTANCE_NONE
|
||||
chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
|
||||
notificationManager.createNotificationChannel(chan)
|
||||
return channelId
|
||||
}
|
||||
|
||||
private val notifyId = 101
|
||||
val notificationManager: NotificationManager by lazy() {
|
||||
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
}
|
||||
|
||||
/// This must be lazy because we use Context
|
||||
private val channelId: String by lazy() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
createNotificationChannel()
|
||||
} else {
|
||||
// If earlier version channel ID is not used
|
||||
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
private val openAppIntent: PendingIntent by lazy() {
|
||||
PendingIntent.getActivity(this, 0, Intent(this, MainActivity::class.java), 0)
|
||||
}
|
||||
|
||||
/// A text message that has a arrived since the last notification update
|
||||
private var recentReceivedText: DataPacket? = null
|
||||
|
||||
private val summaryString
|
||||
get() = when (connectionState) {
|
||||
ConnectionState.CONNECTED -> getString(R.string.connected_count).format(
|
||||
numOnlineNodes,
|
||||
numNodes
|
||||
)
|
||||
ConnectionState.DISCONNECTED -> getString(R.string.disconnected)
|
||||
ConnectionState.DEVICE_SLEEP -> getString(R.string.device_sleeping)
|
||||
}
|
||||
|
||||
|
||||
// Note: do not override toString, it causes infinite recursion on some androids (because contextWrapper.getResources calls to string)
|
||||
// override fun toString() = summaryString
|
||||
|
||||
/**
|
||||
* Generate a new version of our notification - reflecting current app state
|
||||
*/
|
||||
private fun createNotification(): Notification {
|
||||
|
||||
val notificationBuilder = NotificationCompat.Builder(this, channelId)
|
||||
|
||||
val builder = notificationBuilder.setOngoing(true)
|
||||
.setPriority(PRIORITY_MIN)
|
||||
.setCategory(if (recentReceivedText != null) Notification.CATEGORY_SERVICE else Notification.CATEGORY_MESSAGE)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_data_bluetooth)
|
||||
.setContentTitle(summaryString) // leave this off for now so our notification looks smaller
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setContentIntent(openAppIntent)
|
||||
|
||||
// FIXME, show information about the nearest node
|
||||
// if(shortContent != null) builder.setContentText(shortContent)
|
||||
|
||||
// If a text message arrived include it with our notification
|
||||
recentReceivedText?.let { packet ->
|
||||
// Try to show the human name of the sender if possible
|
||||
val sender = nodeDBbyID[packet.from]?.user?.longName ?: packet.from
|
||||
builder.setContentText("Message from $sender")
|
||||
|
||||
builder.setStyle(
|
||||
NotificationCompat.BigTextStyle()
|
||||
.bigText(packet.bytes!!.toString(utf8))
|
||||
)
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update our notification with latest data
|
||||
*/
|
||||
private fun updateNotification() {
|
||||
notificationManager.notify(notifyId, createNotification())
|
||||
}
|
||||
private fun updateNotification() = serviceNotifications.updateNotification(recentReceivedTextPacket, notificationSummary, getSenderName())
|
||||
|
||||
/**
|
||||
* tell android not to kill us
|
||||
|
|
@ -475,10 +246,12 @@ class MeshService : Service(), Logging {
|
|||
info("Requesting foreground service=$wantForeground")
|
||||
|
||||
// We always start foreground because that's how our service is always started (if we didn't then android would kill us)
|
||||
// but if we don't really need forground we immediately stop it.
|
||||
startForeground(notifyId, createNotification())
|
||||
if (!wantForeground)
|
||||
// but if we don't really need foreground we immediately stop it.
|
||||
val notification = serviceNotifications.createNotification(recentReceivedTextPacket, notificationSummary, getSenderName())
|
||||
startForeground(serviceNotifications.notifyId, notification)
|
||||
if (!wantForeground) {
|
||||
stopForeground(true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
|
|
@ -494,9 +267,10 @@ class MeshService : Service(), Logging {
|
|||
loadSettings() // Load our last known node DB
|
||||
|
||||
// we listen for messages from the radio receiver _before_ trying to create the service
|
||||
val filter = IntentFilter()
|
||||
filter.addAction(RadioInterfaceService.RECEIVE_FROMRADIO_ACTION)
|
||||
filter.addAction(RadioInterfaceService.RADIO_CONNECTED_ACTION)
|
||||
val filter = IntentFilter().apply {
|
||||
addAction(RadioInterfaceService.RECEIVE_FROMRADIO_ACTION)
|
||||
addAction(RadioInterfaceService.RADIO_CONNECTED_ACTION)
|
||||
}
|
||||
registerReceiver(radioInterfaceReceiver, filter)
|
||||
|
||||
// We in turn need to use the radiointerface service
|
||||
|
|
@ -541,51 +315,22 @@ class MeshService : Service(), Logging {
|
|||
serviceJob.cancel()
|
||||
}
|
||||
|
||||
|
||||
///
|
||||
/// BEGINNING OF MODEL - FIXME, move elsewhere
|
||||
///
|
||||
|
||||
/// Our saved preferences as stored on disk
|
||||
@Serializable
|
||||
private data class SavedSettings(
|
||||
val nodeDB: Array<NodeInfo>,
|
||||
val myInfo: MyNodeInfo,
|
||||
val messages: Array<DataPacket>
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as SavedSettings
|
||||
|
||||
if (!nodeDB.contentEquals(other.nodeDB)) return false
|
||||
if (myInfo != other.myInfo) return false
|
||||
if (!messages.contentEquals(other.messages)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = nodeDB.contentHashCode()
|
||||
result = 31 * result + myInfo.hashCode()
|
||||
result = 31 * result + messages.contentHashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPrefs() = getSharedPreferences("service-prefs", Context.MODE_PRIVATE)
|
||||
|
||||
/// Save information about our mesh to disk, so we will have it when we next start the service (even before we hear from our device)
|
||||
private fun saveSettings() {
|
||||
myNodeInfo?.let { myInfo ->
|
||||
val settings = SavedSettings(
|
||||
val settings = MeshServiceSettingsData(
|
||||
myInfo = myInfo,
|
||||
nodeDB = nodeDBbyNodeNum.values.toTypedArray(),
|
||||
messages = recentDataPackets.toTypedArray()
|
||||
)
|
||||
val json = Json { isLenient = true }
|
||||
val asString = json.encodeToString(SavedSettings.serializer(), settings)
|
||||
val asString = json.encodeToString(MeshServiceSettingsData.serializer(), settings)
|
||||
debug("Saving settings")
|
||||
getPrefs().edit(commit = true) {
|
||||
// FIXME, not really ideal to store this bigish blob in preferences
|
||||
|
|
@ -594,9 +339,6 @@ class MeshService : Service(), Logging {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a new node DB
|
||||
*/
|
||||
private fun installNewNodeDB(newMyNodeInfo: MyNodeInfo, nodes: Array<NodeInfo>) {
|
||||
discardNodeDB() // Get rid of any old state
|
||||
|
||||
|
|
@ -614,13 +356,12 @@ class MeshService : Service(), Logging {
|
|||
})
|
||||
}
|
||||
|
||||
/// Load our saved DB state
|
||||
private fun loadSettings() {
|
||||
try {
|
||||
getPrefs().getString("json", null)?.let { asString ->
|
||||
|
||||
val json = Json { isLenient = true }
|
||||
val settings = json.decodeFromString(SavedSettings.serializer(), asString)
|
||||
val settings = json.decodeFromString(MeshServiceSettingsData.serializer(), asString)
|
||||
installNewNodeDB(settings.myInfo, settings.nodeDB)
|
||||
|
||||
// Note: we do not haveNodeDB = true because that means we've got a valid db from a real device (rather than this possibly stale hint)
|
||||
|
|
@ -685,7 +426,6 @@ class MeshService : Service(), Logging {
|
|||
id
|
||||
)
|
||||
|
||||
|
||||
private val numNodes get() = nodeDBbyNodeNum.size
|
||||
|
||||
/**
|
||||
|
|
@ -711,7 +451,7 @@ class MeshService : Service(), Logging {
|
|||
nodeDBbyID[userId] = info
|
||||
|
||||
// parcelable is busted
|
||||
broadcastNodeChange(info)
|
||||
serviceBroadcasts.broadcastNodeChange(info)
|
||||
}
|
||||
|
||||
/// My node num
|
||||
|
|
@ -848,9 +588,9 @@ class MeshService : Service(), Logging {
|
|||
MeshProtos.Data.Type.CLEAR_TEXT_VALUE -> {
|
||||
debug("Received CLEAR_TEXT from $fromId")
|
||||
|
||||
recentReceivedText = dataPacket
|
||||
recentReceivedTextPacket = dataPacket
|
||||
updateNotification()
|
||||
broadcastReceivedData(dataPacket)
|
||||
serviceBroadcasts.broadcastReceivedData(dataPacket)
|
||||
}
|
||||
|
||||
MeshProtos.Data.Type.CLEAR_READACK_VALUE ->
|
||||
|
|
@ -859,7 +599,7 @@ class MeshService : Service(), Logging {
|
|||
)
|
||||
|
||||
MeshProtos.Data.Type.OPAQUE_VALUE ->
|
||||
broadcastReceivedData(dataPacket)
|
||||
serviceBroadcasts.broadcastReceivedData(dataPacket)
|
||||
|
||||
else -> TODO()
|
||||
}
|
||||
|
|
@ -939,7 +679,7 @@ class MeshService : Service(), Logging {
|
|||
offlineSentPackets.forEach { p ->
|
||||
// encapsulate our payload in the proper protobufs and fire it off
|
||||
sendNow(p)
|
||||
broadcastMessageStatus(p)
|
||||
serviceBroadcasts.broadcastMessageStatus(p)
|
||||
}
|
||||
offlineSentPackets.clear()
|
||||
}
|
||||
|
|
@ -949,7 +689,7 @@ class MeshService : Service(), Logging {
|
|||
*/
|
||||
private fun changeStatus(p: DataPacket, m: MessageStatus) {
|
||||
p.status = m
|
||||
broadcastMessageStatus(p)
|
||||
serviceBroadcasts.broadcastMessageStatus(p)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1099,7 +839,7 @@ class MeshService : Service(), Logging {
|
|||
}
|
||||
|
||||
// broadcast an intent with our new connection state
|
||||
broadcastConnection()
|
||||
serviceBroadcasts.broadcastConnection()
|
||||
}
|
||||
|
||||
fun startDisconnect() {
|
||||
|
|
@ -1114,7 +854,7 @@ class MeshService : Service(), Logging {
|
|||
GeeksvilleApplication.analytics.track("num_nodes", DataPair(numNodes))
|
||||
|
||||
// broadcast an intent with our new connection state
|
||||
broadcastConnection()
|
||||
serviceBroadcasts.broadcastConnection()
|
||||
}
|
||||
|
||||
fun startConnect() {
|
||||
|
|
@ -1161,18 +901,6 @@ class MeshService : Service(), Logging {
|
|||
updateNotification()
|
||||
}
|
||||
|
||||
/**
|
||||
* broadcast our current connection status
|
||||
*/
|
||||
private fun broadcastConnection() {
|
||||
val intent = Intent(ACTION_MESH_CONNECTED)
|
||||
intent.putExtra(
|
||||
EXTRA_CONNECTED,
|
||||
connectionState.toString()
|
||||
)
|
||||
explicitBroadcast(intent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives messages from our BT radio service and processes them to update our model
|
||||
* and send to clients as needed.
|
||||
|
|
@ -1356,7 +1084,7 @@ class MeshService : Service(), Logging {
|
|||
processEarlyPackets() // send receive any packets that were queued up
|
||||
|
||||
// broadcast an intent with our new connection state
|
||||
broadcastConnection()
|
||||
serviceBroadcasts.broadcastConnection()
|
||||
onNodeDBChanged()
|
||||
reportConnection()
|
||||
}
|
||||
|
|
@ -1378,7 +1106,10 @@ class MeshService : Service(), Logging {
|
|||
})
|
||||
}
|
||||
|
||||
/// Send a position (typically from our built in GPS) into the mesh
|
||||
/**
|
||||
* Send a position (typically from our built in GPS) into the mesh.
|
||||
* Must be called from serviceScope. Use sendPositionScoped() for direct calls.
|
||||
*/
|
||||
private fun sendPosition(
|
||||
lat: Double,
|
||||
lon: Double,
|
||||
|
|
@ -1411,6 +1142,16 @@ class MeshService : Service(), Logging {
|
|||
sendToRadio(packet.build())
|
||||
}
|
||||
|
||||
private fun sendPositionScoped(
|
||||
lat: Double,
|
||||
lon: Double,
|
||||
alt: Int,
|
||||
destNum: Int = NODENUM_BROADCAST,
|
||||
wantResponse: Boolean = false
|
||||
) = serviceScope.handledLaunch {
|
||||
sendPosition(lat, lon, alt, destNum, wantResponse)
|
||||
}
|
||||
|
||||
/** Set our radio config either with the new or old API
|
||||
*/
|
||||
private fun setRadioConfig(payload: ByteArray) {
|
||||
|
|
@ -1599,9 +1340,7 @@ class MeshService : Service(), Logging {
|
|||
this@MeshService.setOwner(myId, longName, shortName)
|
||||
}
|
||||
|
||||
override fun send(
|
||||
p: DataPacket
|
||||
) {
|
||||
override fun send(p: DataPacket) {
|
||||
toRemoteExceptions {
|
||||
// Init from and id
|
||||
myNodeID?.let { myId ->
|
||||
|
|
@ -1673,7 +1412,7 @@ class MeshService : Service(), Logging {
|
|||
}
|
||||
}
|
||||
|
||||
public fun updateNodeInfoTime(it: NodeInfo, rxTime: Int) {
|
||||
fun updateNodeInfoTime(it: NodeInfo, rxTime: Int) {
|
||||
if (it.position?.time == null || it.position?.time!! < rxTime)
|
||||
it.position = it.position?.copy(time = rxTime)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
package com.geeksville.mesh.service
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Parcelable
|
||||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.NodeInfo
|
||||
|
||||
class MeshServiceBroadcasts(
|
||||
private val context: Context,
|
||||
private val clientPackages: MutableMap<String, String>,
|
||||
private val getConnectionState: () -> MeshService.ConnectionState
|
||||
) {
|
||||
/**
|
||||
* The RECEIVED_OPAQUE:
|
||||
* Payload will be a DataPacket
|
||||
*/
|
||||
fun broadcastReceivedData(payload: DataPacket) {
|
||||
val intent = Intent(MeshService.ACTION_RECEIVED_DATA).putExtra(EXTRA_PAYLOAD, payload)
|
||||
explicitBroadcast(intent)
|
||||
}
|
||||
|
||||
fun broadcastNodeChange(info: NodeInfo) {
|
||||
MeshService.debug("Broadcasting node change $info")
|
||||
val intent = Intent(MeshService.ACTION_NODE_CHANGE).putExtra(EXTRA_NODEINFO, info)
|
||||
explicitBroadcast(intent)
|
||||
}
|
||||
|
||||
fun broadcastMessageStatus(p: DataPacket) {
|
||||
if (p.id == 0) {
|
||||
MeshService.debug("Ignoring anonymous packet status")
|
||||
} else {
|
||||
MeshService.debug("Broadcasting message status $p")
|
||||
val intent = Intent(MeshService.ACTION_MESSAGE_STATUS).apply {
|
||||
putExtra(EXTRA_PACKET_ID, p.id)
|
||||
putExtra(EXTRA_STATUS, p.status as Parcelable)
|
||||
}
|
||||
explicitBroadcast(intent)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast our current connection status
|
||||
*/
|
||||
fun broadcastConnection() {
|
||||
val intent = Intent(MeshService.ACTION_MESH_CONNECTED).putExtra(
|
||||
EXTRA_CONNECTED,
|
||||
getConnectionState().toString()
|
||||
)
|
||||
explicitBroadcast(intent)
|
||||
}
|
||||
|
||||
/**
|
||||
* See com.geeksville.mesh broadcast intents.
|
||||
*
|
||||
* RECEIVED_OPAQUE for data received from other nodes
|
||||
* NODE_CHANGE for new IDs appearing or disappearing
|
||||
* ACTION_MESH_CONNECTED for losing/gaining connection to the packet radio
|
||||
* Note: this is not the same as RadioInterfaceService.RADIO_CONNECTED_ACTION,
|
||||
* because it implies we have assembled a valid node db.
|
||||
*/
|
||||
private fun explicitBroadcast(intent: Intent) {
|
||||
context.sendBroadcast(intent) // We also do a regular (not explicit broadcast) so any context-registered rceivers will work
|
||||
clientPackages.forEach {
|
||||
intent.setClassName(it.value, it.key)
|
||||
context.sendBroadcast(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
package com.geeksville.mesh.service
|
||||
|
||||
import android.location.Location
|
||||
import android.os.RemoteException
|
||||
import com.google.android.gms.location.LocationCallback
|
||||
import com.google.android.gms.location.LocationResult
|
||||
|
||||
val Location.isAccurateForMesh: Boolean get() = !this.hasAccuracy() || this.accuracy < 200
|
||||
|
||||
private fun List<Location>.filterAccurateForMesh() = filter { it.isAccurateForMesh }
|
||||
|
||||
private fun LocationResult.lastLocationOrBestEffort(): Location? {
|
||||
return lastLocation ?: locations.filterAccurateForMesh().lastOrNull()
|
||||
}
|
||||
|
||||
typealias SendPosition = (Double, Double, Int, Int, Boolean) -> Unit // Lat, Lon, alt, destNum, wantResponse
|
||||
typealias OnSendFailure = () -> Unit
|
||||
typealias GetNodeNum = () -> Int
|
||||
|
||||
class MeshServiceLocationCallback(
|
||||
private val onSendPosition: SendPosition,
|
||||
private val onSendPositionFailed: OnSendFailure,
|
||||
private val getNodeNum: GetNodeNum,
|
||||
private val sendRateLimitInSeconds: Int = DEFAULT_SEND_RATE_LIMIT
|
||||
) : LocationCallback() {
|
||||
|
||||
companion object {
|
||||
const val DEFAULT_SEND_RATE_LIMIT = 30
|
||||
}
|
||||
|
||||
private var lastSendTimeMs: Long = 0L
|
||||
|
||||
override fun onLocationResult(locationResult: LocationResult) {
|
||||
super.onLocationResult(locationResult)
|
||||
|
||||
locationResult.lastLocationOrBestEffort()?.let { location ->
|
||||
MeshService.info("got phone location")
|
||||
if (location.isAccurateForMesh) { // if within 200 meters, or accuracy is unknown
|
||||
val shouldSend = isAllowedToSend()
|
||||
val destinationNumber = if (shouldSend) MeshService.NODENUM_BROADCAST else getNodeNum()
|
||||
sendPosition(location, destinationNumber, wantResponse = shouldSend)
|
||||
} else {
|
||||
MeshService.warn("accuracy ${location.accuracy} is too poor to use")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendPosition(location: Location, destinationNumber: Int, wantResponse: Boolean) {
|
||||
try {
|
||||
onSendPosition(
|
||||
location.latitude,
|
||||
location.longitude,
|
||||
location.altitude.toInt(),
|
||||
destinationNumber,
|
||||
wantResponse // wantResponse?
|
||||
)
|
||||
} catch (ex: RemoteException) { // Really a RadioNotConnected exception, but it has changed into this type via remoting
|
||||
MeshService.warn("Lost connection to radio, stopping location requests")
|
||||
onSendPositionFailed()
|
||||
} catch (ex: BLEException) { // Really a RadioNotConnected exception, but it has changed into this type via remoting
|
||||
MeshService.warn("BLE exception, stopping location requests $ex")
|
||||
onSendPositionFailed()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rate limiting function.
|
||||
*/
|
||||
private fun isAllowedToSend(): Boolean {
|
||||
val now = System.currentTimeMillis()
|
||||
// we limit our sends onto the lora net to a max one once every FIXME
|
||||
val sendLora = (now - lastSendTimeMs >= sendRateLimitInSeconds * 1000)
|
||||
if (sendLora) {
|
||||
lastSendTimeMs = now
|
||||
}
|
||||
return sendLora
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
package com.geeksville.mesh.service
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.MainActivity
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.android.notificationManager
|
||||
import com.geeksville.mesh.utf8
|
||||
|
||||
class MeshServiceNotifications(
|
||||
private val context: Context
|
||||
) {
|
||||
private val notificationManager: NotificationManager get() = context.notificationManager
|
||||
val notifyId = 101
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun createNotificationChannel(): String {
|
||||
val channelId = "my_service"
|
||||
val channelName = context.getString(R.string.meshtastic_service_notifications)
|
||||
val channel = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH).apply {
|
||||
lightColor = Color.BLUE
|
||||
importance = NotificationManager.IMPORTANCE_NONE
|
||||
lockscreenVisibility = Notification.VISIBILITY_PRIVATE
|
||||
}
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
return channelId
|
||||
}
|
||||
|
||||
private val channelId: String by lazy {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
createNotificationChannel()
|
||||
} else {
|
||||
// If earlier version channel ID is not used
|
||||
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update our notification with latest data
|
||||
*/
|
||||
fun updateNotification(
|
||||
recentReceivedText: DataPacket?,
|
||||
summaryString: String,
|
||||
senderName: String
|
||||
) {
|
||||
val notification = createNotification(recentReceivedText, summaryString, senderName)
|
||||
notificationManager.notify(notifyId, notification)
|
||||
}
|
||||
|
||||
private val openAppIntent: PendingIntent by lazy {
|
||||
PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java), 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new version of our notification - reflecting current app state
|
||||
*/
|
||||
fun createNotification(
|
||||
recentReceivedText: DataPacket?,
|
||||
summaryString: String,
|
||||
senderName: String
|
||||
): Notification {
|
||||
val category = if (recentReceivedText != null) Notification.CATEGORY_SERVICE else Notification.CATEGORY_MESSAGE
|
||||
val builder = NotificationCompat.Builder(context, channelId).setOngoing(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_MIN)
|
||||
.setCategory(category)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_data_bluetooth)
|
||||
.setContentTitle(summaryString) // leave this off for now so our notification looks smaller
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setContentIntent(openAppIntent)
|
||||
|
||||
// FIXME, show information about the nearest node
|
||||
// if(shortContent != null) builder.setContentText(shortContent)
|
||||
|
||||
// If a text message arrived include it with our notification
|
||||
recentReceivedText?.let { packet ->
|
||||
// Try to show the human name of the sender if possible
|
||||
builder.setContentText("Message from $senderName")
|
||||
builder.setStyle(
|
||||
NotificationCompat.BigTextStyle()
|
||||
.bigText(packet.bytes!!.toString(utf8))
|
||||
)
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.geeksville.mesh.service
|
||||
|
||||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.MyNodeInfo
|
||||
import com.geeksville.mesh.NodeInfo
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/// Our saved preferences as stored on disk
|
||||
@Serializable
|
||||
data class MeshServiceSettingsData(
|
||||
val nodeDB: Array<NodeInfo>,
|
||||
val myInfo: MyNodeInfo,
|
||||
val messages: Array<DataPacket>
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as MeshServiceSettingsData
|
||||
|
||||
if (!nodeDB.contentEquals(other.nodeDB)) return false
|
||||
if (myInfo != other.myInfo) return false
|
||||
if (!messages.contentEquals(other.messages)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = nodeDB.contentHashCode()
|
||||
result = 31 * result + myInfo.hashCode()
|
||||
result = 31 * result + messages.contentHashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
package com.geeksville.mesh.service
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Helper that calls MeshService.startService()
|
||||
*/
|
||||
private class ServiceStarter(
|
||||
appContext: Context,
|
||||
workerParams: WorkerParameters
|
||||
) : Worker(appContext, workerParams) {
|
||||
|
||||
override fun doWork(): Result = try {
|
||||
MeshService.startService(this.applicationContext)
|
||||
|
||||
// Indicate whether the task finished successfully with the Result
|
||||
Result.success()
|
||||
} catch (ex: Exception) {
|
||||
MeshService.errormsg("failure starting service, will retry", ex)
|
||||
Result.retry()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Context.startMeshService(): ComponentName? {
|
||||
val intent = MeshService.createIntent()
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
startForegroundService(intent)
|
||||
} else {
|
||||
startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Just after boot the android OS is super busy, so if we call startForegroundService then, our
|
||||
* thread might be stalled long enough to expose this Google/Samsung bug:
|
||||
* https://issuetracker.google.com/issues/76112072#comment56
|
||||
*/
|
||||
fun MeshService.Companion.startServiceLater(context: Context) {
|
||||
// No point in even starting the service if the user doesn't have a device bonded
|
||||
info("Received boot complete announcement, starting mesh service in two minutes")
|
||||
val delayRequest = OneTimeWorkRequestBuilder<ServiceStarter>()
|
||||
.setInitialDelay(2, TimeUnit.MINUTES)
|
||||
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 2, TimeUnit.MINUTES)
|
||||
.addTag("startLater")
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance(context).enqueue(delayRequest)
|
||||
}
|
||||
|
||||
/// Helper function to start running our service
|
||||
fun MeshService.Companion.startService(context: Context) {
|
||||
// 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
|
||||
|
||||
// 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.
|
||||
info("Trying to start service")
|
||||
requireNotNull(context.startMeshService()) { "Failed to start service" }
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue