From ebe5e5c0097632809ba1ed4fc54d3ae79930efbe Mon Sep 17 00:00:00 2001 From: Alex Baker Date: Sun, 12 May 2024 02:37:46 -0500 Subject: [PATCH] Replace gson with kotlin serialization --- app/build.gradle.kts | 1 - .../java/org/tasks/billing/Purchase.kt | 13 +++- .../java/org/tasks/extensions/JsonObject.kt | 9 --- .../java/org/tasks/location/GeocoderMapbox.kt | 42 +++++++------ .../org/tasks/location/GeocoderNominatim.kt | 37 ++++++------ .../org/tasks/location/PlaceSearchGoogle.kt | 59 +++++++++---------- .../org/tasks/location/PlaceSearchMapbox.kt | 30 ++++++---- .../java/org/tasks/sync/microsoft/Error.kt | 7 ++- .../MicrosoftListSettingsActivityViewModel.kt | 9 +-- .../org/tasks/sync/microsoft/TaskLists.kt | 2 + deps_fdroid.txt | 1 - deps_googleplay.txt | 1 - gradle/libs.versions.toml | 2 - 13 files changed, 108 insertions(+), 105 deletions(-) delete mode 100644 app/src/main/java/org/tasks/extensions/JsonObject.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 681726cd6..0e2dea45a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -219,7 +219,6 @@ dependencies { implementation(libs.kotlinx.serialization) implementation(libs.okhttp) implementation(libs.persistent.cookiejar) - implementation(libs.gson) implementation(libs.material) implementation(libs.androidx.compose.material3) implementation(libs.androidx.constraintlayout) diff --git a/app/src/googleplay/java/org/tasks/billing/Purchase.kt b/app/src/googleplay/java/org/tasks/billing/Purchase.kt index 7b5995c1c..44bde11ad 100644 --- a/app/src/googleplay/java/org/tasks/billing/Purchase.kt +++ b/app/src/googleplay/java/org/tasks/billing/Purchase.kt @@ -1,16 +1,23 @@ package org.tasks.billing import com.android.billingclient.api.Purchase -import com.google.gson.GsonBuilder +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive import org.tasks.billing.BillingClientImpl.Companion.STATE_PURCHASED import java.util.regex.Pattern class Purchase(private val purchase: Purchase) { - constructor(json: String?) : this(GsonBuilder().create().fromJson(json, Purchase::class.java)) + constructor(json: String) : this( + Json.parseToJsonElement(json).jsonObject.let { + Purchase(it["zza"]!!.jsonPrimitive.content, it["zzb"]!!.jsonPrimitive.content) + } + ) fun toJson(): String { - return GsonBuilder().create().toJson(purchase) + return Json.encodeToString(mapOf("zza" to purchase.originalJson, "zzb" to purchase.signature)) } override fun toString(): String { diff --git a/app/src/main/java/org/tasks/extensions/JsonObject.kt b/app/src/main/java/org/tasks/extensions/JsonObject.kt deleted file mode 100644 index cf4d7d410..000000000 --- a/app/src/main/java/org/tasks/extensions/JsonObject.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.tasks.extensions - -import com.google.gson.JsonObject - -object JsonObject { - fun JsonObject.getStringOrNull(key: String): String? = getOrNull(key)?.asString - - fun JsonObject.getOrNull(key: String) = if (has(key)) get(key) else null -} diff --git a/app/src/main/java/org/tasks/location/GeocoderMapbox.kt b/app/src/main/java/org/tasks/location/GeocoderMapbox.kt index 5a078126d..4fe9c1cc4 100644 --- a/app/src/main/java/org/tasks/location/GeocoderMapbox.kt +++ b/app/src/main/java/org/tasks/location/GeocoderMapbox.kt @@ -1,18 +1,20 @@ package org.tasks.location import android.content.Context -import com.google.gson.JsonElement -import com.google.gson.JsonObject -import com.google.gson.JsonParser import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.double +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive import okhttp3.OkHttpClient import okhttp3.Request import org.tasks.DebugNetworkInterceptor import org.tasks.R import org.tasks.data.entity.Place -import org.tasks.extensions.JsonObject.getStringOrNull import org.tasks.preferences.Preferences import java.io.IOException import javax.inject.Inject @@ -42,33 +44,35 @@ class GeocoderMapbox @Inject constructor( companion object { internal fun jsonToPlace(json: String): Place? = - JsonParser - .parseString(json).asJsonObject.getAsJsonArray("features") - .takeIf { it.size() > 0 }?.get(0)?.asJsonObject - ?.let { toPlace(it) } + Json.parseToJsonElement(json) + .jsonObject["features"] + ?.jsonArray + ?.firstOrNull() + ?.toPlace() - internal fun toPlace(feature: JsonObject): Place { - val text = feature.get("text").asString - val coords = feature.get("center").asCoordinates + internal fun JsonElement.toPlace(): Place { + val text = jsonObject["text"]!!.jsonPrimitive.content + val coords = jsonObject["center"]!!.asCoordinates return Place( - name = if (feature.get("place_type").asStringList.contains("address")) { - feature - .getStringOrNull("address") + name = if (jsonObject["place_type"]!!.asStringList.contains("address")) { + jsonObject["address"] + ?.jsonPrimitive + ?.content ?.let { "$it $text" } ?: text } else { text }, - address = feature.get("place_name").asString, + address = jsonObject["place_name"]!!.jsonPrimitive.content, longitude = coords.first, latitude = coords.second, ) } private val JsonElement.asStringList: List - get() = asJsonArray.map { it.asString } + get() = jsonArray.map { it.jsonPrimitive.content } - private val JsonElement.asCoordinates: Pair - get() = asJsonArray.let { Pair(it[0].asDouble, it[1].asDouble) } + val JsonElement.asCoordinates: Pair + get() = jsonArray.let { Pair(it[0].jsonPrimitive.double, it[1].jsonPrimitive.double) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/tasks/location/GeocoderNominatim.kt b/app/src/main/java/org/tasks/location/GeocoderNominatim.kt index e386427c9..f53010321 100644 --- a/app/src/main/java/org/tasks/location/GeocoderNominatim.kt +++ b/app/src/main/java/org/tasks/location/GeocoderNominatim.kt @@ -1,19 +1,20 @@ package org.tasks.location import android.content.Context -import com.google.gson.JsonElement -import com.google.gson.JsonParser import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive import okhttp3.Request import org.tasks.BuildConfig import org.tasks.R import org.tasks.data.entity.Place -import org.tasks.extensions.JsonObject.getOrNull -import org.tasks.extensions.JsonObject.getStringOrNull import org.tasks.http.HttpClientFactory import org.tasks.http.HttpException +import org.tasks.location.GeocoderMapbox.Companion.asCoordinates import javax.inject.Inject class GeocoderNominatim @Inject constructor( @@ -41,26 +42,24 @@ class GeocoderNominatim @Inject constructor( private const val UA_VALUE = "${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}-${BuildConfig.BUILD_TYPE})" internal fun jsonToPlace(json: String): Place? = - JsonParser - .parseString(json).asJsonObject.getAsJsonArray("features") - .takeIf { it.size() > 0 }?.get(0)?.asJsonObject - ?.let { feature -> + Json.parseToJsonElement(json) + .jsonObject["features"] + ?.jsonArray + ?.firstOrNull() + ?.let { feature -> val geocoding = feature - .get("properties").asJsonObject - .get("geocoding").asJsonObject - val geometry = feature.get("geometry").asJsonObject - val coords = geometry.get("coordinates").asCoordinates + .jsonObject["properties"]!! + .jsonObject["geocoding"]!! + val geometry = feature.jsonObject["geometry"]!! + val coords = geometry.jsonObject["coordinates"]!!.asCoordinates return Place( - name = geocoding.getStringOrNull("name") - ?: geocoding.getStringOrNull("housenumber") - ?.let { "$it ${geocoding.get("street").asString}" }, - address = geocoding.getOrNull("label")?.asString, + name = geocoding.jsonObject["name"]?.jsonPrimitive?.content + ?: geocoding.jsonObject["housenumber"]?.jsonPrimitive?.content + ?.let { "$it ${geocoding.jsonObject["street"]?.jsonPrimitive?.content}" }, + address = geocoding.jsonObject["label"]?.jsonPrimitive?.content, longitude = coords.first, latitude = coords.second, ) } - - private val JsonElement.asCoordinates: Pair - get() = asJsonArray.let { Pair(it[0].asDouble, it[1].asDouble) } } } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/location/PlaceSearchGoogle.kt b/app/src/main/java/org/tasks/location/PlaceSearchGoogle.kt index baef8700e..e25d0583c 100644 --- a/app/src/main/java/org/tasks/location/PlaceSearchGoogle.kt +++ b/app/src/main/java/org/tasks/location/PlaceSearchGoogle.kt @@ -2,15 +2,19 @@ package org.tasks.location import android.content.Context import android.os.Bundle -import com.google.gson.JsonObject -import com.google.gson.JsonParser import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.double +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive import okhttp3.Request import org.tasks.R -import org.tasks.data.entity.CaldavAccount.Companion.TYPE_TASKS import org.tasks.data.dao.CaldavDao +import org.tasks.data.entity.CaldavAccount.Companion.TYPE_TASKS import org.tasks.data.entity.Place import org.tasks.http.HttpClientFactory import org.tasks.http.HttpException @@ -93,52 +97,45 @@ class PlaceSearchGoogle @Inject constructor( "international_phone_number" ).joinToString(",") - internal fun String.toJson(): JsonObject = JsonParser.parseString(this).asJsonObject + internal fun String.toJson(): JsonObject = Json.parseToJsonElement(this).jsonObject private fun checkResult(json: JsonObject) { - val status = json.get("status").asString + val status = json["status"]?.jsonPrimitive?.content when { status == "OK" -> return - json.has("error_message") -> - throw IllegalStateException(json.get("error_message").asString) - else -> - throw IllegalStateException(status) + else -> throw IllegalStateException( + json["error_message"]?.jsonPrimitive?.content ?: status + ) } } internal fun toSearchResults(json: JsonObject): List = - json.get("predictions") - .asJsonArray - .map { it.asJsonObject } - .filter { it.has("place_id") } + json["predictions"]!! + .jsonArray + .map { it.jsonObject } + .filter { it.contains("place_id") } .map { toSearchEntry(it) } private fun toSearchEntry(json: JsonObject): PlaceSearchResult { - val place = json.get("structured_formatting").asJsonObject + val place = json["structured_formatting"]!!.jsonObject return PlaceSearchResult( - json.get("place_id").asString, - place.get("main_text").asString, - place.get("secondary_text").asString + json["place_id"]!!.jsonPrimitive.content, + place["main_text"]!!.jsonPrimitive.content, + place["secondary_text"]!!.jsonPrimitive.content, ) } internal fun toPlace(json: JsonObject): Place { - val result = json.get("result").asJsonObject - val location = result.get("geometry").asJsonObject.get("location").asJsonObject + val result = json["result"]!!.jsonObject + val location = result["geometry"]!!.jsonObject["location"]!!.jsonObject return Place( - name = result.get("name").asString, - address = result.getString("formatted_address"), - phone = result.getString("international_phone_number"), - url = result.getString("website"), - latitude = location.get("lat").asDouble, - longitude = location.get("lng").asDouble, + name = result["name"]!!.jsonPrimitive.content, + address = result["formatted_address"]?.jsonPrimitive?.content, + phone = result["international_phone_number"]?.jsonPrimitive?.content, + url = result["website"]?.jsonPrimitive?.content, + latitude = location["lat"]!!.jsonPrimitive.double, + longitude = location["lng"]!!.jsonPrimitive.double, ) } - - private fun JsonObject.getString(field: String): String? = if (has(field)) { - get(field).asString - } else { - null - } } } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/location/PlaceSearchMapbox.kt b/app/src/main/java/org/tasks/location/PlaceSearchMapbox.kt index 13f1158af..823f5eff1 100644 --- a/app/src/main/java/org/tasks/location/PlaceSearchMapbox.kt +++ b/app/src/main/java/org/tasks/location/PlaceSearchMapbox.kt @@ -2,10 +2,13 @@ package org.tasks.location import android.content.Context import android.os.Bundle -import com.google.gson.JsonParser import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive import okhttp3.OkHttpClient import okhttp3.Request import org.tasks.DebugNetworkInterceptor @@ -53,17 +56,18 @@ class PlaceSearchMapbox @Inject constructor( companion object { internal fun jsonToSearchResults(json: String): List = - JsonParser - .parseString(json).asJsonObject.getAsJsonArray("features") - .map { it.asJsonObject } - .map { - val place = toPlace(it) - PlaceSearchResult( - it.get("id").asString, - place.name, - place.displayAddress, - place - ) - } + Json.parseToJsonElement(json) + .jsonObject["features"]!! + .jsonArray + .map { it.jsonObject } + .map { + val place = it.toPlace() + PlaceSearchResult( + it["id"]!!.jsonPrimitive.content, + place.name, + place.displayAddress, + place + ) + } } } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/sync/microsoft/Error.kt b/app/src/main/java/org/tasks/sync/microsoft/Error.kt index 78c34cdba..da53f0638 100644 --- a/app/src/main/java/org/tasks/sync/microsoft/Error.kt +++ b/app/src/main/java/org/tasks/sync/microsoft/Error.kt @@ -1,11 +1,14 @@ package org.tasks.sync.microsoft -import com.google.gson.Gson +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json import retrofit2.Response +@Serializable data class Error( val error: ErrorBody, ) { + @Serializable data class ErrorBody( val code: String, val message: String, @@ -13,6 +16,6 @@ data class Error( companion object { fun Response.toMicrosoftError() - = errorBody()?.string()?.let { Gson().fromJson(it, Error::class.java) } + = errorBody()?.string()?.let { Json.decodeFromString(it) } } } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/sync/microsoft/MicrosoftListSettingsActivityViewModel.kt b/app/src/main/java/org/tasks/sync/microsoft/MicrosoftListSettingsActivityViewModel.kt index 521b2f28d..fd00bcf1d 100644 --- a/app/src/main/java/org/tasks/sync/microsoft/MicrosoftListSettingsActivityViewModel.kt +++ b/app/src/main/java/org/tasks/sync/microsoft/MicrosoftListSettingsActivityViewModel.kt @@ -2,17 +2,18 @@ package org.tasks.sync.microsoft import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel -import com.google.gson.Gson import com.todoroo.astrid.service.TaskDeleter import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import okhttp3.RequestBody.Companion.toRequestBody import org.tasks.caldav.BaseCaldavCalendarSettingsActivity +import org.tasks.data.dao.CaldavDao import org.tasks.data.entity.CaldavAccount import org.tasks.data.entity.CaldavCalendar -import org.tasks.data.dao.CaldavDao import org.tasks.http.HttpClientFactory import org.tasks.http.HttpClientFactory.Companion.MEDIA_TYPE_JSON import retrofit2.HttpException @@ -46,7 +47,7 @@ class MicrosoftListSettingsActivityViewModel @Inject constructor( _viewState.update { it.copy(requestInFlight = true) } val microsoftService = httpClientFactory.getMicrosoftService(account) val taskList = TaskLists.TaskList(displayName = displayName) - val body = Gson().toJson(taskList).toRequestBody(MEDIA_TYPE_JSON) + val body = Json.encodeToString(taskList).toRequestBody(MEDIA_TYPE_JSON) val result = microsoftService.createList(body) if (result.isSuccessful) { val list = CaldavCalendar( @@ -77,7 +78,7 @@ class MicrosoftListSettingsActivityViewModel @Inject constructor( _viewState.update { it.copy(requestInFlight = true) } val microsoftService = httpClientFactory.getMicrosoftService(account) val taskList = TaskLists.TaskList(displayName = displayName) - val body = Gson().toJson(taskList).toRequestBody(MEDIA_TYPE_JSON) + val body = Json.encodeToString(taskList).toRequestBody(MEDIA_TYPE_JSON) val result = microsoftService.updateList(list?.uuid!!, body) if (result.isSuccessful) { result.body()?.applyTo(list) diff --git a/app/src/main/java/org/tasks/sync/microsoft/TaskLists.kt b/app/src/main/java/org/tasks/sync/microsoft/TaskLists.kt index 90bef36da..bbf6b7f53 100644 --- a/app/src/main/java/org/tasks/sync/microsoft/TaskLists.kt +++ b/app/src/main/java/org/tasks/sync/microsoft/TaskLists.kt @@ -1,6 +1,7 @@ package org.tasks.sync.microsoft import com.squareup.moshi.Json +import kotlinx.serialization.Serializable import org.tasks.data.entity.CaldavCalendar data class TaskLists( @@ -8,6 +9,7 @@ data class TaskLists( val value: List, @field:Json(name = "@odata.nextLink") val nextPage: String?, ) { + @Serializable data class TaskList( @Json(name = "@odata.etag") val etag: String? = null, val displayName: String? = null, diff --git a/deps_fdroid.txt b/deps_fdroid.txt index 877ae25ef..c111f9c65 100644 --- a/deps_fdroid.txt +++ b/deps_fdroid.txt @@ -619,7 +619,6 @@ ++--- com.squareup.okhttp3:okhttp:4.12.0 (*) ++--- com.github.franmontiel:PersistentCookieJar:1.0.1 +| \--- com.squareup.okhttp3:okhttp:3.1.2 -> 4.12.0 (*) -++--- com.google.code.gson:gson:2.10.1 ++--- com.google.android.material:material:1.12.0 (*) ++--- androidx.compose.material3:material3 -> 1.2.1 +| \--- androidx.compose.material3:material3-android:1.2.1 diff --git a/deps_googleplay.txt b/deps_googleplay.txt index 2618214a5..fd3c1c420 100644 --- a/deps_googleplay.txt +++ b/deps_googleplay.txt @@ -848,7 +848,6 @@ ++--- com.squareup.okhttp3:okhttp:4.12.0 (*) ++--- com.github.franmontiel:PersistentCookieJar:1.0.1 +| \--- com.squareup.okhttp3:okhttp:3.1.2 -> 4.12.0 (*) -++--- com.google.code.gson:gson:2.10.1 ++--- com.google.android.material:material:1.12.0 (*) ++--- androidx.compose.material3:material3 -> 1.2.1 +| \--- androidx.compose.material3:material3-android:1.2.1 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 117f8f254..e61dc4cde 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,7 +24,6 @@ google-api-drive = "v3-rev20240327-2.0.0" google-api-tasks = "v1-rev20240423-2.0.0" google-services = "4.4.1" gradle = "8.4.0" -gson = "2.10.1" hilt = "1.2.0" ical4android = "12fe73a" jchronic = "0.2.6" @@ -123,7 +122,6 @@ google-api-drive = { module = "com.google.apis:google-api-services-drive", versi google-api-tasks = { module = "com.google.apis:google-api-services-tasks", version.ref = "google-api-tasks" } google-services = { module = "com.google.gms:google-services", version.ref = "google-services" } gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" } -gson = { module = "com.google.code.gson:gson", version.ref = "gson" } jchronic = { module = "com.rubiconproject.oss:jchronic", version.ref = "jchronic" } junit = { module = "junit:junit", version.ref = "junit-junit" } kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }