Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -15,7 +20,7 @@ class FeverSecurityKey private constructor() : SecurityKey() {
}

constructor(value: String? = DESUtils.empty) : this() {
decode(value, FeverSecurityKey::class.java).let {
decode<FeverSecurityKey>(value).let {
serverUrl = it.serverUrl
username = it.username
password = it.password
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -15,7 +20,7 @@ class FreshRSSSecurityKey private constructor() : SecurityKey() {
}

constructor(value: String? = DESUtils.empty) : this() {
decode(value, FreshRSSSecurityKey::class.java).let {
decode<FreshRSSSecurityKey>(value).let {
serverUrl = it.serverUrl
username = it.username
password = it.password
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -15,7 +20,7 @@ class GoogleReaderSecurityKey private constructor() : SecurityKey() {
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made the requested changes. I followed the 1st approach, i.e. going with sealded classes instead of abstract classes. Also marking Serializable to it's subclasses. Now one step more, I also added custom serial name to their subclasses.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unsure if @Serializable is needed on the base class, and I don't think a sealed class is necessary. I propose creating some fake objects and the corresponding JSON produced by previous Gson implementation to compare against in unit tests.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand. I actually followed this rule: https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md#concrete-properties-in-a-base-class

Moreover, I can you please help me with how do I reproduce this to test it out? I mean the JSON response before and after the implementation. Do we need to write testcases for it? If yes can you help me with the dummy data which is valid for GSON?

constructor(value: String? = DESUtils.empty) : this() {
decode(value, GoogleReaderSecurityKey::class.java).let {
decode<GoogleReaderSecurityKey>(value).let {
serverUrl = it.serverUrl
username = it.username
password = it.password
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
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() {
decode(value, LocalSecurityKey::class.java).let {
decode<LocalSecurityKey>(value).let {

}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
package me.ash.reader.domain.model.account.security

import com.google.gson.Gson
import kotlinx.serialization.*
import kotlinx.serialization.json.*

abstract class SecurityKey {

fun <T> decode(value: String?, classOfT: Class<T>): T =
Gson().fromJson(DESUtils.decrypt(value?.ifEmpty { DESUtils.empty } ?: DESUtils.empty), classOfT)
@Serializable
sealed class SecurityKey {

inline fun <reified T> decode(value: String?): T {
val decrypted = DESUtils.decrypt(value?.ifEmpty { DESUtils.empty } ?: DESUtils.empty)
return Json.decodeFromString<T>(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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 <reified T> toDTO(jsonStr: String): T =
gson.fromJson(jsonStr, T::class.java)!!
}
protected inline fun <reified T> toDTO(jsonStr: String): T {
return json.decodeFromString(jsonStr)
}
}
Original file line number Diff line number Diff line change
@@ -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<String>,
@SerialName("errors") val errors: List<String>,
)

/**
Expand All @@ -16,6 +18,7 @@ object GoogleReaderDTO {
* "Auth": "demo/718*********************************7fa"
* }
*/
@Serializable
data class MinifluxAuthData(
val SID: String?,
val LSID: String?,
Expand All @@ -32,6 +35,7 @@ object GoogleReaderDTO {
* "userEmail": ""
* }
*/
@Serializable
data class User(
val userId: String?,
val userName: String?,
Expand Down Expand Up @@ -60,10 +64,12 @@ object GoogleReaderDTO {
* ]
* }
*/
@Serializable
data class SubscriptionList(
val subscriptions: List<Feed>,
)

@Serializable
data class Feed(
val id: String?,
val title: String?,
Expand All @@ -74,6 +80,7 @@ object GoogleReaderDTO {
val sortid: String?,
)

@Serializable
data class Category(
val id: String?,
val label: String?,
Expand All @@ -90,6 +97,7 @@ object GoogleReaderDTO {
* }
*
*/
@Serializable
data class QuickAddFeed(
val numResults: Long?,
val query: String?,
Expand All @@ -108,6 +116,7 @@ object GoogleReaderDTO {
* ]
* }
*/
@Serializable
data class ItemIds(
val itemRefs: List<Item>?,
val continuation: String?,
Expand Down Expand Up @@ -159,12 +168,14 @@ object GoogleReaderDTO {
* ]
* }
*/
@Serializable
data class ItemsContents(
val id: String? = null,
val updated: Long? = null,
val items: List<Item>? = null,
)

@Serializable
data class Item(
val id: String? = null,
val crawlTimeMsec: String? = null,
Expand All @@ -179,14 +190,17 @@ object GoogleReaderDTO {
val alternate: List<CanonicalItem>? = 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,
Expand Down
40 changes: 24 additions & 16 deletions app/src/main/java/me/ash/reader/ui/ext/DataStoreExt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<Preferences> by preferencesDataStore(name = "settings")
Expand Down Expand Up @@ -261,29 +263,35 @@ val ignorePreferencesOnExportAndImport = listOf(

suspend fun Context.fromDataStoreToJSONString(): String {
val preferences = dataStore.data.first()
val map: Map<String, Any?> =
preferences.asMap().mapKeys { it.key.name }.filterKeys { it !in ignorePreferencesOnExportAndImport }
return Gson().toJson(map)
val map: Map<String, Any?> = 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<Map<String, *>>() {}.type
val map: Map<String, Any> = 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<String>] = value as String
Int::class.java -> preferences[item.key as Preferences.Key<Int>] = (value as Double).toInt()
Boolean::class.java -> preferences[item.key as Preferences.Key<Boolean>] = value as Boolean
Float::class.java -> preferences[item.key as Preferences.Key<Float>] = (value as Double).toFloat()
Long::class.java -> preferences[item.key as Preferences.Key<Long>] = (value as Double).toLong()
String::class.java -> preferences[item.key as Preferences.Key<String>] = value.content
Int::class.java -> preferences[item.key as Preferences.Key<Int>] = value.content.toInt()
Boolean::class.java -> preferences[item.key as Preferences.Key<Boolean>] = value.content.toBoolean()
Float::class.java -> preferences[item.key as Preferences.Key<Float>] = value.content.toFloat()
Long::class.java -> preferences[item.key as Preferences.Key<Long>] = value.content.toLong()
else -> throw IllegalArgumentException("Unsupported type")
}
}
}
}
}
}
9 changes: 7 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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" }
Expand All @@ -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" }
Expand Down Expand Up @@ -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" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hiltAndroid" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
Loading