Replace gson with kotlin serialization

pull/2883/head
Alex Baker 1 month ago
parent d556863fda
commit ebe5e5c009

@ -219,7 +219,6 @@ dependencies {
implementation(libs.kotlinx.serialization) implementation(libs.kotlinx.serialization)
implementation(libs.okhttp) implementation(libs.okhttp)
implementation(libs.persistent.cookiejar) implementation(libs.persistent.cookiejar)
implementation(libs.gson)
implementation(libs.material) implementation(libs.material)
implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material3)
implementation(libs.androidx.constraintlayout) implementation(libs.androidx.constraintlayout)

@ -1,16 +1,23 @@
package org.tasks.billing package org.tasks.billing
import com.android.billingclient.api.Purchase 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 org.tasks.billing.BillingClientImpl.Companion.STATE_PURCHASED
import java.util.regex.Pattern import java.util.regex.Pattern
class Purchase(private val purchase: Purchase) { class Purchase(private val purchase: Purchase) {
constructor(json: String?) : this(GsonBuilder().create().fromJson<Purchase>(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 { fun toJson(): String {
return GsonBuilder().create().toJson(purchase) return Json.encodeToString(mapOf("zza" to purchase.originalJson, "zzb" to purchase.signature))
} }
override fun toString(): String { override fun toString(): String {

@ -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
}

@ -1,18 +1,20 @@
package org.tasks.location package org.tasks.location
import android.content.Context 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 dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext 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.OkHttpClient
import okhttp3.Request import okhttp3.Request
import org.tasks.DebugNetworkInterceptor import org.tasks.DebugNetworkInterceptor
import org.tasks.R import org.tasks.R
import org.tasks.data.entity.Place import org.tasks.data.entity.Place
import org.tasks.extensions.JsonObject.getStringOrNull
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import java.io.IOException import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
@ -42,33 +44,35 @@ class GeocoderMapbox @Inject constructor(
companion object { companion object {
internal fun jsonToPlace(json: String): Place? = internal fun jsonToPlace(json: String): Place? =
JsonParser Json.parseToJsonElement(json)
.parseString(json).asJsonObject.getAsJsonArray("features") .jsonObject["features"]
.takeIf { it.size() > 0 }?.get(0)?.asJsonObject ?.jsonArray
?.let { toPlace(it) } ?.firstOrNull()
?.toPlace()
internal fun toPlace(feature: JsonObject): Place { internal fun JsonElement.toPlace(): Place {
val text = feature.get("text").asString val text = jsonObject["text"]!!.jsonPrimitive.content
val coords = feature.get("center").asCoordinates val coords = jsonObject["center"]!!.asCoordinates
return Place( return Place(
name = if (feature.get("place_type").asStringList.contains("address")) { name = if (jsonObject["place_type"]!!.asStringList.contains("address")) {
feature jsonObject["address"]
.getStringOrNull("address") ?.jsonPrimitive
?.content
?.let { "$it $text" } ?.let { "$it $text" }
?: text ?: text
} else { } else {
text text
}, },
address = feature.get("place_name").asString, address = jsonObject["place_name"]!!.jsonPrimitive.content,
longitude = coords.first, longitude = coords.first,
latitude = coords.second, latitude = coords.second,
) )
} }
private val JsonElement.asStringList: List<String> private val JsonElement.asStringList: List<String>
get() = asJsonArray.map { it.asString } get() = jsonArray.map { it.jsonPrimitive.content }
private val JsonElement.asCoordinates: Pair<Double, Double> val JsonElement.asCoordinates: Pair<Double, Double>
get() = asJsonArray.let { Pair(it[0].asDouble, it[1].asDouble) } get() = jsonArray.let { Pair(it[0].jsonPrimitive.double, it[1].jsonPrimitive.double) }
} }
} }

@ -1,19 +1,20 @@
package org.tasks.location package org.tasks.location
import android.content.Context import android.content.Context
import com.google.gson.JsonElement
import com.google.gson.JsonParser
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext 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 okhttp3.Request
import org.tasks.BuildConfig import org.tasks.BuildConfig
import org.tasks.R import org.tasks.R
import org.tasks.data.entity.Place 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.HttpClientFactory
import org.tasks.http.HttpException import org.tasks.http.HttpException
import org.tasks.location.GeocoderMapbox.Companion.asCoordinates
import javax.inject.Inject import javax.inject.Inject
class GeocoderNominatim @Inject constructor( 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})" private const val UA_VALUE = "${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}-${BuildConfig.BUILD_TYPE})"
internal fun jsonToPlace(json: String): Place? = internal fun jsonToPlace(json: String): Place? =
JsonParser Json.parseToJsonElement(json)
.parseString(json).asJsonObject.getAsJsonArray("features") .jsonObject["features"]
.takeIf { it.size() > 0 }?.get(0)?.asJsonObject ?.jsonArray
?.let { feature -> ?.firstOrNull()
?.let { feature ->
val geocoding = feature val geocoding = feature
.get("properties").asJsonObject .jsonObject["properties"]!!
.get("geocoding").asJsonObject .jsonObject["geocoding"]!!
val geometry = feature.get("geometry").asJsonObject val geometry = feature.jsonObject["geometry"]!!
val coords = geometry.get("coordinates").asCoordinates val coords = geometry.jsonObject["coordinates"]!!.asCoordinates
return Place( return Place(
name = geocoding.getStringOrNull("name") name = geocoding.jsonObject["name"]?.jsonPrimitive?.content
?: geocoding.getStringOrNull("housenumber") ?: geocoding.jsonObject["housenumber"]?.jsonPrimitive?.content
?.let { "$it ${geocoding.get("street").asString}" }, ?.let { "$it ${geocoding.jsonObject["street"]?.jsonPrimitive?.content}" },
address = geocoding.getOrNull("label")?.asString, address = geocoding.jsonObject["label"]?.jsonPrimitive?.content,
longitude = coords.first, longitude = coords.first,
latitude = coords.second, latitude = coords.second,
) )
} }
private val JsonElement.asCoordinates: Pair<Double, Double>
get() = asJsonArray.let { Pair(it[0].asDouble, it[1].asDouble) }
} }
} }

@ -2,15 +2,19 @@ package org.tasks.location
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext 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 okhttp3.Request
import org.tasks.R import org.tasks.R
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_TASKS
import org.tasks.data.dao.CaldavDao import org.tasks.data.dao.CaldavDao
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_TASKS
import org.tasks.data.entity.Place import org.tasks.data.entity.Place
import org.tasks.http.HttpClientFactory import org.tasks.http.HttpClientFactory
import org.tasks.http.HttpException import org.tasks.http.HttpException
@ -93,52 +97,45 @@ class PlaceSearchGoogle @Inject constructor(
"international_phone_number" "international_phone_number"
).joinToString(",") ).joinToString(",")
internal fun String.toJson(): JsonObject = JsonParser.parseString(this).asJsonObject internal fun String.toJson(): JsonObject = Json.parseToJsonElement(this).jsonObject
private fun checkResult(json: JsonObject) { private fun checkResult(json: JsonObject) {
val status = json.get("status").asString val status = json["status"]?.jsonPrimitive?.content
when { when {
status == "OK" -> return status == "OK" -> return
json.has("error_message") -> else -> throw IllegalStateException(
throw IllegalStateException(json.get("error_message").asString) json["error_message"]?.jsonPrimitive?.content ?: status
else -> )
throw IllegalStateException(status)
} }
} }
internal fun toSearchResults(json: JsonObject): List<PlaceSearchResult> = internal fun toSearchResults(json: JsonObject): List<PlaceSearchResult> =
json.get("predictions") json["predictions"]!!
.asJsonArray .jsonArray
.map { it.asJsonObject } .map { it.jsonObject }
.filter { it.has("place_id") } .filter { it.contains("place_id") }
.map { toSearchEntry(it) } .map { toSearchEntry(it) }
private fun toSearchEntry(json: JsonObject): PlaceSearchResult { private fun toSearchEntry(json: JsonObject): PlaceSearchResult {
val place = json.get("structured_formatting").asJsonObject val place = json["structured_formatting"]!!.jsonObject
return PlaceSearchResult( return PlaceSearchResult(
json.get("place_id").asString, json["place_id"]!!.jsonPrimitive.content,
place.get("main_text").asString, place["main_text"]!!.jsonPrimitive.content,
place.get("secondary_text").asString place["secondary_text"]!!.jsonPrimitive.content,
) )
} }
internal fun toPlace(json: JsonObject): Place { internal fun toPlace(json: JsonObject): Place {
val result = json.get("result").asJsonObject val result = json["result"]!!.jsonObject
val location = result.get("geometry").asJsonObject.get("location").asJsonObject val location = result["geometry"]!!.jsonObject["location"]!!.jsonObject
return Place( return Place(
name = result.get("name").asString, name = result["name"]!!.jsonPrimitive.content,
address = result.getString("formatted_address"), address = result["formatted_address"]?.jsonPrimitive?.content,
phone = result.getString("international_phone_number"), phone = result["international_phone_number"]?.jsonPrimitive?.content,
url = result.getString("website"), url = result["website"]?.jsonPrimitive?.content,
latitude = location.get("lat").asDouble, latitude = location["lat"]!!.jsonPrimitive.double,
longitude = location.get("lng").asDouble, longitude = location["lng"]!!.jsonPrimitive.double,
) )
} }
private fun JsonObject.getString(field: String): String? = if (has(field)) {
get(field).asString
} else {
null
}
} }
} }

@ -2,10 +2,13 @@ package org.tasks.location
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import com.google.gson.JsonParser
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext 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.OkHttpClient
import okhttp3.Request import okhttp3.Request
import org.tasks.DebugNetworkInterceptor import org.tasks.DebugNetworkInterceptor
@ -53,17 +56,18 @@ class PlaceSearchMapbox @Inject constructor(
companion object { companion object {
internal fun jsonToSearchResults(json: String): List<PlaceSearchResult> = internal fun jsonToSearchResults(json: String): List<PlaceSearchResult> =
JsonParser Json.parseToJsonElement(json)
.parseString(json).asJsonObject.getAsJsonArray("features") .jsonObject["features"]!!
.map { it.asJsonObject } .jsonArray
.map { .map { it.jsonObject }
val place = toPlace(it) .map {
PlaceSearchResult( val place = it.toPlace()
it.get("id").asString, PlaceSearchResult(
place.name, it["id"]!!.jsonPrimitive.content,
place.displayAddress, place.name,
place place.displayAddress,
) place
} )
}
} }
} }

@ -1,11 +1,14 @@
package org.tasks.sync.microsoft package org.tasks.sync.microsoft
import com.google.gson.Gson import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import retrofit2.Response import retrofit2.Response
@Serializable
data class Error( data class Error(
val error: ErrorBody, val error: ErrorBody,
) { ) {
@Serializable
data class ErrorBody( data class ErrorBody(
val code: String, val code: String,
val message: String, val message: String,
@ -13,6 +16,6 @@ data class Error(
companion object { companion object {
fun <T> Response<T>.toMicrosoftError() fun <T> Response<T>.toMicrosoftError()
= errorBody()?.string()?.let { Gson().fromJson(it, Error::class.java) } = errorBody()?.string()?.let { Json.decodeFromString<Error>(it) }
} }
} }

@ -2,17 +2,18 @@ package org.tasks.sync.microsoft
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.google.gson.Gson
import com.todoroo.astrid.service.TaskDeleter import com.todoroo.astrid.service.TaskDeleter
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import org.tasks.caldav.BaseCaldavCalendarSettingsActivity import org.tasks.caldav.BaseCaldavCalendarSettingsActivity
import org.tasks.data.dao.CaldavDao
import org.tasks.data.entity.CaldavAccount import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.dao.CaldavDao
import org.tasks.http.HttpClientFactory import org.tasks.http.HttpClientFactory
import org.tasks.http.HttpClientFactory.Companion.MEDIA_TYPE_JSON import org.tasks.http.HttpClientFactory.Companion.MEDIA_TYPE_JSON
import retrofit2.HttpException import retrofit2.HttpException
@ -46,7 +47,7 @@ class MicrosoftListSettingsActivityViewModel @Inject constructor(
_viewState.update { it.copy(requestInFlight = true) } _viewState.update { it.copy(requestInFlight = true) }
val microsoftService = httpClientFactory.getMicrosoftService(account) val microsoftService = httpClientFactory.getMicrosoftService(account)
val taskList = TaskLists.TaskList(displayName = displayName) 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) val result = microsoftService.createList(body)
if (result.isSuccessful) { if (result.isSuccessful) {
val list = CaldavCalendar( val list = CaldavCalendar(
@ -77,7 +78,7 @@ class MicrosoftListSettingsActivityViewModel @Inject constructor(
_viewState.update { it.copy(requestInFlight = true) } _viewState.update { it.copy(requestInFlight = true) }
val microsoftService = httpClientFactory.getMicrosoftService(account) val microsoftService = httpClientFactory.getMicrosoftService(account)
val taskList = TaskLists.TaskList(displayName = displayName) 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) val result = microsoftService.updateList(list?.uuid!!, body)
if (result.isSuccessful) { if (result.isSuccessful) {
result.body()?.applyTo(list) result.body()?.applyTo(list)

@ -1,6 +1,7 @@
package org.tasks.sync.microsoft package org.tasks.sync.microsoft
import com.squareup.moshi.Json import com.squareup.moshi.Json
import kotlinx.serialization.Serializable
import org.tasks.data.entity.CaldavCalendar import org.tasks.data.entity.CaldavCalendar
data class TaskLists( data class TaskLists(
@ -8,6 +9,7 @@ data class TaskLists(
val value: List<TaskList>, val value: List<TaskList>,
@field:Json(name = "@odata.nextLink") val nextPage: String?, @field:Json(name = "@odata.nextLink") val nextPage: String?,
) { ) {
@Serializable
data class TaskList( data class TaskList(
@Json(name = "@odata.etag") val etag: String? = null, @Json(name = "@odata.etag") val etag: String? = null,
val displayName: String? = null, val displayName: String? = null,

@ -619,7 +619,6 @@
++--- com.squareup.okhttp3:okhttp:4.12.0 (*) ++--- com.squareup.okhttp3:okhttp:4.12.0 (*)
++--- com.github.franmontiel:PersistentCookieJar:1.0.1 ++--- com.github.franmontiel:PersistentCookieJar:1.0.1
+| \--- com.squareup.okhttp3:okhttp:3.1.2 -> 4.12.0 (*) +| \--- 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 (*) ++--- com.google.android.material:material:1.12.0 (*)
++--- androidx.compose.material3:material3 -> 1.2.1 ++--- androidx.compose.material3:material3 -> 1.2.1
+| \--- androidx.compose.material3:material3-android:1.2.1 +| \--- androidx.compose.material3:material3-android:1.2.1

@ -848,7 +848,6 @@
++--- com.squareup.okhttp3:okhttp:4.12.0 (*) ++--- com.squareup.okhttp3:okhttp:4.12.0 (*)
++--- com.github.franmontiel:PersistentCookieJar:1.0.1 ++--- com.github.franmontiel:PersistentCookieJar:1.0.1
+| \--- com.squareup.okhttp3:okhttp:3.1.2 -> 4.12.0 (*) +| \--- 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 (*) ++--- com.google.android.material:material:1.12.0 (*)
++--- androidx.compose.material3:material3 -> 1.2.1 ++--- androidx.compose.material3:material3 -> 1.2.1
+| \--- androidx.compose.material3:material3-android:1.2.1 +| \--- androidx.compose.material3:material3-android:1.2.1

@ -24,7 +24,6 @@ google-api-drive = "v3-rev20240327-2.0.0"
google-api-tasks = "v1-rev20240423-2.0.0" google-api-tasks = "v1-rev20240423-2.0.0"
google-services = "4.4.1" google-services = "4.4.1"
gradle = "8.4.0" gradle = "8.4.0"
gson = "2.10.1"
hilt = "1.2.0" hilt = "1.2.0"
ical4android = "12fe73a" ical4android = "12fe73a"
jchronic = "0.2.6" 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-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" } google-services = { module = "com.google.gms:google-services", version.ref = "google-services" }
gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" } 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" } jchronic = { module = "com.rubiconproject.oss:jchronic", version.ref = "jchronic" }
junit = { module = "junit:junit", version.ref = "junit-junit" } junit = { module = "junit:junit", version.ref = "junit-junit" }
kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }

Loading…
Cancel
Save