Replaces GSON with kotlinx.serialization
Test: Updated unit tests.
Relnote: JSON serialization/deserialization within the library is now handled by kotlinx.serialization instead of GSON.
Bug: 374810938
Change-Id: Ia4337631f5f5b52b04446e6f6e00eb4baff25bfe
diff --git a/security/security-state/build.gradle b/security/security-state/build.gradle
index 3ed2917..027680e 100644
--- a/security/security-state/build.gradle
+++ b/security/security-state/build.gradle
@@ -27,13 +27,13 @@
id("AndroidXPlugin")
id("com.android.library")
id("org.jetbrains.kotlin.android")
+ alias(libs.plugins.kotlinSerialization)
}
dependencies {
api("androidx.annotation:annotation:1.8.1")
- api(libs.kotlinStdlib)
api(libs.kotlinCoroutinesCore)
- implementation("com.google.code.gson:gson:2.10.1")
+ implementation(libs.kotlinSerializationJson)
implementation("androidx.core:core:1.12.0")
implementation("androidx.webkit:webkit:1.11.0")
@@ -56,9 +56,6 @@
android {
compileSdk 35
namespace "androidx.security.state"
- defaultConfig {
- consumerProguardFiles 'proguard-rules.pro'
- }
}
androidx {
diff --git a/security/security-state/proguard-rules.pro b/security/security-state/proguard-rules.pro
deleted file mode 100644
index be33693..0000000
--- a/security/security-state/proguard-rules.pro
+++ /dev/null
@@ -1 +0,0 @@
--keepclassmembers class androidx.security.state.SecurityPatchState$** { *; }
\ No newline at end of file
diff --git a/security/security-state/src/main/java/androidx/security/state/SecurityPatchState.kt b/security/security-state/src/main/java/androidx/security/state/SecurityPatchState.kt
index 4496a2a..d45c24b 100644
--- a/security/security-state/src/main/java/androidx/security/state/SecurityPatchState.kt
+++ b/security/security-state/src/main/java/androidx/security/state/SecurityPatchState.kt
@@ -24,15 +24,16 @@
import androidx.security.state.SecurityStateManager.Companion.KEY_KERNEL_VERSION
import androidx.security.state.SecurityStateManager.Companion.KEY_SYSTEM_SPL
import androidx.security.state.SecurityStateManager.Companion.KEY_VENDOR_SPL
-import com.google.gson.Gson
-import com.google.gson.JsonSyntaxException
-import com.google.gson.annotations.SerializedName
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
import java.util.regex.Pattern
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.SerializationException
+import kotlinx.serialization.json.Json
/**
* Provides methods to access and manage security state information for various components within a
@@ -308,20 +309,21 @@
public fun getBuildVersion(): Int = buildVersion
}
+ @Serializable
private data class VulnerabilityReport(
/* Key is the SPL date yyyy-MM-dd */
- @SerializedName("vulnerabilities")
val vulnerabilities: Map<String, List<VulnerabilityGroup>>,
/* Key is the SPL date yyyy-MM-dd, values are kernel versions */
- @SerializedName("kernel_lts_versions") val kernelLtsVersions: Map<String, List<String>>
+ @SerialName("kernel_lts_versions") val kernelLtsVersions: Map<String, List<String>>
)
+ @Serializable
private data class VulnerabilityGroup(
- @SerializedName("cve_identifiers") val cveIdentifiers: List<String>,
- @SerializedName("asb_identifiers") val asbIdentifiers: List<String>,
- @SerializedName("severity") val severity: String,
- @SerializedName("components") val components: List<String>
+ @SerialName("cve_identifiers") val cveIdentifiers: List<String>,
+ @SerialName("asb_identifiers") val asbIdentifiers: List<String>,
+ val severity: String,
+ val components: List<String>
)
/**
@@ -354,8 +356,9 @@
val result: VulnerabilityReport
try {
- result = Gson().fromJson(jsonString, VulnerabilityReport::class.java)
- } catch (e: JsonSyntaxException) {
+ val json = Json { ignoreUnknownKeys = true }
+ result = json.decodeFromString<VulnerabilityReport>(jsonString)
+ } catch (e: SerializationException) {
throw IllegalArgumentException("Malformed JSON input: ${e.message}")
}
@@ -794,7 +797,6 @@
val updates = mutableListOf<UpdateInfo>()
val contentResolver = context.contentResolver
- val gson = Gson()
updateInfoProviders.forEach { providerUri ->
val cursor = contentResolver.query(providerUri, arrayOf("json"), null, null, null)
@@ -802,7 +804,9 @@
while (it.moveToNext()) {
val json = it.getString(it.getColumnIndexOrThrow("json"))
try {
- val updateInfo = gson.fromJson(json, UpdateInfo::class.java) ?: continue
+ val serializableUpdateInfo =
+ Json.decodeFromString<SerializableUpdateInfo>(json)
+ val updateInfo: UpdateInfo = serializableUpdateInfo.toUpdateInfo()
val component = updateInfo.component
val deviceSpl = getDeviceSecurityPatchLevel(component)
diff --git a/security/security-state/src/main/java/androidx/security/state/UpdateInfo.kt b/security/security-state/src/main/java/androidx/security/state/UpdateInfo.kt
index 8ef484f..49b29b45 100644
--- a/security/security-state/src/main/java/androidx/security/state/UpdateInfo.kt
+++ b/security/security-state/src/main/java/androidx/security/state/UpdateInfo.kt
@@ -16,8 +16,39 @@
package androidx.security.state
+import java.text.SimpleDateFormat
import java.util.Date
import java.util.Objects
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.descriptors.PrimitiveKind
+import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+
+private object DateSerializer : KSerializer<Date> {
+ override val descriptor: SerialDescriptor =
+ PrimitiveSerialDescriptor("Date", PrimitiveKind.STRING)
+
+ private val dateFormat: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
+
+ override fun serialize(encoder: Encoder, value: Date): Unit =
+ encoder.encodeString(dateFormat.format(value))
+
+ override fun deserialize(decoder: Decoder): Date = dateFormat.parse(decoder.decodeString())!!
+}
+
+@Serializable
+internal class SerializableUpdateInfo(
+ private val uri: String,
+ private val component: String,
+ private val securityPatchLevel: String,
+ @Serializable(with = DateSerializer::class) private val publishedDate: Date
+) {
+ internal fun toUpdateInfo(): UpdateInfo =
+ UpdateInfo(uri, component, securityPatchLevel, publishedDate)
+}
/** Represents information about an available update for a component. */
public class UpdateInfo(
@@ -33,6 +64,10 @@
/** Date when the available update was published. */
public val publishedDate: Date
) {
+
+ internal fun toSerializableUpdateInfo(): SerializableUpdateInfo =
+ SerializableUpdateInfo(uri, component, securityPatchLevel, publishedDate)
+
/**
* Returns a string representation of the update information.
*
diff --git a/security/security-state/src/main/java/androidx/security/state/UpdateInfoProvider.kt b/security/security-state/src/main/java/androidx/security/state/UpdateInfoProvider.kt
index 7dec3e4..1c6c6e6 100644
--- a/security/security-state/src/main/java/androidx/security/state/UpdateInfoProvider.kt
+++ b/security/security-state/src/main/java/androidx/security/state/UpdateInfoProvider.kt
@@ -22,7 +22,7 @@
import android.database.Cursor
import android.database.MatrixCursor
import android.net.Uri
-import com.google.gson.Gson
+import kotlinx.serialization.json.Json
/**
* A content provider for managing and serving update information for system components. This class
@@ -179,7 +179,11 @@
val sharedPreferences = context.getSharedPreferences(updateInfoPrefs, Context.MODE_PRIVATE)
val editor = sharedPreferences?.edit()
val key = getKeyForUpdateInfo(updateInfo)
- val json = Gson().toJson(updateInfo)
+ val json =
+ Json.encodeToString(
+ SerializableUpdateInfo.serializer(),
+ updateInfo.toSerializableUpdateInfo()
+ )
editor?.putString(key, json)
editor?.apply()
}
@@ -218,7 +222,8 @@
for ((_, value) in allEntries) {
val json = value as? String
if (json != null) {
- val updateInfo: UpdateInfo = Gson().fromJson(json, UpdateInfo::class.java)
+ val serializableUpdateInfo: SerializableUpdateInfo = Json.decodeFromString(json)
+ val updateInfo: UpdateInfo = serializableUpdateInfo.toUpdateInfo()
allUpdates.add(updateInfo)
}
}
diff --git a/security/security-state/src/test/java/androidx/security/state/SecurityPatchStateTest.kt b/security/security-state/src/test/java/androidx/security/state/SecurityPatchStateTest.kt
index d3a8f6fd..7ab74c2 100644
--- a/security/security-state/src/test/java/androidx/security/state/SecurityPatchStateTest.kt
+++ b/security/security-state/src/test/java/androidx/security/state/SecurityPatchStateTest.kt
@@ -28,10 +28,10 @@
import android.os.Bundle
import androidx.annotation.RequiresApi
import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.gson.Gson
import java.time.LocalDate
import java.time.ZoneOffset
import java.util.Date
+import kotlinx.serialization.json.Json
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
@@ -56,6 +56,11 @@
.setSecurityPatchLevel("2022-01-01")
.setPublishedDate(Date.from(LocalDate.now().atStartOfDay().toInstant(ZoneOffset.UTC)))
.build()
+ private val updateInfoJson =
+ Json.encodeToString(
+ SerializableUpdateInfo.serializer(),
+ updateInfo.toSerializableUpdateInfo()
+ )
private val mockEmptyEditor: SharedPreferences.Editor = mock<SharedPreferences.Editor> {}
private val mockEditor: SharedPreferences.Editor =
mock<SharedPreferences.Editor> {
@@ -65,7 +70,7 @@
private val mockPrefs: SharedPreferences =
mock<SharedPreferences> {
on { edit() } doReturn mockEditor
- on { all } doReturn mapOf(Pair("key", Gson().toJson(updateInfo)))
+ on { all } doReturn mapOf(Pair("key", updateInfoJson))
}
private val mockPackageManager: PackageManager =
mock<PackageManager> {
@@ -77,7 +82,7 @@
mock<Cursor> {
on { moveToNext() } doReturn true doReturn false doReturn true doReturn false
on { getColumnIndexOrThrow(Mockito.eq("json")) } doReturn 123
- on { getString(Mockito.eq(123)) } doReturn Gson().toJson(updateInfo)
+ on { getString(Mockito.eq(123)) } doReturn updateInfoJson
}
private val mockContentResolver: ContentResolver =
mock<ContentResolver> {
diff --git a/security/security-state/src/test/java/androidx/security/state/UpdateInfoProviderTest.kt b/security/security-state/src/test/java/androidx/security/state/UpdateInfoProviderTest.kt
index ec7c5f8..6d3aff3 100644
--- a/security/security-state/src/test/java/androidx/security/state/UpdateInfoProviderTest.kt
+++ b/security/security-state/src/test/java/androidx/security/state/UpdateInfoProviderTest.kt
@@ -23,10 +23,10 @@
import android.net.Uri
import androidx.security.state.SecurityPatchState.Companion.COMPONENT_SYSTEM
import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.gson.Gson
import java.time.LocalDate
import java.time.ZoneOffset
import java.util.Date
+import kotlinx.serialization.json.Json
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
@@ -63,7 +63,11 @@
.setSecurityPatchLevel("2022-01-01")
.setPublishedDate(publishedDate)
.build()
- private val expectedJson = Gson().toJson(updateInfo)
+ private val expectedJson =
+ Json.encodeToString(
+ SerializableUpdateInfo.serializer(),
+ updateInfo.toSerializableUpdateInfo()
+ )
private val mockEmptyEditor: SharedPreferences.Editor = mock<SharedPreferences.Editor> {}
private val mockEditor: SharedPreferences.Editor =
mock<SharedPreferences.Editor> {
@@ -73,7 +77,7 @@
private val mockPrefs: SharedPreferences =
mock<SharedPreferences> {
on { edit() } doReturn mockEditor
- on { all } doReturn mapOf(Pair("key", Gson().toJson(updateInfo)))
+ on { all } doReturn mapOf(Pair("key", expectedJson))
}
private val mockContext: Context =
mock<Context> {