diff --git a/TODO.md b/TODO.md index b7371a95..856196ed 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,12 @@ # High priority Work items for soon alpha builds +* use states for meshservice: disconnected -> connected -> deviceasleep -> disconnected + +* use compose on each page, but not for the outer wrapper +* one view per page: https://developer.android.com/guide/navigation/navigation-swipe-view-2 +* use viewgroup with a unique ID https://developer.android.com/reference/kotlin/androidx/ui/core/package-summary#(android.view.ViewGroup).setContent(kotlin.Function0) + * let channel be editited * make link sharing work * finish map view @@ -19,6 +25,8 @@ Work items for soon alpha builds # Medium priority Features for future builds +* use coroutines in the services, to ensure low latency for both API calls and GUI operations https://developer.android.com/kotlin/coroutines & +https://medium.com/@kenkyee/android-kotlin-coroutine-best-practices-bc033fed62e7 & https://codelabs.developers.google.com/codelabs/kotlin-coroutines/#5 * fix notification setSmallIcon parameter - change it to use the meshtastic icon * ditch compose and use https://github.com/zsmb13/MaterialDrawerKt + https://github.com/Kotlin/anko/wiki/Anko-Layouts? * describe user experience: devices always point to each other and show distance, you can send texts between nodes diff --git a/app/build.gradle b/app/build.gradle index a5dc6552..650a633a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -82,6 +82,10 @@ dependencies { androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + // Coroutines + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" + // You need to depend on the lite runtime library, not protobuf-java // For now I'm not using javalite, because I want JSON printing //implementation 'com.google.protobuf:protobuf-java:3.11.1' diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index f1b42451..960ab5d6 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -19,3 +19,8 @@ # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile + +# per https://medium.com/@kenkyee/android-kotlin-coroutine-best-practices-bc033fed62e7 +-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {} +-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {} +-keepclassmembernames class kotlinx.** { volatile ; } \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt index 14c19290..c176e37e 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -11,6 +11,7 @@ import android.os.Build import android.os.IBinder import android.os.RemoteException import androidx.annotation.RequiresApi +import androidx.annotation.UiThread import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat.PRIORITY_MIN import com.geeksville.analytics.DataPair @@ -27,12 +28,26 @@ 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 kotlinx.coroutines.* import java.nio.charset.Charset +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext class RadioNotConnectedException() : Exception("Not connected to radio") +private val errorHandler = CoroutineExceptionHandler { _, exception -> + Exceptions.report(exception, "MeshService-coroutine", "coroutine-exception") +} + +/// Wrap launch with an exception handler, FIXME, move into a utility lib +fun CoroutineScope.handledLaunch( + context: CoroutineContext = EmptyCoroutineContext, + start: CoroutineStart = CoroutineStart.DEFAULT, + block: suspend CoroutineScope.() -> Unit +) = this.launch(context = context + errorHandler, start = start, block = block) + /** * Handles all the communication with android apps. Also keeps an internal model * of the network state. @@ -96,6 +111,9 @@ class MeshService : Service(), Logging { IRadioInterfaceService.Stub.asInterface(it) } + private val serviceJob = Job() + private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob) + /* see com.geeksville.mesh broadcast intents // RECEIVED_OPAQUE for data received from other nodes @@ -117,7 +135,7 @@ class MeshService : Service(), Logging { private var lastSendMsec = 0L override fun onLocationResult(locationResult: LocationResult) { - exceptionReporter { + serviceScope.handledLaunch { super.onLocationResult(locationResult) var l = locationResult.lastLocation @@ -163,6 +181,7 @@ class MeshService : Service(), Logging { * per https://developer.android.com/training/location/change-location-settings */ @SuppressLint("MissingPermission") + @UiThread private fun startLocationRequests() { if (fusedLocationClient == null) { GeeksvilleApplication.analytics.track("location_start") // Figure out how many users needed to use the phone GPS @@ -375,6 +394,7 @@ class MeshService : Service(), Logging { radio.close() super.onDestroy() + serviceJob.cancel() } @@ -712,14 +732,17 @@ class MeshService : Service(), Logging { if (!myNodeInfo!!.hasGPS) { // If we have at least one other person in the mesh, send our GPS position otherwise stop listening to GPS - if (numOnlineNodes >= 2) - startLocationRequests() - else - stopLocationRequests() + serviceScope.handledLaunch(Dispatchers.Main) { + if (numOnlineNodes >= 2) + startLocationRequests() + else + stopLocationRequests() + } } else debug("Our radio has a built in GPS, so not reading GPS in phone") } + /// Called when we gain/lose connection to our radio private fun onConnectionChanged(c: Boolean) { debug("onConnectionChanged connected=$c") @@ -773,41 +796,42 @@ class MeshService : Service(), Logging { // Important to never throw exceptions out of onReceive override fun onReceive(context: Context, intent: Intent) = exceptionReporter { + serviceScope.handledLaunch { + debug("Received broadcast ${intent.action}") + when (intent.action) { + RadioInterfaceService.RADIO_CONNECTED_ACTION -> { + try { + onConnectionChanged(intent.getBooleanExtra(EXTRA_CONNECTED, false)) - debug("Received broadcast ${intent.action}") - when (intent.action) { - RadioInterfaceService.RADIO_CONNECTED_ACTION -> { - try { - onConnectionChanged(intent.getBooleanExtra(EXTRA_CONNECTED, false)) - - // forward the connection change message to anyone who is listening to us. but change the action - // to prevent an infinite loop from us receiving our own broadcast. ;-) - intent.action = ACTION_MESH_CONNECTED - explicitBroadcast(intent) - } catch (ex: RemoteException) { - // This can happen sometimes (especially if the device is slowly dying due to killing power, don't report to crashlytics - warn("Abandoning reconnect attempt, due to errors during init: ${ex.message}") + // forward the connection change message to anyone who is listening to us. but change the action + // to prevent an infinite loop from us receiving our own broadcast. ;-) + intent.action = ACTION_MESH_CONNECTED + explicitBroadcast(intent) + } catch (ex: RemoteException) { + // This can happen sometimes (especially if the device is slowly dying due to killing power, don't report to crashlytics + warn("Abandoning reconnect attempt, due to errors during init: ${ex.message}") + } } - } - RadioInterfaceService.RECEIVE_FROMRADIO_ACTION -> { - val proto = - MeshProtos.FromRadio.parseFrom( - intent.getByteArrayExtra( - EXTRA_PAYLOAD - )!! - ) - info("Received from radio service: ${proto.toOneLineString()}") - when (proto.variantCase.number) { - MeshProtos.FromRadio.PACKET_FIELD_NUMBER -> handleReceivedMeshPacket( - proto.packet - ) + RadioInterfaceService.RECEIVE_FROMRADIO_ACTION -> { + val proto = + MeshProtos.FromRadio.parseFrom( + intent.getByteArrayExtra( + EXTRA_PAYLOAD + )!! + ) + info("Received from radio service: ${proto.toOneLineString()}") + when (proto.variantCase.number) { + MeshProtos.FromRadio.PACKET_FIELD_NUMBER -> handleReceivedMeshPacket( + proto.packet + ) - else -> TODO("Unexpected FromRadio variant") + else -> TODO("Unexpected FromRadio variant") + } } - } - else -> TODO("Unexpected radio interface broadcast") + else -> TODO("Unexpected radio interface broadcast") + } } } } diff --git a/build.gradle b/build.gradle index 1af1d03f..ee34f205 100644 --- a/build.gradle +++ b/build.gradle @@ -3,6 +3,8 @@ buildscript { ext.kotlin_version = '1.3.61' ext.compose_version = '0.1.0-dev07' + ext.coroutines_version = "1.3.5" + repositories { google() jcenter()