diff --git a/.gitignore b/.gitignore index 8c470b3..c216f78 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,6 @@ fastlane/Preview.html fastlane/screenshots fastlane/test_output fastlane/readme.md + +# kotlin +.kotlin/ diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index f8467b4..d4b7acc 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 2d17775..38f3438 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,6 +2,7 @@ plugins { id 'com.android.application' id 'kotlin-android' id 'com.google.devtools.ksp' + id 'org.jetbrains.kotlin.plugin.serialization' } android { @@ -51,11 +52,12 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'androidx.recyclerview:recyclerview:1.3.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 'androidx.constraintlayout:constraintlayout:2.1.4' 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' implementation "dev.rikka.shizuku:api:$shizuku_version" @@ -71,7 +73,7 @@ dependencies { androidTestImplementation "androidx.room:room-testing:$roomVersion" // Lifecycle components - def lifecycleVersion = '2.8.5' + def lifecycleVersion = '2.8.6' implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion" diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/model/DnsServer.kt b/app/src/main/java/ru/karasevm/privatednstoggle/model/DnsServer.kt index 35375d1..38c1be4 100644 --- a/app/src/main/java/ru/karasevm/privatednstoggle/model/DnsServer.kt +++ b/app/src/main/java/ru/karasevm/privatednstoggle/model/DnsServer.kt @@ -3,19 +3,21 @@ package ru.karasevm.privatednstoggle.model import androidx.room.ColumnInfo import androidx.room.Entity 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 +@Serializable @Entity(tableName = "dns_servers") data class DnsServer( - @SerializedName("id") + @SerialName("id") @PrimaryKey(autoGenerate = true) val id: Int = 0, - @SerializedName("server") + @SerialName("server") val server: String = "", - @SerializedName("label") + @SerialName("label") val label: String = "", - @SerializedName("enabled") + @SerialName("enabled") @ColumnInfo(defaultValue = "1") val enabled: Boolean = true, val sortOrder: Int? = null diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/ui/MainActivity.kt b/app/src/main/java/ru/karasevm/privatednstoggle/ui/MainActivity.kt index 1ee354e..e029c25 100644 --- a/app/src/main/java/ru/karasevm/privatednstoggle/ui/MainActivity.kt +++ b/app/src/main/java/ru/karasevm/privatednstoggle/ui/MainActivity.kt @@ -27,12 +27,9 @@ import androidx.recyclerview.widget.ItemTouchHelper.DOWN import androidx.recyclerview.widget.ItemTouchHelper.UP import androidx.recyclerview.widget.LinearLayoutManager 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.serialization.encodeToString +import kotlinx.serialization.json.Json import org.lsposed.hiddenapibypass.HiddenApiBypass import rikka.shizuku.Shizuku import rikka.shizuku.ShizukuBinderWrapper @@ -58,7 +55,6 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi private lateinit var sharedPrefs: SharedPreferences private lateinit var adapter: ServerListRecyclerAdapter private lateinit var clipboard: ClipboardManager - private lateinit var gson: Gson private val dnsServerViewModel: DnsServerViewModel by viewModels { DnsServerViewModelFactory((application as PrivateDNSApp).repository) } private val itemTouchHelper by lazy { @@ -126,7 +122,7 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi private fun importSettings(json: String) { runCatching { - val data: BackupUtils.Backup = gson.fromJson(json, BackupUtils.Backup::class.java) + val data: BackupUtils.Backup = Json.decodeFromString(json) BackupUtils.import(data, dnsServerViewModel, sharedPrefs) }.onSuccess { Toast.makeText( @@ -134,28 +130,18 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi ).show() }.onFailure { exception -> runCatching { - val objectType = object : TypeToken>() {}.type - val data: Map = gson.fromJson(json, objectType) + Log.e("IMPORT", "Malformed json, falling back to legacy", exception) + val data = Json.decodeFromString(json) BackupUtils.importLegacy(data, dnsServerViewModel, sharedPrefs) }.onSuccess { Toast.makeText( this, getString(R.string.import_success), Toast.LENGTH_SHORT ).show() - }.onFailure { + }.onFailure { exception -> Log.e("IMPORT", "Import failed", exception) - when (exception) { - is JsonSyntaxException -> { - Toast.makeText( - this, getString(R.string.import_failure_json), Toast.LENGTH_SHORT - ).show() - } - - else -> { - Toast.makeText( - this, getString(R.string.import_failure), Toast.LENGTH_SHORT - ).show() - } - } + 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) clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager - gson = GsonBuilder().setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE).create() migrateServerList() @@ -249,7 +234,7 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi R.id.export_settings_clipboard -> { dnsServerViewModel.viewModelScope.launch { val data = BackupUtils.export(dnsServerViewModel, sharedPrefs) - val jsonData = gson.toJson(data) + val jsonData = Json.encodeToString(data) clipboard.setPrimaryClip(ClipData.newPlainText("", jsonData)) // Only show a toast for Android 12 and lower. if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) Toast.makeText( @@ -263,7 +248,7 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi val activityContext = this dnsServerViewModel.viewModelScope.launch { val data = BackupUtils.export(dnsServerViewModel, sharedPrefs) - val jsonData = gson.toJson(data) + val jsonData = Json.encodeToString(data) ShareCompat.IntentBuilder(activityContext).setText(jsonData) .setType("text/plain") .startChooser() @@ -319,7 +304,7 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi if (result.resultCode == RESULT_OK) { val data: Intent? = result.data data?.data?.also { uri -> - val jsonData = gson.toJson(BackupUtils.export(dnsServerViewModel, sharedPrefs)) + val jsonData = Json.encodeToString(BackupUtils.export(dnsServerViewModel, sharedPrefs)) val contentResolver = applicationContext.contentResolver runCatching { contentResolver.openOutputStream(uri)?.use { outputStream -> diff --git a/app/src/main/java/ru/karasevm/privatednstoggle/util/BackupUtils.kt b/app/src/main/java/ru/karasevm/privatednstoggle/util/BackupUtils.kt index 715c720..549dd9c 100644 --- a/app/src/main/java/ru/karasevm/privatednstoggle/util/BackupUtils.kt +++ b/app/src/main/java/ru/karasevm/privatednstoggle/util/BackupUtils.kt @@ -1,20 +1,26 @@ package ru.karasevm.privatednstoggle.util 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.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.requireUnlock object BackupUtils { + @Serializable data class Backup( - @SerializedName("dns_servers") val dnsServers: List, - @SerializedName("auto_mode") val autoMode: Int?, - @SerializedName("require_unlock") val requireUnlock: Boolean?, + @SerialName("dns_servers") val dnsServers: List, + @SerialName("auto_mode") val autoMode: Int?, + @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 - * @param map Deserialized backup + * @param legacyBackup Deserialized backup * @param viewModel View model * @param sharedPreferences Shared preferences */ fun importLegacy( - map: Map, + legacyBackup: LegacyBackup, viewModel: DnsServerViewModel, sharedPreferences: SharedPreferences ) { - map[DNS_SERVERS]?.let { servers -> - if (servers is String) { + legacyBackup.dnsServers.let { servers -> val serverList = servers.split(",") serverList.forEach { server -> val parts = server.split(" : ") @@ -67,9 +72,8 @@ object BackupUtils { viewModel.insert(DnsServer(0, server, "")) } } - } } - sharedPreferences.autoMode = map[AUTO_MODE] as? Int ?: 0 - sharedPreferences.requireUnlock = map[REQUIRE_UNLOCK] as? Boolean ?: false + sharedPreferences.autoMode = legacyBackup.autoMode?: 0 + sharedPreferences.requireUnlock = legacyBackup.requireUnlock?: false } } \ No newline at end of file diff --git a/build.gradle b/build.gradle index 59c7c50..5ba942e 100644 --- a/build.gradle +++ b/build.gradle @@ -6,15 +6,16 @@ buildscript { } dependencies { 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 // in the individual module build.gradle files } } plugins { - id 'org.jetbrains.kotlin.android' version '1.9.10' apply false - id "com.google.devtools.ksp" version "1.9.10-1.0.13" apply false + id 'org.jetbrains.kotlin.android' version '2.0.20' 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) {