diff --git a/app/build.gradle b/app/build.gradle index aceb46b2..3bbc54b9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlinx-serialization' apply plugin: 'com.google.gms.google-services' // Apply the Crashlytics Gradle plugin @@ -103,6 +104,9 @@ dependencies { androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + // kotlin serialization + implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0" + // rate this app implementation "com.vorlonsoft:androidrate:1.2.1" diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 3ba46df0..6ffefad0 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -28,5 +28,16 @@ # Needed for protobufs -keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite { ; } +# for kotlinx.serialization +-keepattributes *Annotation*, InnerClasses +-dontnote kotlinx.serialization.SerializationKt +-keep,includedescriptorclasses class com.yourcompany.yourpackage.**$$serializer { *; } # <-- change package name to your app's +-keepclassmembers class com.geeksville.mesh.** { # <-- change package name to your app's + *** Companion; +} +-keepclasseswithmembers class com.geeksville.mesh.** { # <-- change package name to your app's + kotlinx.serialization.KSerializer serializer(...); +} + # Our app is opensource no need to obsfucate -dontobfuscate \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/NodeInfo.kt b/app/src/main/java/com/geeksville/mesh/NodeInfo.kt index e430520e..2632ac8f 100644 --- a/app/src/main/java/com/geeksville/mesh/NodeInfo.kt +++ b/app/src/main/java/com/geeksville/mesh/NodeInfo.kt @@ -4,6 +4,7 @@ import android.os.Parcelable import com.geeksville.mesh.ui.bearing import com.geeksville.mesh.ui.latLongToMeter import kotlinx.android.parcel.Parcelize +import kotlinx.serialization.Serializable /** @@ -23,6 +24,7 @@ val Any?.anonymized: String /** * A parcelable version of the protobuf MeshPacket + Data subpacket. */ +@Serializable @Parcelize data class DataPacket( val from: String, // a nodeID string @@ -61,6 +63,7 @@ data class DataPacket( } } +@Serializable @Parcelize data class MeshUser(val id: String, val longName: String, val shortName: String) : Parcelable { @@ -70,6 +73,7 @@ data class MeshUser(val id: String, val longName: String, val shortName: String) } } +@Serializable @Parcelize data class Position( val latitude: Double, @@ -89,6 +93,7 @@ data class Position( } +@Serializable @Parcelize data class NodeInfo( val num: Int, // This is immutable, and used as a key 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 7dc312c0..7395e841 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -14,6 +14,7 @@ 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 com.geeksville.analytics.DataPair import com.geeksville.android.GeeksvilleApplication import com.geeksville.android.Logging @@ -29,6 +30,9 @@ import com.google.android.gms.common.api.ResolvableApiException import com.google.android.gms.location.* import com.google.protobuf.ByteString import kotlinx.coroutines.* +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonConfiguration import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext @@ -100,7 +104,7 @@ class MeshService : Service(), Logging { } } - public enum class ConnectionState { + enum class ConnectionState { DISCONNECTED, CONNECTED, DEVICE_SLEEP // device is in LS sleep state, it will reconnected to us over bluetooth once it has data @@ -379,18 +383,23 @@ class MeshService : Service(), Logging { info("Creating mesh service") startForeground() - // 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) - registerReceiver(radioInterfaceReceiver, filter) + // Switch to the IO thread + serviceScope.handledLaunch { + loadSettings() // Load our last known node DB - // We in turn need to use the radiointerface service - val intent = Intent(this, RadioInterfaceService::class.java) - // intent.action = IMeshService::class.java.name - radio.connect(this, intent, Context.BIND_AUTO_CREATE) + // 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) + registerReceiver(radioInterfaceReceiver, filter) - // the rest of our init will happen once we are in radioConnection.onServiceConnected + // We in turn need to use the radiointerface service + val intent = Intent(this@MeshService, RadioInterfaceService::class.java) + // intent.action = IMeshService::class.java.name + radio.connect(this@MeshService, intent, Context.BIND_AUTO_CREATE) + + // the rest of our init will happen once we are in radioConnection.onServiceConnected + } } @@ -398,6 +407,7 @@ class MeshService : Service(), Logging { info("Destroying mesh service") unregisterReceiver(radioInterfaceReceiver) radio.close() + saveSettings() super.onDestroy() serviceJob.cancel() @@ -412,6 +422,7 @@ class MeshService : Service(), Logging { val NODENUM_BROADCAST = 255 // MyNodeInfo sent via special protobuf from radio + @Serializable data class MyNodeInfo( val myNodeNum: Int, val hasGPS: Boolean, @@ -420,6 +431,84 @@ class MeshService : Service(), Logging { val firmwareVersion: String ) + /// Our saved preferences as stored on disk + @Serializable + private data class SavedSettings( + val nodeDB: Array, + val myInfo: MyNodeInfo, + val messages: Array + ) { + 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( + myInfo = myInfo, + nodeDB = nodeDBbyNodeNum.values.toTypedArray(), + messages = recentDataPackets.toTypedArray() + ) + val json = Json(JsonConfiguration.Default) + val asString = json.stringify(SavedSettings.serializer(), settings) + debug("Saving settings as $asString") + getPrefs().edit(commit = true) { + // FIXME, not really ideal to store this bigish blob in preferences + putString("json", asString) + } + } + } + + /// Load our saved DB state + private fun loadSettings() { + try { + getPrefs().getString("json", null)?.let { asString -> + val json = Json(JsonConfiguration.Default) + val settings = json.parse(SavedSettings.serializer(), asString) + myNodeInfo = settings.myInfo + + // put our node array into our two different map representations + nodeDBbyNodeNum.clear() + nodeDBbyNodeNum.putAll(settings.nodeDB.map { Pair(it.num, it) }) + nodeDBbyID.clear() + nodeDBbyID.putAll(settings.nodeDB.mapNotNull { + it.user?.let { user -> // ignore records that don't have a valid user + Pair( + user.id, + it + ) + } + }) + // 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) + + recentDataPackets.clear() + recentDataPackets.addAll(settings.messages) + } + } catch (ex: Exception) { + errormsg("Ignoring error loading saved state for service: ${ex.message}") + } + } + var myNodeInfo: MyNodeInfo? = null private var radioConfig: MeshProtos.RadioConfig? = null @@ -819,6 +908,9 @@ class MeshService : Service(), Logging { } fun startDisconnect() { + // Just in case the user uncleanly reboots the phone, save now (we normally save in onDestroy) + saveSettings() + GeeksvilleApplication.analytics.track( "mesh_disconnect", DataPair("num_nodes", numNodes), diff --git a/build.gradle b/build.gradle index c5812afc..7d2b34f1 100644 --- a/build.gradle +++ b/build.gradle @@ -12,6 +12,8 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:4.0.0-beta04' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" + // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files