@@ -6,6 +6,7 @@ import android.os.Build
66import java.util.concurrent.TimeUnit
77import kotlin.math.roundToInt
88import kotlinx.datetime.Instant
9+ import kotlinx.serialization.ExperimentalSerializationApi
910import kotlinx.serialization.KSerializer
1011import kotlinx.serialization.SerialName
1112import kotlinx.serialization.Serializable
@@ -20,12 +21,34 @@ import kotlinx.serialization.json.JsonElement
2021import kotlinx.serialization.json.JsonPrimitive
2122import kotlinx.serialization.json.JsonTransformingSerializer
2223import kotlinx.serialization.json.doubleOrNull
24+ import kotlinx.serialization.json.intOrNull
25+ import kotlinx.serialization.json.jsonPrimitive
2326import org.jetbrains.annotations.NotNull
2427import org.owntracks.android.model.BatteryStatus
2528import org.owntracks.android.net.WifiInfoProvider
2629import org.owntracks.android.preferences.Preferences
2730import org.owntracks.android.preferences.types.MonitoringMode
2831
32+ /* * Accepts both integer and float JSON values for Int fields (some clients send `0.0`). */
33+ object LenientIntSerializer : JsonTransformingSerializer<Int>(Int .serializer()) {
34+ override fun transformDeserialize (element : JsonElement ): JsonElement =
35+ if (element is JsonPrimitive && element.content != " null" && element.doubleOrNull != null ) {
36+ JsonPrimitive (element.doubleOrNull!! .toInt())
37+ } else {
38+ element
39+ }
40+
41+ fun parseIntLenient (element : JsonElement ? ): Int =
42+ if (element == null ) {
43+ 0
44+ } else if (element is JsonPrimitive && element.doubleOrNull != null ) {
45+ element.doubleOrNull!! .toInt()
46+ } else {
47+ element.jsonPrimitive.intOrNull ? : 0
48+ }
49+ }
50+
51+ @OptIn(ExperimentalSerializationApi ::class )
2952@SuppressLint(" UnsafeOptInUsageError" )
3053@Serializable
3154@SerialName(MessageLocation .TYPE )
@@ -40,9 +63,7 @@ open class MessageLocation(
4063
4164 @kotlinx.serialization.EncodeDefault (kotlinx.serialization.EncodeDefault .Mode .ALWAYS )
4265 @SerialName(" created_at" )
43- override var createdAt:
44- @kotlinx.serialization.Serializable (with = InstantEpochSecondsSerializer ::class )
45- Instant =
66+ override var createdAt: @Serializable(with = InstantEpochSecondsSerializer ::class ) Instant =
4667 messageWithCreatedAtImpl.createdAt
4768
4869 @kotlinx.serialization.EncodeDefault (kotlinx.serialization.EncodeDefault .Mode .ALWAYS )
@@ -57,19 +78,19 @@ open class MessageLocation(
5778
5879 @SerialName(" bs" ) var batteryStatus: BatteryStatus ? = null
5980
60- @SerialName(" acc" ) var accuracy = 0
81+ @SerialName(" acc" ) @Serializable(with = LenientIntSerializer :: class ) var accuracy = 0
6182
62- @SerialName(" vac" ) var verticalAccuracy = 0
83+ @SerialName(" vac" ) @Serializable(with = LenientIntSerializer :: class ) var verticalAccuracy = 0
6384
64- @SerialName(" lat" ) var latitude = 0.0
85+ @SerialName(" lat" ) var latitude: Double = 0.0
6586
66- @SerialName(" lon" ) var longitude = 0.0
87+ @SerialName(" lon" ) var longitude: Double = 0.0
6788
68- @Serializable(with = LenientIntSerializer ::class ) @SerialName( " alt " ) var altitude = 0
89+ @SerialName( " alt " ) @ Serializable(with = LenientIntSerializer ::class ) var altitude = 0
6990
70- @SerialName(" vel" ) var velocity = 0
91+ @SerialName(" vel" ) @Serializable(with = LenientIntSerializer :: class ) var velocity = 0
7192
72- @SerialName(" cog" ) var bearing = 0
93+ @SerialName(" cog" ) @Serializable(with = LenientIntSerializer :: class ) var bearing = 0
7394
7495 @SerialName(" tst" ) var timestamp: Long = 0
7596
@@ -86,7 +107,9 @@ open class MessageLocation(
86107 @SerialName(" tid" ) var trackerId: String? = null
87108
88109 override fun isValidMessage (): Boolean {
89- return timestamp > 0
110+ // The deserializer validates that required fields are present in JSON
111+ // Constraints: timestamp > 0 and (trackerId is not empty OR visibleTopic is not empty)
112+ return timestamp > 0 && (! trackerId.isNullOrEmpty() || visibleTopic.isNotEmpty())
90113 }
91114
92115 override fun toString (): String =
@@ -101,19 +124,6 @@ open class MessageLocation(
101124 companion object {
102125 const val TYPE = " location"
103126
104- /* * Accepts both integer and float JSON values for Int fields (some clients send `0.0`). */
105- object LenientIntSerializer : JsonTransformingSerializer<Int>(Int .serializer()) {
106- override fun transformDeserialize (element : JsonElement ): JsonElement =
107- if (element is JsonPrimitive &&
108- element.content != " null" &&
109- element.doubleOrNull != null ) {
110- JsonPrimitive (element.doubleOrNull!! .toInt())
111- } else {
112- element
113- }
114- }
115-
116- @SuppressLint(" NewApi" )
117127 @JvmStatic
118128 fun fromLocation (location : Location , sdk : Int = Build .VERSION .SDK_INT ): MessageLocation =
119129 MessageLocation ().apply {
0 commit comments