diff --git a/TODO.md b/TODO.md
index 643e81fd..42dfd5a5 100644
--- a/TODO.md
+++ b/TODO.md
@@ -24,6 +24,8 @@
# Medium priority
+* test with oldest android
+* stop using a foreground service
* change info() log strings to debug()
* use platform theme (dark or light)
* remove mixpanel analytics
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 9e227dee..12702886 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -27,6 +27,9 @@
+
+
+
diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt
index 82233b9b..2d437133 100644
--- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt
+++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt
@@ -8,6 +8,7 @@ import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.PackageManager
+import android.os.Build
import android.os.Bundle
import android.os.Debug
import android.os.IBinder
@@ -171,9 +172,10 @@ class MainActivity : AppCompatActivity(), Logging {
requestPermission()
}
- var meshService: IMeshService? = null
+ private var meshService: IMeshService? = null
+ private var isBound = false
- private val serviceConnection = object : ServiceConnection {
+ private var serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
val m = IMeshService.Stub.asInterface(service)
meshService = m
@@ -201,14 +203,25 @@ class MainActivity : AppCompatActivity(), Logging {
logAssert(meshService == null)
// bind to our service using the same mechanism an external client would use (for testing coverage)
- val intent = Intent()
- intent.setClassName("com.geeksville.mesh", "com.geeksville.mesh.MeshService")
-
// The following would work for us, but not external users
//val intent = Intent(this, MeshService::class.java)
//intent.action = IMeshService::class.java.name
+ val intent = Intent()
+ intent.setClassName("com.geeksville.mesh", "com.geeksville.mesh.MeshService")
+ // 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.
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ startForegroundService(intent)
+ } else {
+ startService(intent)
+ }
+
+ // ALSO bind so we can use the api
logAssert(bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE))
+ isBound = true;
}
private fun unbindMeshService() {
@@ -216,7 +229,8 @@ class MainActivity : AppCompatActivity(), Logging {
// it, then now is the time to unregister.
// if we never connected, do nothing
debug("Unbinding from mesh service!")
- unbindService(serviceConnection)
+ if (isBound)
+ unbindService(serviceConnection)
meshService = null
}
diff --git a/app/src/main/java/com/geeksville/mesh/MeshService.kt b/app/src/main/java/com/geeksville/mesh/MeshService.kt
index ca0a0c5b..ca49da65 100644
--- a/app/src/main/java/com/geeksville/mesh/MeshService.kt
+++ b/app/src/main/java/com/geeksville/mesh/MeshService.kt
@@ -1,16 +1,26 @@
package com.geeksville.mesh
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
import android.app.Service
import android.content.*
+import android.graphics.Color
+import android.os.Build
import android.os.IBinder
+import androidx.annotation.RequiresApi
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationCompat.PRIORITY_MIN
import com.geeksville.android.Logging
import com.geeksville.mesh.MeshProtos.MeshPacket
import com.geeksville.mesh.MeshProtos.ToRadio
+import com.geeksville.util.exceptionReporter
import com.geeksville.util.toOneLineString
import com.geeksville.util.toRemoteExceptions
import com.google.protobuf.ByteString
import java.nio.charset.Charset
+
class RadioNotConnectedException() : Exception("Can't find radio")
/**
@@ -106,13 +116,74 @@ class MeshService : Service(), Logging {
}
}
+ @RequiresApi(Build.VERSION_CODES.O)
+ private fun createNotificationChannel(): String {
+ val channelId = "my_service"
+ val channelName = "My Background Service"
+ val chan = NotificationChannel(
+ channelId,
+ channelName, NotificationManager.IMPORTANCE_HIGH
+ )
+ chan.lightColor = Color.BLUE
+ chan.importance = NotificationManager.IMPORTANCE_NONE
+ chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
+ val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ service.createNotificationChannel(chan)
+ return channelId
+ }
+
+ private fun startForeground() {
+
+ val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ val channelId =
+ 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)
+ ""
+ }
+
+ val notificationBuilder = NotificationCompat.Builder(this, channelId)
+ val notification = notificationBuilder.setOngoing(true)
+ .setPriority(PRIORITY_MIN)
+ .setCategory(Notification.CATEGORY_SERVICE)
+ .setSmallIcon(android.R.drawable.stat_sys_data_bluetooth)
+ //.setContentTitle("Meshtastic") // leave this off for now so our notification looks smaller
+ //.setContentText("Listening for mesh...")
+ .build()
+ startForeground(101, notification)
+ }
+
override fun onCreate() {
super.onCreate()
info("Creating mesh service")
+ /*
+ // This intent will be used if the user clicks on the item in the status bar
+ val notificationIntent = Intent(this, MainActivity::class.java)
+ val pendingIntent = PendingIntent.getActivity(
+ this, 0,
+ notificationIntent, 0
+ )
+
+ val notification: Notification = NotificationCompat.Builder(this)
+ .setSmallIcon(android.R.drawable.stat_sys_data_bluetooth)
+ .setContentTitle("Meshtastic")
+ .setContentText("Listening for mesh...")
+ .setContentIntent(pendingIntent).build()
+
+ // We are required to call this within a few seconds of create
+ startForeground(1337, notification)
+
+ */
+ startForeground()
+
// we listen for messages from the radio receiver _before_ trying to create the service
- val filter = IntentFilter(RadioInterfaceService.RECEIVE_FROMRADIO_ACTION)
+ val filter = IntentFilter()
+ filter.addAction(RadioInterfaceService.RECEIVE_FROMRADIO_ACTION)
+ filter.addAction(RadioInterfaceService.CONNECTCHANGED_ACTION)
registerReceiver(radioInterfaceReceiver, filter)
// We in turn need to use the radiointerface service
@@ -303,6 +374,7 @@ class MeshService : Service(), Logging {
/// Called when we gain/lose connection to our radio
private fun onConnectionChanged(c: Boolean) {
+ debug("onConnectionChanged connected=$c")
isConnected = c
if (c) {
// Do our startup init
@@ -342,7 +414,6 @@ class MeshService : Service(), Logging {
infoBytes = connectedRadio.readNodeInfo()
}
}
- TODO("FIXME - set our owner, get node infos, set our local nodenum, dont process received packets until we have the full node db")
}
/**
@@ -351,8 +422,10 @@ class MeshService : Service(), Logging {
*/
private val radioInterfaceReceiver = object : BroadcastReceiver() {
- override fun onReceive(context: Context, intent: Intent) {
+ // Important to never throw exceptions out of onReceive
+ override fun onReceive(context: Context, intent: Intent) = exceptionReporter {
+ debug("Received broadcast ${intent.action}")
when (intent.action) {
RadioInterfaceService.CONNECTCHANGED_ACTION -> {
onConnectionChanged(intent.getBooleanExtra(EXTRA_CONNECTED, false))
@@ -428,7 +501,7 @@ class MeshService : Service(), Logging {
override fun isConnected(): Boolean = toRemoteExceptions {
val r = this@MeshService.isConnected
- info("in isConnected=r")
+ info("in isConnected=$r")
r
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/RadioInterfaceService.kt b/app/src/main/java/com/geeksville/mesh/RadioInterfaceService.kt
index d0c27dff..f042c8c7 100644
--- a/app/src/main/java/com/geeksville/mesh/RadioInterfaceService.kt
+++ b/app/src/main/java/com/geeksville/mesh/RadioInterfaceService.kt
@@ -150,6 +150,7 @@ class RadioInterfaceService : Service(), Logging {
private val clientOperations = DeferredExecution()
private fun broadcastConnectionChanged(isConnected: Boolean) {
+ debug("Broadcasting connection=$isConnected")
val intent = Intent(CONNECTCHANGED_ACTION)
intent.putExtra(EXTRA_CONNECTED, isConnected)
sendBroadcast(intent)
@@ -195,14 +196,14 @@ class RadioInterfaceService : Service(), Logging {
override fun onCreate() {
super.onCreate()
- info("Creating radio interface service")
-
// FIXME, let user GUI select which device we are talking to
// Note: this call does no comms, it just creates the device object (even if the
// device is off/not connected)
val usetbeam = false
val address = if (usetbeam) "B4:E6:2D:EA:32:B7" else "24:6F:28:96:C9:2A"
+ info("Creating radio interface service. device=$address")
+
device = bluetoothAdapter.getRemoteDevice(address)
// Note this constructor also does no comm
safe = SafeBluetooth(this, device)
diff --git a/app/src/main/java/com/geeksville/mesh/SafeBluetooth.kt b/app/src/main/java/com/geeksville/mesh/SafeBluetooth.kt
index e6b61722..40de7956 100644
--- a/app/src/main/java/com/geeksville/mesh/SafeBluetooth.kt
+++ b/app/src/main/java/com/geeksville/mesh/SafeBluetooth.kt
@@ -135,6 +135,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
w
}
+ debug("work ${work.tag} is completed, resuming with status $status")
if (status != 0)
work.completion.resumeWithException(IOException("Bluetooth status=$status"))
else