Switch from gson to kotlinx.serialization

Kotlin offers better null and type safety compared to gson

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2025-05-16 16:05:45 +08:00
parent e39bc3b8a5
commit eda01a6ec0
24 changed files with 96 additions and 85 deletions

View File

@@ -145,7 +145,6 @@ dependencies {
//Google's Goodies
implementation(libs.google.android.material)
implementation(libs.google.gson)
implementation(libs.google.protobuf.javalite)
//AndroidX

View File

@@ -19,6 +19,9 @@
package com.aurora.store.data.model
import kotlinx.serialization.Serializable
@Serializable
data class Auth(
val email: String,
val auth: String

View File

@@ -21,12 +21,16 @@ package com.aurora.store.data.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
@Serializable
data class ExodusReport(
val creator: String = String(),
val name: String = String(),
val reports: List<Report> = listOf()
)
@Serializable
@Parcelize
data class Report(
val id: Int = 0,
@@ -38,6 +42,7 @@ data class Report(
val trackers: List<Int> = listOf()
) : Parcelable
@Serializable
data class ExodusTracker(
val id: Int = 0,
val name: String = String(),

View File

@@ -19,6 +19,9 @@
package com.aurora.store.data.model
import kotlinx.serialization.Serializable
@Serializable
data class Filter(
val appsWithAds: Boolean = true,
val appsWithIAP: Boolean = true,

View File

@@ -2,27 +2,32 @@ package com.aurora.store.data.model
import androidx.annotation.StringRes
import com.aurora.store.R
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class PlexusReport(
@SerializedName("data")
@SerialName("data")
val report: Data?
)
@Serializable
data class Data(
val name: String,
val scores: Scores,
@SerializedName("updated_at")
@SerialName("updated_at")
val updatedAt: String
)
@Serializable
data class Scores(
@SerializedName("micro_g")
@SerialName("micro_g")
val microG: Rating,
@SerializedName("native")
@SerialName("native")
val aosp: Rating
)
@Serializable
data class Rating(
val denominator: Float,
val numerator: Float,

View File

@@ -27,14 +27,16 @@ import com.aurora.gplayapi.data.models.PlayFile
import com.aurora.store.BuildConfig
import com.aurora.store.R
import com.aurora.store.util.CertUtil
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class SelfUpdate(
@SerializedName("version_name") var versionName: String = String(),
@SerializedName("version_code") var versionCode: Long = 0,
@SerializedName("aurora_build") var auroraBuild: String = String(),
@SerializedName("fdroid_build") var fdroidBuild: String = String(),
@SerializedName("updated_on") var updatedOn: String = String(),
@SerialName("version_name") var versionName: String = String(),
@SerialName("version_code") var versionCode: Long = 0,
@SerialName("aurora_build") var auroraBuild: String = String(),
@SerialName("fdroid_build") var fdroidBuild: String = String(),
@SerialName("updated_on") var updatedOn: String = String(),
val changelog: String = String(),
val size: Long = 0L,
val timestamp: Long = 0L

View File

@@ -28,12 +28,12 @@ import com.aurora.store.data.model.Algorithm
import com.aurora.store.data.model.ProxyInfo
import com.aurora.store.util.Preferences
import com.aurora.store.util.Preferences.PREFERENCE_PROXY_INFO
import com.google.gson.Gson
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.serialization.json.Json
import okhttp3.CertificatePinner
import okhttp3.OkHttpClient
import java.io.ByteArrayInputStream
@@ -95,10 +95,10 @@ object OkHttpClientModule {
@Provides
@Singleton
fun providesProxyInstance(@ApplicationContext context: Context, gson: Gson): Proxy? {
fun providesProxyInstance(@ApplicationContext context: Context, json: Json): Proxy? {
val proxyInfoString = Preferences.getString(context, PREFERENCE_PROXY_INFO)
if (proxyInfoString.isNotBlank() && proxyInfoString != "{}") {
val proxyInfo = gson.fromJson(proxyInfoString, ProxyInfo::class.java)
val proxyInfo = json.decodeFromString<ProxyInfo>(proxyInfoString)
val proxy = Proxy(
if (proxyInfo.protocol.removeSuffix("5") == "SOCKS") Proxy.Type.SOCKS else Proxy.Type.HTTP,

View File

@@ -23,7 +23,6 @@ import android.content.Context
import android.util.Log
import com.aurora.gplayapi.data.models.AuthData
import com.aurora.gplayapi.data.models.PlayResponse
import com.aurora.gplayapi.helpers.AppDetailsHelper
import com.aurora.gplayapi.helpers.AuthHelper
import com.aurora.gplayapi.network.IHttpClient
import com.aurora.store.R
@@ -32,17 +31,17 @@ import com.aurora.store.data.model.Auth
import com.aurora.store.util.Preferences
import com.aurora.store.util.Preferences.PREFERENCE_AUTH_DATA
import com.aurora.store.util.Preferences.PREFERENCE_DISPENSER_URLS
import com.google.gson.Gson
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AuthProvider @Inject constructor(
@ApplicationContext private val context: Context,
private val gson: Gson,
private val json: Json,
private val spoofProvider: SpoofProvider,
private val httpClient: IHttpClient
) {
@@ -60,7 +59,7 @@ class AuthProvider @Inject constructor(
Log.i(TAG, "Loading saved AuthData")
val rawAuth: String = Preferences.getString(context, PREFERENCE_AUTH_DATA)
return if (rawAuth.isNotBlank()) {
gson.fromJson(rawAuth, AuthData::class.java)
json.decodeFromString<AuthData>(rawAuth)
} else {
null
}
@@ -117,7 +116,7 @@ class AuthProvider @Inject constructor(
if (!it.isSuccessful) throwError(it, context)
}
val auth = gson.fromJson(String(playResponse.responseBytes), Auth::class.java)
val auth = json.decodeFromString<Auth>(String(playResponse.responseBytes))
return@withContext Result.success(
AuthHelper.build(
email = auth.email,
@@ -139,7 +138,7 @@ class AuthProvider @Inject constructor(
* Saves given [AuthData]
*/
fun saveAuthData(authData: AuthData) {
Preferences.putString(context, PREFERENCE_AUTH_DATA, gson.toJson(authData))
Preferences.putString(context, PREFERENCE_AUTH_DATA, json.encodeToString(authData))
}
/**

View File

@@ -24,23 +24,22 @@ import android.content.SharedPreferences
import androidx.preference.PreferenceManager
import com.aurora.extensions.isNAndAbove
import com.aurora.store.util.Preferences
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.serialization.json.Json
import java.io.File
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class BlacklistProvider @Inject constructor(
private val gson: Gson,
private val json: Json,
@ApplicationContext val context: Context,
) {
private val PREFERENCE_BLACKLIST = "PREFERENCE_BLACKLIST"
var blacklist: MutableSet<String>
set(value) = Preferences.putString(context, PREFERENCE_BLACKLIST, gson.toJson(value))
set(value) = Preferences.putString(context, PREFERENCE_BLACKLIST, json.encodeToString(value))
get() {
return try {
val rawBlacklist = if (isNAndAbove) {
@@ -66,7 +65,7 @@ class BlacklistProvider @Inject constructor(
if (rawBlacklist!!.isEmpty())
mutableSetOf()
else
gson.fromJson(rawBlacklist, object : TypeToken<Set<String?>?>() {}.type)
json.decodeFromString<MutableSet<String>>(rawBlacklist)
} catch (e: Exception) {
mutableSetOf()
}

View File

@@ -23,14 +23,14 @@ import android.content.Context
import com.aurora.store.data.model.Filter
import com.aurora.store.util.Preferences
import com.aurora.store.util.remove
import com.google.gson.Gson
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.serialization.json.Json
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class FilterProvider @Inject constructor(
private val gson: Gson,
private val json: Json,
@ApplicationContext private val context: Context
) {
@@ -45,10 +45,10 @@ class FilterProvider @Inject constructor(
fun getSavedFilter(): Filter {
val rawFilter = Preferences.getString(context, PREFERENCE_FILTER)
return gson.fromJson(rawFilter.ifEmpty { "{}" }, Filter::class.java)
return json.decodeFromString<Filter>(rawFilter.ifEmpty { "{}" })
}
fun saveFilter(filter: Filter) {
Preferences.putString(context, PREFERENCE_FILTER, gson.toJson(filter))
Preferences.putString(context, PREFERENCE_FILTER, json.encodeToString(filter))
}
}

View File

@@ -23,8 +23,8 @@ import android.content.Context
import com.aurora.store.R
import com.aurora.store.util.Preferences
import com.aurora.store.util.Preferences.PREFERENCE_VENDING_VERSION
import com.google.gson.Gson
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.serialization.json.Json
import java.util.Locale
import java.util.Properties
import javax.inject.Inject
@@ -35,7 +35,7 @@ import javax.inject.Singleton
*/
@Singleton
class SpoofProvider @Inject constructor(
private val gson: Gson,
private val json: Json,
@ApplicationContext val context: Context,
) : SpoofDeviceProvider(context) {
@@ -85,9 +85,8 @@ class SpoofProvider @Inject constructor(
.build()
private val spoofDeviceProperties: Properties
get() = gson.fromJson(
Preferences.getString(context, DEVICE_SPOOF_PROPERTIES),
Properties::class.java
get() = json.decodeFromString<Properties>(
Preferences.getString(context, DEVICE_SPOOF_PROPERTIES)
)
fun setSpoofLocale(locale: Locale) {
@@ -98,7 +97,7 @@ class SpoofProvider @Inject constructor(
fun setSpoofDeviceProperties(properties: Properties) {
Preferences.putBoolean(context, DEVICE_SPOOF_ENABLED, true)
Preferences.putString(context, DEVICE_SPOOF_PROPERTIES, gson.toJson(properties))
Preferences.putString(context, DEVICE_SPOOF_PROPERTIES, json.encodeToString(properties))
}
fun removeSpoofLocale() {

View File

@@ -3,34 +3,31 @@ package com.aurora.store.data.room.download
import androidx.room.ProvidedTypeConverter
import androidx.room.TypeConverter
import com.aurora.gplayapi.data.models.PlayFile
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kotlinx.serialization.json.Json
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
@ProvidedTypeConverter
class DownloadConverter @Inject constructor(private val gson: Gson) {
class DownloadConverter @Inject constructor(private val json: Json) {
@TypeConverter
fun toSharedLibList(string: String): List<SharedLib> {
val listType = object : TypeToken<List<SharedLib>>() {}.type
return gson.fromJson(string, listType)
return json.decodeFromString<List<SharedLib>>(string)
}
@TypeConverter
fun fromSharedLibList(list: List<SharedLib>): String {
return gson.toJson(list)
return json.encodeToString(list)
}
@TypeConverter
fun toGPlayFileList(string: String): List<PlayFile> {
val listType = object : TypeToken<List<PlayFile>>() {}.type
return gson.fromJson(string, listType)
return json.decodeFromString<List<PlayFile>>(string)
}
@TypeConverter
fun fromGPlayFileList(list: List<PlayFile>): String {
return gson.toJson(list)
return json.encodeToString(list)
}
}

View File

@@ -4,7 +4,9 @@ import android.os.Parcelable
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.PlayFile
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
@Serializable
@Parcelize
data class SharedLib(
val packageName: String,

View File

@@ -6,7 +6,9 @@ import androidx.room.PrimaryKey
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.Artwork
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
@Serializable
@Parcelize
@Entity(tableName = "favourite")
data class Favourite(

View File

@@ -1,7 +1,9 @@
package com.aurora.store.data.room.favourite
import com.aurora.store.BuildConfig
import kotlinx.serialization.Serializable
@Serializable
data class ImportExport(
val favourites: List<Favourite>,
val auroraStoreVersion: Int = BuildConfig.VERSION_CODE,

View File

@@ -29,11 +29,11 @@ import com.aurora.store.util.NotificationUtil
import com.aurora.store.util.PackageUtil
import com.aurora.store.util.Preferences
import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_AUTO
import com.google.gson.Gson
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import java.util.Locale
/**
@@ -46,7 +46,7 @@ import java.util.Locale
*/
@HiltWorker
class UpdateWorker @AssistedInject constructor(
private val gson: Gson,
private val json: Json,
private val blacklistProvider: BlacklistProvider,
private val httpClient: IHttpClient,
private val updateDao: UpdateDao,
@@ -187,10 +187,7 @@ class UpdateWorker @AssistedInject constructor(
try {
val response = httpClient.get(updateUrl, mapOf())
val selfUpdate = gson.fromJson(
String(response.responseBytes),
SelfUpdate::class.java
)
val selfUpdate = json.decodeFromString<SelfUpdate>(String(response.responseBytes))
val isUpdate = when (BuildType.CURRENT) {
BuildType.NIGHTLY,

View File

@@ -1,11 +1,10 @@
package com.aurora.store.module
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.serialization.json.Json
import javax.inject.Singleton
@Module
@@ -14,9 +13,11 @@ object CommonModule {
@Singleton
@Provides
fun providesGsonInstance(): Gson {
return GsonBuilder()
.setPrettyPrinting()
.create()
fun providesJsonInstance(): Json {
return Json {
prettyPrint = true
ignoreUnknownKeys = true
coerceInputValues = true
}
}
}

View File

@@ -79,7 +79,7 @@ class ProxyURLDialog: DialogFragment() {
val proxyInfo = CommonUtil.parseProxyUrl(url)
if (proxyInfo != null) {
save(PREFERENCE_PROXY_URL, url)
save(PREFERENCE_PROXY_INFO, viewModel.gson.toJson(proxyInfo))
save(PREFERENCE_PROXY_INFO, viewModel.json.encodeToString(proxyInfo))
toast(R.string.toast_proxy_success)
findNavController().navigate(R.id.forceRestartDialog)
return

View File

@@ -33,19 +33,18 @@ import com.aurora.store.data.providers.BlacklistProvider
import com.aurora.store.util.CertUtil
import com.aurora.store.util.PackageUtil
import com.aurora.store.util.Preferences
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import javax.inject.Inject
@HiltViewModel
class BlacklistViewModel @Inject constructor(
private val gson: Gson,
private val json: Json,
private val updateHelper: UpdateHelper,
private val blacklistProvider: BlacklistProvider,
@ApplicationContext private val context: Context
@@ -132,9 +131,8 @@ class BlacklistViewModel @Inject constructor(
viewModelScope.launch(Dispatchers.IO) {
try {
context.contentResolver.openInputStream(uri)?.use {
val importedSet: MutableSet<String> = gson.fromJson(
it.bufferedReader().readText(),
object : TypeToken<MutableSet<String?>?>() {}.type
val importedSet = json.decodeFromString<MutableSet<String>>(
it.bufferedReader().readText()
)
val validImportedSet = importedSet
@@ -155,7 +153,7 @@ class BlacklistViewModel @Inject constructor(
viewModelScope.launch(Dispatchers.IO) {
try {
context.contentResolver.openOutputStream(uri)?.use {
it.write(gson.toJson(blacklistProvider.blacklist).encodeToByteArray())
it.write(json.encodeToString(blacklistProvider.blacklist).encodeToByteArray())
}
} catch (exception: Exception) {
Log.e(TAG, "Failed to export blacklist", exception)

View File

@@ -27,18 +27,18 @@ import androidx.lifecycle.viewModelScope
import com.aurora.store.data.room.favourite.Favourite
import com.aurora.store.data.room.favourite.FavouriteDao
import com.aurora.store.data.room.favourite.ImportExport
import com.google.gson.Gson
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import javax.inject.Inject
@HiltViewModel
class FavouriteViewModel @Inject constructor(
private val favouriteDao: FavouriteDao,
private val gson: Gson
private val json: Json
) : ViewModel() {
private val TAG = FavouriteViewModel::class.java.simpleName
@@ -49,9 +49,8 @@ class FavouriteViewModel @Inject constructor(
viewModelScope.launch(Dispatchers.IO) {
try {
context.contentResolver.openInputStream(uri)?.use {
val importExport = gson.fromJson(
it.bufferedReader().readText(),
ImportExport::class.java
val importExport = json.decodeFromString<ImportExport>(
it.bufferedReader().readText()
)
favouriteDao.insertAll(
@@ -68,7 +67,10 @@ class FavouriteViewModel @Inject constructor(
viewModelScope.launch(Dispatchers.IO) {
try {
context.contentResolver.openOutputStream(uri)?.use {
it.write(gson.toJson(ImportExport(favouritesList.value!!)).encodeToByteArray())
it.write(
json.encodeToString(ImportExport(favouritesList.value!!))
.encodeToByteArray()
)
}
} catch (exception: Exception) {
Log.e(TAG, "Failed to export favourites", exception)

View File

@@ -30,20 +30,20 @@ import com.aurora.store.data.providers.BlacklistProvider
import com.aurora.store.data.room.favourite.Favourite
import com.aurora.store.data.room.favourite.ImportExport
import com.aurora.store.util.PackageUtil
import com.google.gson.Gson
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import javax.inject.Inject
@HiltViewModel
class InstalledViewModel @Inject constructor(
@ApplicationContext private val context: Context,
private val blacklistProvider: BlacklistProvider,
private val gson: Gson,
private val json: Json,
private val webAppDetailsHelper: WebAppDetailsHelper
) : ViewModel() {
@@ -83,7 +83,7 @@ class InstalledViewModel @Inject constructor(
Favourite.fromApp(app, Favourite.Mode.IMPORT)
}
context.contentResolver.openOutputStream(uri)?.use {
it.write(gson.toJson(ImportExport(favourites)).encodeToByteArray())
it.write(json.encodeToString(ImportExport(favourites)).encodeToByteArray())
}
} catch (exception: Exception) {
Log.e(TAG, "Failed to installed apps", exception)

View File

@@ -23,7 +23,6 @@ import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.data.room.favourite.Favourite
import com.aurora.store.data.room.favourite.FavouriteDao
import com.aurora.store.util.PackageUtil
import com.google.gson.Gson
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
@@ -35,6 +34,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import org.json.JSONObject
import javax.inject.Inject
import com.aurora.gplayapi.data.models.datasafety.Report as DataSafetyReport
@@ -49,7 +49,7 @@ class AppDetailsViewModel @Inject constructor(
private val downloadHelper: DownloadHelper,
private val favouriteDao: FavouriteDao,
private val httpClient: IHttpClient,
private val gson: Gson
private val json: Json
) : ViewModel() {
private val TAG = AppDetailsViewModel::class.java.simpleName
@@ -159,7 +159,7 @@ class AppDetailsViewModel @Inject constructor(
val plexusReport = plexusReportStash.getOrPut(packageName) {
val url = "${Constants.PLEXUS_API_URL}/${packageName}/?scores=true"
val playResponse = httpClient.get(url, emptyMap())
gson.fromJson(String(playResponse.responseBytes), PlexusReport::class.java)
json.decodeFromString<PlexusReport>(String(playResponse.responseBytes))
}
_plexusReport.emit(plexusReport)
@@ -284,9 +284,7 @@ class AppDetailsViewModel @Inject constructor(
try {
val jsonObject = JSONObject(response)
val exodusObject = jsonObject.getJSONObject(packageName)
val exodusReport: ExodusReport = gson.fromJson(
exodusObject.toString(), ExodusReport::class.java
)
val exodusReport = json.decodeFromString<ExodusReport>(exodusObject.toString())
return exodusReport.reports
} catch (e: Exception) {

View File

@@ -6,9 +6,9 @@
package com.aurora.store.viewmodel.preferences
import androidx.lifecycle.ViewModel
import com.google.gson.Gson
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.serialization.json.Json
import javax.inject.Inject
@HiltViewModel
class ProxyURLViewModel @Inject constructor(val gson: Gson) : ViewModel()
class ProxyURLViewModel @Inject constructor(val json: Json) : ViewModel()

View File

@@ -9,8 +9,7 @@ composeBom = "2025.05.00"
core = "1.16.0"
epoxy = "5.1.4"
espresso = "3.6.1"
gplayapi = "3.5.0"
gson = "2.13.1"
gplayapi = "3.5.2"
hiddenapibypass = "6.1"
hilt = "2.56.2"
hiltWork = "1.2.0"
@@ -74,7 +73,6 @@ coil-network = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref =
facebook-shimmer = { module = "com.facebook.shimmer:shimmer", version.ref = "shimmer" }
github-topjohnwu-libsu = { module = "com.github.topjohnwu.libsu:core", version.ref = "libsu" }
google-android-material = { module = "com.google.android.material:material", version.ref = "material" }
google-gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
google-protobuf-javalite = { module = "com.google.protobuf:protobuf-javalite", version.ref = "protobufJavalite" }
google-truth = { module = "com.google.truth:truth", version.ref = "truth" }
hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" }