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