From 7153bca0705a6f94211e7610a5481797a213439d Mon Sep 17 00:00:00 2001 From: Binay Shaw Date: Fri, 18 Apr 2025 01:34:57 +0530 Subject: [PATCH 1/3] feat: replace gson with kotlinx.serialization --- app/build.gradle.kts | 4 +- .../account/security/FeverSecurityKey.kt | 2 +- .../account/security/FreshRSSSecurityKey.kt | 2 +- .../security/GoogleReaderSecurityKey.kt | 2 +- .../account/security/LocalSecurityKey.kt | 2 +- .../model/account/security/SecurityKey.kt | 14 +++++-- .../infrastructure/net/NetworkDataSource.kt | 8 +++- .../rss/provider/ProviderAPI.kt | 12 +++--- .../rss/provider/greader/GoogleReaderDTO.kt | 18 ++++++++- .../java/me/ash/reader/ui/ext/DataStoreExt.kt | 40 +++++++++++-------- gradle/libs.versions.toml | 9 ++++- 11 files changed, 76 insertions(+), 37 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ced8459f2..6172f38a0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -8,6 +8,7 @@ plugins { alias(libs.plugins.aboutlibraries) alias(libs.plugins.room) alias(libs.plugins.hilt) + alias(libs.plugins.kotlin.serialization) } fun fetchGitCommitHash(): String { @@ -157,7 +158,8 @@ dependencies { implementation(libs.okhttp) implementation(libs.okhttp.coroutines) implementation(libs.retrofit) - implementation(libs.retrofit.gson) + implementation(libs.converter.kotlinx.serialization) + implementation(libs.kotlin.serialization) implementation(libs.profileinstaller) implementation(libs.work.runtime.ktx) implementation(libs.datastore.preferences) diff --git a/app/src/main/java/me/ash/reader/domain/model/account/security/FeverSecurityKey.kt b/app/src/main/java/me/ash/reader/domain/model/account/security/FeverSecurityKey.kt index 55de0d664..38c48d32e 100644 --- a/app/src/main/java/me/ash/reader/domain/model/account/security/FeverSecurityKey.kt +++ b/app/src/main/java/me/ash/reader/domain/model/account/security/FeverSecurityKey.kt @@ -15,7 +15,7 @@ class FeverSecurityKey private constructor() : SecurityKey() { } constructor(value: String? = DESUtils.empty) : this() { - decode(value, FeverSecurityKey::class.java).let { + decode(value).let { serverUrl = it.serverUrl username = it.username password = it.password diff --git a/app/src/main/java/me/ash/reader/domain/model/account/security/FreshRSSSecurityKey.kt b/app/src/main/java/me/ash/reader/domain/model/account/security/FreshRSSSecurityKey.kt index 611d22741..59f2f6ea7 100644 --- a/app/src/main/java/me/ash/reader/domain/model/account/security/FreshRSSSecurityKey.kt +++ b/app/src/main/java/me/ash/reader/domain/model/account/security/FreshRSSSecurityKey.kt @@ -15,7 +15,7 @@ class FreshRSSSecurityKey private constructor() : SecurityKey() { } constructor(value: String? = DESUtils.empty) : this() { - decode(value, FreshRSSSecurityKey::class.java).let { + decode(value).let { serverUrl = it.serverUrl username = it.username password = it.password diff --git a/app/src/main/java/me/ash/reader/domain/model/account/security/GoogleReaderSecurityKey.kt b/app/src/main/java/me/ash/reader/domain/model/account/security/GoogleReaderSecurityKey.kt index f824191df..43d5b1550 100644 --- a/app/src/main/java/me/ash/reader/domain/model/account/security/GoogleReaderSecurityKey.kt +++ b/app/src/main/java/me/ash/reader/domain/model/account/security/GoogleReaderSecurityKey.kt @@ -15,7 +15,7 @@ class GoogleReaderSecurityKey private constructor() : SecurityKey() { } constructor(value: String? = DESUtils.empty) : this() { - decode(value, GoogleReaderSecurityKey::class.java).let { + decode(value).let { serverUrl = it.serverUrl username = it.username password = it.password diff --git a/app/src/main/java/me/ash/reader/domain/model/account/security/LocalSecurityKey.kt b/app/src/main/java/me/ash/reader/domain/model/account/security/LocalSecurityKey.kt index 4cedec6e0..e3c33ec3b 100644 --- a/app/src/main/java/me/ash/reader/domain/model/account/security/LocalSecurityKey.kt +++ b/app/src/main/java/me/ash/reader/domain/model/account/security/LocalSecurityKey.kt @@ -3,7 +3,7 @@ package me.ash.reader.domain.model.account.security class LocalSecurityKey private constructor() : SecurityKey() { constructor(value: String? = DESUtils.empty) : this() { - decode(value, LocalSecurityKey::class.java).let { + decode(value).let { } } diff --git a/app/src/main/java/me/ash/reader/domain/model/account/security/SecurityKey.kt b/app/src/main/java/me/ash/reader/domain/model/account/security/SecurityKey.kt index fb44d19d7..a15cc7ac9 100644 --- a/app/src/main/java/me/ash/reader/domain/model/account/security/SecurityKey.kt +++ b/app/src/main/java/me/ash/reader/domain/model/account/security/SecurityKey.kt @@ -1,14 +1,20 @@ package me.ash.reader.domain.model.account.security -import com.google.gson.Gson +import kotlinx.serialization.* +import kotlinx.serialization.json.* + +@Serializable abstract class SecurityKey { - fun decode(value: String?, classOfT: Class): T = - Gson().fromJson(DESUtils.decrypt(value?.ifEmpty { DESUtils.empty } ?: DESUtils.empty), classOfT) + inline fun decode(value: String?): T { + val decrypted = DESUtils.decrypt(value?.ifEmpty { DESUtils.empty } ?: DESUtils.empty) + return Json.decodeFromString(decrypted) + } override fun toString(): String { - return DESUtils.encrypt(Gson().toJson(this)) + val json = Json.encodeToString(serializer(), this) + return DESUtils.encrypt(json) } override fun equals(other: Any?): Boolean { diff --git a/app/src/main/java/me/ash/reader/infrastructure/net/NetworkDataSource.kt b/app/src/main/java/me/ash/reader/infrastructure/net/NetworkDataSource.kt index 1f0f99687..1ea696dbb 100644 --- a/app/src/main/java/me/ash/reader/infrastructure/net/NetworkDataSource.kt +++ b/app/src/main/java/me/ash/reader/infrastructure/net/NetworkDataSource.kt @@ -5,12 +5,14 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType import okhttp3.ResponseBody import retrofit2.Response import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory import retrofit2.http.GET import retrofit2.http.Streaming +import retrofit2.converter.kotlinx.serialization.asConverterFactory import retrofit2.http.Url import java.io.File @@ -28,10 +30,12 @@ interface NetworkDataSource { private var instance: NetworkDataSource? = null fun getInstance(): NetworkDataSource { + val networkJson = Json { ignoreUnknownKeys = true } return instance ?: synchronized(this) { instance ?: Retrofit.Builder() .baseUrl("https://api.github.com/") - .addConverterFactory(GsonConverterFactory.create()) +// .addConverterFactory(GsonConverterFactory.create()) + .addConverterFactory(networkJson.asConverterFactory("application/json".toMediaType())) .build().create(NetworkDataSource::class.java).also { instance = it } diff --git a/app/src/main/java/me/ash/reader/infrastructure/rss/provider/ProviderAPI.kt b/app/src/main/java/me/ash/reader/infrastructure/rss/provider/ProviderAPI.kt index 8745c2c5c..d23d2c6fe 100644 --- a/app/src/main/java/me/ash/reader/infrastructure/rss/provider/ProviderAPI.kt +++ b/app/src/main/java/me/ash/reader/infrastructure/rss/provider/ProviderAPI.kt @@ -1,8 +1,7 @@ package me.ash.reader.infrastructure.rss.provider import android.content.Context -import com.google.gson.Gson -import com.google.gson.GsonBuilder +import kotlinx.serialization.json.Json import me.ash.reader.infrastructure.di.UserAgentInterceptor import me.ash.reader.infrastructure.di.cachingHttpClient import okhttp3.OkHttpClient @@ -17,8 +16,9 @@ abstract class ProviderAPI(context: Context, clientCertificateAlias: String?) { .addNetworkInterceptor(UserAgentInterceptor) .build() - protected val gson: Gson = GsonBuilder().create() + protected val json: Json = Json { ignoreUnknownKeys = true } - protected inline fun toDTO(jsonStr: String): T = - gson.fromJson(jsonStr, T::class.java)!! -} + protected inline fun toDTO(jsonStr: String): T { + return json.decodeFromString(jsonStr) + } +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/infrastructure/rss/provider/greader/GoogleReaderDTO.kt b/app/src/main/java/me/ash/reader/infrastructure/rss/provider/greader/GoogleReaderDTO.kt index c233c4860..9cde290cd 100644 --- a/app/src/main/java/me/ash/reader/infrastructure/rss/provider/greader/GoogleReaderDTO.kt +++ b/app/src/main/java/me/ash/reader/infrastructure/rss/provider/greader/GoogleReaderDTO.kt @@ -1,11 +1,13 @@ package me.ash.reader.infrastructure.rss.provider.greader -import com.google.gson.annotations.SerializedName +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable object GoogleReaderDTO { + @Serializable data class GReaderError( - @SerializedName("errors") val errors: List, + @SerialName("errors") val errors: List, ) /** @@ -16,6 +18,7 @@ object GoogleReaderDTO { * "Auth": "demo/718*********************************7fa" * } */ + @Serializable data class MinifluxAuthData( val SID: String?, val LSID: String?, @@ -32,6 +35,7 @@ object GoogleReaderDTO { * "userEmail": "" * } */ + @Serializable data class User( val userId: String?, val userName: String?, @@ -60,10 +64,12 @@ object GoogleReaderDTO { * ] * } */ + @Serializable data class SubscriptionList( val subscriptions: List, ) + @Serializable data class Feed( val id: String?, val title: String?, @@ -74,6 +80,7 @@ object GoogleReaderDTO { val sortid: String?, ) + @Serializable data class Category( val id: String?, val label: String?, @@ -90,6 +97,7 @@ object GoogleReaderDTO { * } * */ + @Serializable data class QuickAddFeed( val numResults: Long?, val query: String?, @@ -108,6 +116,7 @@ object GoogleReaderDTO { * ] * } */ + @Serializable data class ItemIds( val itemRefs: List?, val continuation: String?, @@ -159,12 +168,14 @@ object GoogleReaderDTO { * ] * } */ + @Serializable data class ItemsContents( val id: String? = null, val updated: Long? = null, val items: List? = null, ) + @Serializable data class Item( val id: String? = null, val crawlTimeMsec: String? = null, @@ -179,14 +190,17 @@ object GoogleReaderDTO { val alternate: List? = null, ) + @Serializable data class Summary( val content: String? = null, ) + @Serializable data class CanonicalItem( val href: String? = null, ) + @Serializable data class OriginItem( val streamId: String? = null, val htmlUrl: String? = null, diff --git a/app/src/main/java/me/ash/reader/ui/ext/DataStoreExt.kt b/app/src/main/java/me/ash/reader/ui/ext/DataStoreExt.kt index 7185b9666..ccd01f7e4 100644 --- a/app/src/main/java/me/ash/reader/ui/ext/DataStoreExt.kt +++ b/app/src/main/java/me/ash/reader/ui/ext/DataStoreExt.kt @@ -11,14 +11,16 @@ import androidx.datastore.preferences.core.floatPreferencesKey import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject import java.io.IOException val Context.dataStore: DataStore by preferencesDataStore(name = "settings") @@ -261,29 +263,35 @@ val ignorePreferencesOnExportAndImport = listOf( suspend fun Context.fromDataStoreToJSONString(): String { val preferences = dataStore.data.first() - val map: Map = - preferences.asMap().mapKeys { it.key.name }.filterKeys { it !in ignorePreferencesOnExportAndImport } - return Gson().toJson(map) + val map: Map = preferences.asMap().mapKeys { it.key.name }.filterKeys { it !in ignorePreferencesOnExportAndImport } + + val jsonObject = buildJsonObject { + map.forEach { (key, value) -> + put(key, JsonPrimitive(value.toString())) + } + } + + return Json.encodeToString(JsonObject.serializer(), jsonObject) } suspend fun String.fromJSONStringToDataStore(context: Context) { - val gson = Gson() - val type = object : TypeToken>() {}.type - val map: Map = gson.fromJson(this, type) + val json = Json { ignoreUnknownKeys = true } + val jsonObject = json.decodeFromString(JsonObject.serializer(), this) + context.dataStore.edit { preferences -> - map.filterKeys { it !in ignorePreferencesOnExportAndImport }.forEach { (keyString, value) -> + jsonObject.filterKeys { it !in ignorePreferencesOnExportAndImport }.forEach { (keyString, value) -> val item = DataStoreKey.keys[keyString] Log.d("RLog", "fromJSONStringToDataStore: ${item?.key?.name}, ${item?.type}") - if (item != null) { + if (item != null && value is JsonPrimitive) { when (item.type) { - String::class.java -> preferences[item.key as Preferences.Key] = value as String - Int::class.java -> preferences[item.key as Preferences.Key] = (value as Double).toInt() - Boolean::class.java -> preferences[item.key as Preferences.Key] = value as Boolean - Float::class.java -> preferences[item.key as Preferences.Key] = (value as Double).toFloat() - Long::class.java -> preferences[item.key as Preferences.Key] = (value as Double).toLong() + String::class.java -> preferences[item.key as Preferences.Key] = value.content + Int::class.java -> preferences[item.key as Preferences.Key] = value.content.toInt() + Boolean::class.java -> preferences[item.key as Preferences.Key] = value.content.toBoolean() + Float::class.java -> preferences[item.key as Preferences.Key] = value.content.toFloat() + Long::class.java -> preferences[item.key as Preferences.Key] = value.content.toLong() else -> throw IllegalArgumentException("Unsupported type") } } } } -} +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ca38138c8..8e41395ed 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,6 +4,8 @@ androidGradlePlugin = "8.2.1" # Kotlin kotlin = "1.9.22" +kotlinSerialization = "1.4.1" +converterKotlinxSerialization = "2.11.0" ksp = "1.9.22-1.0.17" # AboutLibraries @@ -91,6 +93,9 @@ hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-com # AndroidX android-svg = { group = "com.caverock", name = "androidsvg-aar", version.ref = "androidSVG" } +kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinSerialization" } +converter-kotlinx-serialization = { module = "com.squareup.retrofit2:converter-kotlinx-serialization", version.ref = "converterKotlinxSerialization" } + opml-parser = { group = "be.ceau", name = "opml-parser", version.ref = "opmlParser" } readability4j = { group = "net.dankito.readability4j", name = "readability4j", version.ref = "readability4j" } rome = { group = "com.rometools", name = "rome", version.ref = "rome" } @@ -99,7 +104,6 @@ swipe = { group = "me.saket.swipe", name = "swipe", version.ref = "swipe" } okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } okhttp-coroutines = { group = "com.squareup.okhttp3", name = "okhttp-coroutines-jvm", version.ref = "okhttp" } retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit2" } -retrofit-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit2" } profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "profileinstaller" } work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work" } datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" } @@ -133,4 +137,5 @@ kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutLibsRelease" } room = { id = "androidx.room", version.ref = "room" } -hilt = { id = "com.google.dagger.hilt.android", version.ref = "hiltAndroid" } \ No newline at end of file +hilt = { id = "com.google.dagger.hilt.android", version.ref = "hiltAndroid" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } \ No newline at end of file From 08ed6f8aeb8d06ad0c43cc26985da1667bc37b17 Mon Sep 17 00:00:00 2001 From: Binay Shaw Date: Sat, 19 Apr 2025 16:50:36 +0530 Subject: [PATCH 2/3] Refactor: Change SecurityKey from abstract class to sealed class --- .../me/ash/reader/domain/model/account/security/SecurityKey.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/me/ash/reader/domain/model/account/security/SecurityKey.kt b/app/src/main/java/me/ash/reader/domain/model/account/security/SecurityKey.kt index a15cc7ac9..ee870b847 100644 --- a/app/src/main/java/me/ash/reader/domain/model/account/security/SecurityKey.kt +++ b/app/src/main/java/me/ash/reader/domain/model/account/security/SecurityKey.kt @@ -5,7 +5,7 @@ import kotlinx.serialization.json.* @Serializable -abstract class SecurityKey { +sealed class SecurityKey { inline fun decode(value: String?): T { val decrypted = DESUtils.decrypt(value?.ifEmpty { DESUtils.empty } ?: DESUtils.empty) From 24d341c4c898df99996a9456101a480a30ef791e Mon Sep 17 00:00:00 2001 From: Binay Shaw Date: Sat, 19 Apr 2025 16:55:15 +0530 Subject: [PATCH 3/3] add: Serializable annotation with SerialName to security keys subclasses --- .../reader/domain/model/account/security/FeverSecurityKey.kt | 5 +++++ .../domain/model/account/security/FreshRSSSecurityKey.kt | 5 +++++ .../domain/model/account/security/GoogleReaderSecurityKey.kt | 5 +++++ .../reader/domain/model/account/security/LocalSecurityKey.kt | 5 +++++ 4 files changed, 20 insertions(+) diff --git a/app/src/main/java/me/ash/reader/domain/model/account/security/FeverSecurityKey.kt b/app/src/main/java/me/ash/reader/domain/model/account/security/FeverSecurityKey.kt index 38c48d32e..d6ed9e307 100644 --- a/app/src/main/java/me/ash/reader/domain/model/account/security/FeverSecurityKey.kt +++ b/app/src/main/java/me/ash/reader/domain/model/account/security/FeverSecurityKey.kt @@ -1,5 +1,10 @@ package me.ash.reader.domain.model.account.security +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +@SerialName("fever-security-key") class FeverSecurityKey private constructor() : SecurityKey() { var serverUrl: String? = null diff --git a/app/src/main/java/me/ash/reader/domain/model/account/security/FreshRSSSecurityKey.kt b/app/src/main/java/me/ash/reader/domain/model/account/security/FreshRSSSecurityKey.kt index 59f2f6ea7..3a0fae467 100644 --- a/app/src/main/java/me/ash/reader/domain/model/account/security/FreshRSSSecurityKey.kt +++ b/app/src/main/java/me/ash/reader/domain/model/account/security/FreshRSSSecurityKey.kt @@ -1,5 +1,10 @@ package me.ash.reader.domain.model.account.security +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +@SerialName("fresh-rss-security-key") class FreshRSSSecurityKey private constructor() : SecurityKey() { var serverUrl: String? = null diff --git a/app/src/main/java/me/ash/reader/domain/model/account/security/GoogleReaderSecurityKey.kt b/app/src/main/java/me/ash/reader/domain/model/account/security/GoogleReaderSecurityKey.kt index 43d5b1550..0365c2989 100644 --- a/app/src/main/java/me/ash/reader/domain/model/account/security/GoogleReaderSecurityKey.kt +++ b/app/src/main/java/me/ash/reader/domain/model/account/security/GoogleReaderSecurityKey.kt @@ -1,5 +1,10 @@ package me.ash.reader.domain.model.account.security +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +@SerialName("google-reader-security-key") class GoogleReaderSecurityKey private constructor() : SecurityKey() { var serverUrl: String? = null diff --git a/app/src/main/java/me/ash/reader/domain/model/account/security/LocalSecurityKey.kt b/app/src/main/java/me/ash/reader/domain/model/account/security/LocalSecurityKey.kt index e3c33ec3b..0514840fa 100644 --- a/app/src/main/java/me/ash/reader/domain/model/account/security/LocalSecurityKey.kt +++ b/app/src/main/java/me/ash/reader/domain/model/account/security/LocalSecurityKey.kt @@ -1,5 +1,10 @@ package me.ash.reader.domain.model.account.security +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +@SerialName("local-security-key") class LocalSecurityKey private constructor() : SecurityKey() { constructor(value: String? = DESUtils.empty) : this() {