Replace gson with kotlinx-serialization

This commit is contained in:
Maksim Karasev 2024-10-03 16:38:23 +03:00
parent fa9d259a21
commit 6a5f405211
7 changed files with 50 additions and 53 deletions

3
.gitignore vendored
View file

@ -67,3 +67,6 @@ fastlane/Preview.html
fastlane/screenshots fastlane/screenshots
fastlane/test_output fastlane/test_output
fastlane/readme.md fastlane/readme.md
# kotlin
.kotlin/

2
.idea/kotlinc.xml generated
View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="KotlinJpsPluginSettings"> <component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.10" /> <option name="version" value="2.0.20" />
</component> </component>
</project> </project>

View file

@ -2,6 +2,7 @@ plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'com.google.devtools.ksp' id 'com.google.devtools.ksp'
id 'org.jetbrains.kotlin.plugin.serialization'
} }
android { android {
@ -51,11 +52,12 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.recyclerview:recyclerview:1.3.2' implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation 'androidx.activity:activity-ktx:1.9.2' implementation 'androidx.activity:activity-ktx:1.9.2'
implementation 'androidx.fragment:fragment-ktx:1.8.3' implementation 'androidx.fragment:fragment-ktx:1.8.4'
implementation 'com.google.android.material:material:1.12.0' implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'com.google.guava:guava:33.1.0-android' implementation 'com.google.guava:guava:33.1.0-android'
implementation 'com.google.code.gson:gson:2.11.0' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3'
def shizuku_version = '13.1.5' def shizuku_version = '13.1.5'
implementation "dev.rikka.shizuku:api:$shizuku_version" implementation "dev.rikka.shizuku:api:$shizuku_version"
@ -71,7 +73,7 @@ dependencies {
androidTestImplementation "androidx.room:room-testing:$roomVersion" androidTestImplementation "androidx.room:room-testing:$roomVersion"
// Lifecycle components // Lifecycle components
def lifecycleVersion = '2.8.5' def lifecycleVersion = '2.8.6'
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"

View file

@ -3,19 +3,21 @@ package ru.karasevm.privatednstoggle.model
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.google.gson.annotations.SerializedName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
// All fields must have default values for proper deserialization // All fields must have default values for proper deserialization
@Serializable
@Entity(tableName = "dns_servers") @Entity(tableName = "dns_servers")
data class DnsServer( data class DnsServer(
@SerializedName("id") @SerialName("id")
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
val id: Int = 0, val id: Int = 0,
@SerializedName("server") @SerialName("server")
val server: String = "", val server: String = "",
@SerializedName("label") @SerialName("label")
val label: String = "", val label: String = "",
@SerializedName("enabled") @SerialName("enabled")
@ColumnInfo(defaultValue = "1") @ColumnInfo(defaultValue = "1")
val enabled: Boolean = true, val enabled: Boolean = true,
val sortOrder: Int? = null val sortOrder: Int? = null

View file

@ -27,12 +27,9 @@ import androidx.recyclerview.widget.ItemTouchHelper.DOWN
import androidx.recyclerview.widget.ItemTouchHelper.UP import androidx.recyclerview.widget.ItemTouchHelper.UP
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonSyntaxException
import com.google.gson.ToNumberPolicy
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.lsposed.hiddenapibypass.HiddenApiBypass import org.lsposed.hiddenapibypass.HiddenApiBypass
import rikka.shizuku.Shizuku import rikka.shizuku.Shizuku
import rikka.shizuku.ShizukuBinderWrapper import rikka.shizuku.ShizukuBinderWrapper
@ -58,7 +55,6 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
private lateinit var sharedPrefs: SharedPreferences private lateinit var sharedPrefs: SharedPreferences
private lateinit var adapter: ServerListRecyclerAdapter private lateinit var adapter: ServerListRecyclerAdapter
private lateinit var clipboard: ClipboardManager private lateinit var clipboard: ClipboardManager
private lateinit var gson: Gson
private val dnsServerViewModel: DnsServerViewModel by viewModels { DnsServerViewModelFactory((application as PrivateDNSApp).repository) } private val dnsServerViewModel: DnsServerViewModel by viewModels { DnsServerViewModelFactory((application as PrivateDNSApp).repository) }
private val itemTouchHelper by lazy { private val itemTouchHelper by lazy {
@ -126,7 +122,7 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
private fun importSettings(json: String) { private fun importSettings(json: String) {
runCatching { runCatching {
val data: BackupUtils.Backup = gson.fromJson(json, BackupUtils.Backup::class.java) val data: BackupUtils.Backup = Json.decodeFromString<BackupUtils.Backup>(json)
BackupUtils.import(data, dnsServerViewModel, sharedPrefs) BackupUtils.import(data, dnsServerViewModel, sharedPrefs)
}.onSuccess { }.onSuccess {
Toast.makeText( Toast.makeText(
@ -134,28 +130,18 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
).show() ).show()
}.onFailure { exception -> }.onFailure { exception ->
runCatching { runCatching {
val objectType = object : TypeToken<Map<String, Any>>() {}.type Log.e("IMPORT", "Malformed json, falling back to legacy", exception)
val data: Map<String, Any> = gson.fromJson(json, objectType) val data = Json.decodeFromString<BackupUtils.LegacyBackup>(json)
BackupUtils.importLegacy(data, dnsServerViewModel, sharedPrefs) BackupUtils.importLegacy(data, dnsServerViewModel, sharedPrefs)
}.onSuccess { }.onSuccess {
Toast.makeText( Toast.makeText(
this, getString(R.string.import_success), Toast.LENGTH_SHORT this, getString(R.string.import_success), Toast.LENGTH_SHORT
).show() ).show()
}.onFailure { }.onFailure { exception ->
Log.e("IMPORT", "Import failed", exception) Log.e("IMPORT", "Import failed", exception)
when (exception) { Toast.makeText(
is JsonSyntaxException -> { this, getString(R.string.import_failure), Toast.LENGTH_SHORT
Toast.makeText( ).show()
this, getString(R.string.import_failure_json), Toast.LENGTH_SHORT
).show()
}
else -> {
Toast.makeText(
this, getString(R.string.import_failure), Toast.LENGTH_SHORT
).show()
}
}
} }
} }
} }
@ -193,7 +179,6 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
sharedPrefs = PreferenceHelper.defaultPreference(this) sharedPrefs = PreferenceHelper.defaultPreference(this)
clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
gson = GsonBuilder().setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE).create()
migrateServerList() migrateServerList()
@ -249,7 +234,7 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
R.id.export_settings_clipboard -> { R.id.export_settings_clipboard -> {
dnsServerViewModel.viewModelScope.launch { dnsServerViewModel.viewModelScope.launch {
val data = BackupUtils.export(dnsServerViewModel, sharedPrefs) val data = BackupUtils.export(dnsServerViewModel, sharedPrefs)
val jsonData = gson.toJson(data) val jsonData = Json.encodeToString(data)
clipboard.setPrimaryClip(ClipData.newPlainText("", jsonData)) clipboard.setPrimaryClip(ClipData.newPlainText("", jsonData))
// Only show a toast for Android 12 and lower. // Only show a toast for Android 12 and lower.
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) Toast.makeText( if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) Toast.makeText(
@ -263,7 +248,7 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
val activityContext = this val activityContext = this
dnsServerViewModel.viewModelScope.launch { dnsServerViewModel.viewModelScope.launch {
val data = BackupUtils.export(dnsServerViewModel, sharedPrefs) val data = BackupUtils.export(dnsServerViewModel, sharedPrefs)
val jsonData = gson.toJson(data) val jsonData = Json.encodeToString(data)
ShareCompat.IntentBuilder(activityContext).setText(jsonData) ShareCompat.IntentBuilder(activityContext).setText(jsonData)
.setType("text/plain") .setType("text/plain")
.startChooser() .startChooser()
@ -319,7 +304,7 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
if (result.resultCode == RESULT_OK) { if (result.resultCode == RESULT_OK) {
val data: Intent? = result.data val data: Intent? = result.data
data?.data?.also { uri -> data?.data?.also { uri ->
val jsonData = gson.toJson(BackupUtils.export(dnsServerViewModel, sharedPrefs)) val jsonData = Json.encodeToString(BackupUtils.export(dnsServerViewModel, sharedPrefs))
val contentResolver = applicationContext.contentResolver val contentResolver = applicationContext.contentResolver
runCatching { runCatching {
contentResolver.openOutputStream(uri)?.use { outputStream -> contentResolver.openOutputStream(uri)?.use { outputStream ->

View file

@ -1,20 +1,26 @@
package ru.karasevm.privatednstoggle.util package ru.karasevm.privatednstoggle.util
import android.content.SharedPreferences import android.content.SharedPreferences
import com.google.gson.annotations.SerializedName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import ru.karasevm.privatednstoggle.data.DnsServerViewModel import ru.karasevm.privatednstoggle.data.DnsServerViewModel
import ru.karasevm.privatednstoggle.model.DnsServer import ru.karasevm.privatednstoggle.model.DnsServer
import ru.karasevm.privatednstoggle.util.PreferenceHelper.AUTO_MODE
import ru.karasevm.privatednstoggle.util.PreferenceHelper.DNS_SERVERS
import ru.karasevm.privatednstoggle.util.PreferenceHelper.REQUIRE_UNLOCK
import ru.karasevm.privatednstoggle.util.PreferenceHelper.autoMode import ru.karasevm.privatednstoggle.util.PreferenceHelper.autoMode
import ru.karasevm.privatednstoggle.util.PreferenceHelper.requireUnlock import ru.karasevm.privatednstoggle.util.PreferenceHelper.requireUnlock
object BackupUtils { object BackupUtils {
@Serializable
data class Backup( data class Backup(
@SerializedName("dns_servers") val dnsServers: List<DnsServer>, @SerialName("dns_servers") val dnsServers: List<DnsServer>,
@SerializedName("auto_mode") val autoMode: Int?, @SerialName("auto_mode") val autoMode: Int?,
@SerializedName("require_unlock") val requireUnlock: Boolean?, @SerialName("require_unlock") val requireUnlock: Boolean?,
)
@Serializable
data class LegacyBackup(
@SerialName("dns_servers") val dnsServers: String,
@SerialName("auto_mode") val autoMode: Int?,
@SerialName("require_unlock") val requireUnlock: Boolean?,
) )
/** /**
@ -47,17 +53,16 @@ object BackupUtils {
/** /**
* Imports old server list * Imports old server list
* @param map Deserialized backup * @param legacyBackup Deserialized backup
* @param viewModel View model * @param viewModel View model
* @param sharedPreferences Shared preferences * @param sharedPreferences Shared preferences
*/ */
fun importLegacy( fun importLegacy(
map: Map<String, Any>, legacyBackup: LegacyBackup,
viewModel: DnsServerViewModel, viewModel: DnsServerViewModel,
sharedPreferences: SharedPreferences sharedPreferences: SharedPreferences
) { ) {
map[DNS_SERVERS]?.let { servers -> legacyBackup.dnsServers.let { servers ->
if (servers is String) {
val serverList = servers.split(",") val serverList = servers.split(",")
serverList.forEach { server -> serverList.forEach { server ->
val parts = server.split(" : ") val parts = server.split(" : ")
@ -67,9 +72,8 @@ object BackupUtils {
viewModel.insert(DnsServer(0, server, "")) viewModel.insert(DnsServer(0, server, ""))
} }
} }
}
} }
sharedPreferences.autoMode = map[AUTO_MODE] as? Int ?: 0 sharedPreferences.autoMode = legacyBackup.autoMode?: 0
sharedPreferences.requireUnlock = map[REQUIRE_UNLOCK] as? Boolean ?: false sharedPreferences.requireUnlock = legacyBackup.requireUnlock?: false
} }
} }

View file

@ -6,15 +6,16 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:8.5.2' classpath 'com.android.tools.build:gradle:8.5.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.20"
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files
} }
} }
plugins { plugins {
id 'org.jetbrains.kotlin.android' version '1.9.10' apply false id 'org.jetbrains.kotlin.android' version '2.0.20' apply false
id "com.google.devtools.ksp" version "1.9.10-1.0.13" apply false id 'org.jetbrains.kotlin.plugin.serialization' version '2.0.20'
id "com.google.devtools.ksp" version "2.0.20-1.0.25" apply false
} }
tasks.register('clean', Delete) { tasks.register('clean', Delete) {