From 959883714a0182c9036c248f465435a7dc5957a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Santos?= Date: Mon, 5 Aug 2024 15:43:24 +0100 Subject: [PATCH] Finish task events --- composeApp/build.gradle.kts | 2 +- .../kotlin/org/ooni/engine/Engine.kt | 44 +---- .../org/ooni/engine/NetworkTypeFinder.kt | 7 + .../kotlin/org/ooni/engine/TaskEventMapper.kt | 122 +++++++++++++ .../ooni/engine/models/MeasurementResult.kt | 32 ++++ .../org/ooni/engine/models/NetworkType.kt | 44 +++-- .../org/ooni/engine/models/TaskEvent.kt | 52 +++++- .../{EventResult.kt => TaskEventResult.kt} | 2 +- .../models/serializers/InstantSerializer.kt | 52 ++++++ .../kotlin/org/ooni/probe/di/Dependencies.kt | 24 ++- .../org/ooni/engine/TaskEventMapperTest.kt | 170 ++++++++++++++++++ gradle/libs.versions.toml | 15 +- 12 files changed, 498 insertions(+), 68 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/org/ooni/engine/NetworkTypeFinder.kt create mode 100644 composeApp/src/commonMain/kotlin/org/ooni/engine/TaskEventMapper.kt create mode 100644 composeApp/src/commonMain/kotlin/org/ooni/engine/models/MeasurementResult.kt rename composeApp/src/commonMain/kotlin/org/ooni/engine/models/{EventResult.kt => TaskEventResult.kt} (98%) create mode 100644 composeApp/src/commonMain/kotlin/org/ooni/engine/models/serializers/InstantSerializer.kt create mode 100644 composeApp/src/commonTest/kotlin/org/ooni/engine/TaskEventMapperTest.kt diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 5c03ce11..fa0dbfe9 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -77,7 +77,7 @@ kotlin { implementation(compose.ui) implementation(compose.components.resources) implementation(compose.components.uiToolingPreview) - implementation(libs.kotlin.serialization) + implementation(libs.bundles.kotlin) implementation(libs.bundles.ui) implementation(libs.bundles.tooling) diff --git a/composeApp/src/commonMain/kotlin/org/ooni/engine/Engine.kt b/composeApp/src/commonMain/kotlin/org/ooni/engine/Engine.kt index 2f02c138..c4e1844b 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/engine/Engine.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/engine/Engine.kt @@ -9,16 +9,16 @@ import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.withContext import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -import org.ooni.engine.models.EventResult import org.ooni.engine.models.TaskEvent +import org.ooni.engine.models.TaskEventResult import org.ooni.engine.models.TaskSettings -import kotlin.math.roundToInt class Engine( private val bridge: OonimkallBridge, private val json: Json, private val baseFilePath: String, private val cacheDir: String, + private val taskEventMapper: TaskEventMapper, ) { fun startTask(taskSettings: TaskSettings): Flow = channelFlow { @@ -42,7 +42,7 @@ class Engine( json.encodeToString( checkinResults?.urls?.map { it.url }?.let { finalSettings.copy( - inputs = it, + inputs = it.take(1), options = finalSettings.options.copy( maxRuntime = 90, @@ -54,8 +54,8 @@ class Engine( while (!task.isDone()) { val eventJson = task.waitForNextEvent() - val eventResult = json.decodeFromString(eventJson) - eventResult.toTaskEvent()?.let { send(it) } + val taskEventResult = json.decodeFromString(eventJson) + taskEventMapper(taskEventResult)?.let { send(it) } } invokeOnClose { @@ -121,38 +121,4 @@ class Engine( ).body } } - - private fun EventResult.toTaskEvent(): TaskEvent? = - when (key) { - "status.started" -> TaskEvent.Started - - "status.end" -> TaskEvent.StatusEnd - - "status.progress" -> - value?.percentage?.let { percentageValue -> - TaskEvent.Progress( - percentage = (percentageValue * 100.0).roundToInt(), - message = value?.message, - ) - } - - "log" -> - value?.message?.let { message -> - TaskEvent.Log( - level = value?.logLevel, - message = message, - ) - } - - "status.report_create" -> - value?.reportId?.let { - TaskEvent.ReportCreate(reportId = it) - } - - "task_terminated" -> TaskEvent.TaskTerminated - - "failure.startup" -> TaskEvent.FailureStartup(message = value?.failure) - - else -> null - } } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/engine/NetworkTypeFinder.kt b/composeApp/src/commonMain/kotlin/org/ooni/engine/NetworkTypeFinder.kt new file mode 100644 index 00000000..9edfe9a8 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/ooni/engine/NetworkTypeFinder.kt @@ -0,0 +1,7 @@ +package org.ooni.engine + +import org.ooni.engine.models.NetworkType + +fun interface NetworkTypeFinder { + operator fun invoke(): NetworkType +} diff --git a/composeApp/src/commonMain/kotlin/org/ooni/engine/TaskEventMapper.kt b/composeApp/src/commonMain/kotlin/org/ooni/engine/TaskEventMapper.kt new file mode 100644 index 00000000..fd24693d --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/ooni/engine/TaskEventMapper.kt @@ -0,0 +1,122 @@ +package org.ooni.engine + +import co.touchlab.kermit.Logger +import kotlinx.serialization.json.Json +import org.ooni.engine.models.TaskEvent +import org.ooni.engine.models.TaskEventResult +import kotlin.math.roundToInt + +class TaskEventMapper( + private val networkTypeFinder: NetworkTypeFinder, + private val json: Json, +) { + operator fun invoke(result: TaskEventResult): TaskEvent? { + val key = result.key + val value = result.value + + return when (key) { + "bug.json_dump" -> + value?.let { + TaskEvent.BugJsonDump(value = value) + } ?: run { + Logger.d("Task Event $key missing 'value'") + null + } + + "failure.measurement_submission" -> + TaskEvent.MeasurementSubmissionFailure( + index = value?.idx ?: 0, + message = value?.failure, + ) + + "failure.resolver_lookup" -> TaskEvent.ResolverLookupFailure(message = value?.failure) + + "failure.startup" -> TaskEvent.StartupFailure(message = value?.failure) + + "log" -> + value?.message?.let { message -> + TaskEvent.Log( + level = value.logLevel, + message = message, + ) + } ?: run { + Logger.d("Task Event $key missing 'message'") + null + } + + "measurement" -> + value?.jsonStr?.let { jsonString -> + TaskEvent.Measurement( + index = value.idx, + json = jsonString, + result = + try { + json.decodeFromString(jsonString) + } catch (e: Exception) { + Logger.d("Could not deserialize $key 'jsonStr'", throwable = e) + null + }, + ) + } ?: run { + Logger.d("Task Event $key missing 'jsonStr'") + null + } + + "status.end" -> TaskEvent.End + + "status.geoip_lookup" -> + TaskEvent.GeoIpLookup( + networkName = value?.probeNetworkName, + asn = value?.probeAsn, + ip = value?.probeIp, + countryCode = value?.probeCc, + networkType = networkTypeFinder(), + ) + + "status.measurement_done" -> + TaskEvent.MeasurementDone(index = value?.idx ?: 0) + + "status.measurement_start" -> + value?.input?.ifEmpty { null }?.let { url -> + TaskEvent.MeasurementStart( + index = value.idx, + url = url, + ) + } ?: run { + Logger.d("Task Event $key missing 'input'") + null + } + + "status.measurement_submission" -> + TaskEvent.MeasurementSubmissionSuccessful(index = value?.idx ?: 0) + + "status.progress" -> + value?.percentage?.let { percentageValue -> + TaskEvent.Progress( + percentage = (percentageValue * 100.0).roundToInt(), + message = value.message, + ) + } ?: run { + Logger.d("Task Event $key missing 'percentage'") + null + } + + "status.report_create" -> + value?.reportId?.let { + TaskEvent.ReportCreate(reportId = it) + } ?: run { + Logger.d("Task Event $key missing 'reportId'") + null + } + + "status.started" -> TaskEvent.Started + + "task_terminated" -> TaskEvent.TaskTerminated + + else -> { + Logger.d("Task Event $key ignored") + null + } + } + } +} diff --git a/composeApp/src/commonMain/kotlin/org/ooni/engine/models/MeasurementResult.kt b/composeApp/src/commonMain/kotlin/org/ooni/engine/models/MeasurementResult.kt new file mode 100644 index 00000000..6d325bb2 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/ooni/engine/models/MeasurementResult.kt @@ -0,0 +1,32 @@ +package org.ooni.engine.models + +import kotlinx.datetime.Instant +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import org.ooni.engine.models.serializers.InstantSerializer + +@Serializable +data class MeasurementResult( + @SerialName("probe_asn") + val probeAsn: String? = null, + @SerialName("probe_cc") + val probeCountryCode: String? = null, + @SerialName("test_start_time") + @Serializable(with = InstantSerializer::class) + val testStartTime: Instant? = null, + @SerialName("measurement_start_time") + @Serializable(with = InstantSerializer::class) + val measurementStartTime: Instant? = null, + @SerialName("test_runtime") + val testRuntime: Double? = null, + @SerialName("probe_ip") + val probeIp: String? = null, + @SerialName("report_id") + val reportId: String? = null, + @SerialName("input") + val input: String? = null, + /* + Field `test_keys` is ignored because we're not planning on storing the measurement results + as structured data. + */ +) diff --git a/composeApp/src/commonMain/kotlin/org/ooni/engine/models/NetworkType.kt b/composeApp/src/commonMain/kotlin/org/ooni/engine/models/NetworkType.kt index 511a9b61..2fdc45d1 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/engine/models/NetworkType.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/engine/models/NetworkType.kt @@ -8,16 +8,41 @@ import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder @Serializable(with = NetworkTypeSerializer::class) -enum class NetworkType(val value: String) { - VPN("vpn"), - Wifi("wifi"), - Mobile("mobile"), - NoInternet("no_internet"), +sealed interface NetworkType { + val value: String + + data object VPN : NetworkType { + override val value = "vpn" + } + + data object Wifi : NetworkType { + override val value = "wifi" + } + + data object Mobile : NetworkType { + override val value = "mobile" + } + + data object NoInternet : NetworkType { + override val value = "no_internet" + } + + data class Unknown(override val value: String) : NetworkType + + companion object { + fun fromValue(value: String) = + when (value) { + VPN.value -> VPN + Wifi.value -> Wifi + Mobile.value -> Mobile + NoInternet.value -> NoInternet + else -> Unknown(value) + } + } } object NetworkTypeSerializer : KSerializer { - override val descriptor = - PrimitiveSerialDescriptor("NetworkType", PrimitiveKind.STRING) + override val descriptor = PrimitiveSerialDescriptor("NetworkType", PrimitiveKind.STRING) override fun serialize( encoder: Encoder, @@ -26,8 +51,5 @@ object NetworkTypeSerializer : KSerializer { encoder.encodeString(value.value) } - override fun deserialize(decoder: Decoder): NetworkType { - val string = decoder.decodeString() - return NetworkType.entries.firstOrNull { it.value == string } ?: NetworkType.NoInternet - } + override fun deserialize(decoder: Decoder): NetworkType = NetworkType.fromValue(decoder.decodeString()) } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TaskEvent.kt b/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TaskEvent.kt index 04afca96..9522e824 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TaskEvent.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TaskEvent.kt @@ -1,15 +1,47 @@ package org.ooni.engine.models sealed interface TaskEvent { + data class BugJsonDump( + val value: TaskEventResult.Value, + ) : TaskEvent + + data object End : TaskEvent + + data class GeoIpLookup( + val networkName: String?, + val ip: String?, + val asn: String?, + val countryCode: String?, + val networkType: NetworkType, + ) : TaskEvent + data class Log( val level: String?, val message: String, ) : TaskEvent - data object Started : TaskEvent + data class Measurement( + val index: Int, + val json: String, + val result: MeasurementResult?, + ) : TaskEvent - data class ReportCreate( - val reportId: String, + data class MeasurementDone( + val index: Int, + ) : TaskEvent + + data class MeasurementStart( + val index: Int, + val url: String, + ) : TaskEvent + + data class MeasurementSubmissionSuccessful( + val index: Int, + ) : TaskEvent + + data class MeasurementSubmissionFailure( + val index: Int, + val message: String?, ) : TaskEvent data class Progress( @@ -17,11 +49,19 @@ sealed interface TaskEvent { val message: String?, ) : TaskEvent - data object StatusEnd : TaskEvent + data class ReportCreate( + val reportId: String, + ) : TaskEvent - data object TaskTerminated : TaskEvent + data class ResolverLookupFailure( + val message: String?, + ) : TaskEvent + + data object Started : TaskEvent - data class FailureStartup( + data class StartupFailure( val message: String?, ) : TaskEvent + + data object TaskTerminated : TaskEvent } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/engine/models/EventResult.kt b/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TaskEventResult.kt similarity index 98% rename from composeApp/src/commonMain/kotlin/org/ooni/engine/models/EventResult.kt rename to composeApp/src/commonMain/kotlin/org/ooni/engine/models/TaskEventResult.kt index cf4ca726..c1436545 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/engine/models/EventResult.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TaskEventResult.kt @@ -4,7 +4,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -class EventResult { +class TaskEventResult { @SerialName("key") var key: String? = null diff --git a/composeApp/src/commonMain/kotlin/org/ooni/engine/models/serializers/InstantSerializer.kt b/composeApp/src/commonMain/kotlin/org/ooni/engine/models/serializers/InstantSerializer.kt new file mode 100644 index 00000000..f3837245 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/ooni/engine/models/serializers/InstantSerializer.kt @@ -0,0 +1,52 @@ +package org.ooni.engine.models.serializers + +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalTime +import kotlinx.datetime.format +import kotlinx.datetime.format.DateTimeComponents +import kotlinx.datetime.format.DateTimeFormat +import kotlinx.datetime.format.char +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +object InstantSerializer : KSerializer { + private val format: DateTimeFormat = + DateTimeComponents.Format { + date( + LocalDate.Format { + year() + char('-') + monthNumber() + char('-') + dayOfMonth() + }, + ) + char(' ') + time( + LocalTime.Format { + hour() + char(':') + minute() + char(':') + second() + }, + ) + } + + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("kotlinx.datetime.Instant", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): Instant = Instant.parse(decoder.decodeString(), format = format) + + override fun serialize( + encoder: Encoder, + value: Instant, + ) { + encoder.encodeString(value.format(format = format)) + } +} diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt index de3531f7..ddabea7a 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt @@ -1,8 +1,12 @@ package org.ooni.probe.di +import androidx.annotation.VisibleForTesting import kotlinx.serialization.json.Json import org.ooni.engine.Engine +import org.ooni.engine.NetworkTypeFinder import org.ooni.engine.OonimkallBridge +import org.ooni.engine.TaskEventMapper +import org.ooni.engine.models.NetworkType import org.ooni.probe.data.models.TestResult import org.ooni.probe.shared.PlatformInfo import org.ooni.probe.ui.dashboard.DashboardViewModel @@ -17,16 +21,13 @@ class Dependencies( ) { // Data - private val json by lazy { - Json { - encodeDefaults = true - ignoreUnknownKeys = true - } - } + private val json by lazy { buildJson() } // Engine - private val engine by lazy { Engine(oonimkallBridge, json, baseFileDir, cacheDir) } + private val networkTypeFinder by lazy { NetworkTypeFinder { NetworkType.Unknown("") } } // TODO + private val taskEventMapper by lazy { TaskEventMapper(networkTypeFinder, json) } + private val engine by lazy { Engine(oonimkallBridge, json, baseFileDir, cacheDir, taskEventMapper) } // ViewModels @@ -38,4 +39,13 @@ class Dependencies( resultId: TestResult.Id, onBack: () -> Unit, ) = ResultViewModel(resultId, onBack) + + companion object { + @VisibleForTesting + fun buildJson() = + Json { + encodeDefaults = true + ignoreUnknownKeys = true + } + } } diff --git a/composeApp/src/commonTest/kotlin/org/ooni/engine/TaskEventMapperTest.kt b/composeApp/src/commonTest/kotlin/org/ooni/engine/TaskEventMapperTest.kt new file mode 100644 index 00000000..815fb86e --- /dev/null +++ b/composeApp/src/commonTest/kotlin/org/ooni/engine/TaskEventMapperTest.kt @@ -0,0 +1,170 @@ +package org.ooni.engine + +import kotlinx.datetime.Month +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import org.ooni.engine.models.NetworkType +import org.ooni.engine.models.TaskEvent +import org.ooni.engine.models.TaskEventResult +import org.ooni.probe.di.Dependencies +import kotlin.test.Test +import kotlin.test.assertEquals + +class TaskEventMapperTest { + private val json = Dependencies.buildJson() + private val subject = + TaskEventMapper( + json = json, + networkTypeFinder = { NetworkType.NoInternet }, + ) + + @Test + fun started() { + val result = + json.decodeFromString( + """{"key":"status.started","value":{}}""", + ) + + val event = subject(result)!! + + assertEquals(TaskEvent.Started::class, event::class) + } + + @Test + fun log() { + val result = + json.decodeFromString( + """{"key":"log","value":{"log_level":"INFO","message":"Looking up OONI backends... please, be patient"}}""", + ) + + val event = subject(result)!! + + assertEquals(TaskEvent.Log::class, event::class) + with(event as TaskEvent.Log) { + assertEquals("INFO", level) + assertEquals("Looking up OONI backends... please, be patient", message) + } + } + + @Test + fun progress() { + val result = + json.decodeFromString( + """{"key":"status.progress","value":{"message":"contacted bouncer","percentage":0.1}}""", + ) + + val event = subject(result)!! + + assertEquals(TaskEvent.Progress::class, event::class) + with(event as TaskEvent.Progress) { + assertEquals(10, percentage) + assertEquals("contacted bouncer", message) + } + } + + @Test + fun geoIpLookup() { + val result = + json.decodeFromString( + @Suppress("ktlint:standard:max-line-length") + """{"key":"status.geoip_lookup","value":{"probe_asn":"AS12345","probe_cc":"PT","probe_ip":"1.2.3.4","probe_network_name":"Vodafone"}}""", + ) + + val event = subject(result)!! + + assertEquals(TaskEvent.GeoIpLookup::class, event::class) + with(event as TaskEvent.GeoIpLookup) { + assertEquals("AS12345", asn) + assertEquals("PT", countryCode) + assertEquals("1.2.3.4", ip) + assertEquals("Vodafone", networkName) + assertEquals(NetworkType.NoInternet, networkType) + } + } + + @Test + fun measurementStart() { + val result = + json.decodeFromString( + """{"key":"status.measurement_start","value":{"idx":0,"input":"https://www.reddit.com/"}}""", + ) + + val event = subject(result)!! + + assertEquals(TaskEvent.MeasurementStart::class, event::class) + with(event as TaskEvent.MeasurementStart) { + assertEquals(0, index) + assertEquals("https://www.reddit.com/", url) + } + } + + @Test + fun measurement() { + val result = + json.decodeFromString( + @Suppress("ktlint:standard:max-line-length") + """{"key":"measurement","value":{"idx":99,"input":"https://www.reddit.com/","json_str":"{\"annotations\":{\"architecture\":\"arm64\",\"engine_name\":\"ooniprobe-engine\",\"engine_version\":\"3.22.0\",\"flavor\":\"ooniprobe\",\"go_version\":\"go1.21.10\",\"network_type\":\"wifi\",\"origin\":\"ooni-run\",\"platform\":\"android\",\"vcs_modified\":\"\",\"vcs_revision\":\"\",\"vcs_time\":\"\",\"vcs_tool\":\"\"},\"data_format_version\":\"0.2.0\",\"input\":\"https://www.reddit.com/\",\"measurement_start_time\":\"2024-08-05 13:22:31\",\"probe_asn\":\"AS12345\",\"probe_cc\":\"PT\",\"probe_ip\":\"127.0.0.1\",\"probe_network_name\":\"Vodafone\",\"report_id\":\"\",\"resolver_asn\":\"AS12345\",\"resolver_ip\":\"1.2.3.4\",\"resolver_network_name\":\"Vodafone\",\"software_name\":\"ooniprobe\",\"software_version\":\"1.0\"}"}}""", + ) + + val event = subject(result)!! + + assertEquals(TaskEvent.Measurement::class, event::class) + with(event as TaskEvent.Measurement) { + assertEquals(99, index) + with(event.result!!) { + assertEquals("127.0.0.1", probeIp) + assertEquals("AS12345", probeAsn) + assertEquals("PT", probeCountryCode) + assertEquals("", reportId) + } + with(event.result?.measurementStartTime?.toLocalDateTime(TimeZone.UTC)!!) { // 2024-08-05 13:22:31 + assertEquals(2024, year) + assertEquals(Month.AUGUST, month) + assertEquals(5, dayOfMonth) + assertEquals(13, hour) + assertEquals(22, minute) + assertEquals(31, second) + } + assertEquals("https://www.reddit.com/", event.result?.input) + } + } + + @Test + fun measurementDone() { + val result = + json.decodeFromString( + """{"key":"status.measurement_done","value":{"idx":3,"input":"https://www.reddit.com/"}}""", + ) + + val event = subject(result)!! + + assertEquals(TaskEvent.MeasurementDone::class, event::class) + with(event as TaskEvent.MeasurementDone) { + assertEquals(3, index) + } + } + + @Test + fun end() { + val result = + json.decodeFromString( + """{"key":"status.end","value":{"downloaded_kb":692.8134765625,"failure":"","uploaded_kb":4.994140625}}""", + ) + + val event = subject(result)!! + + assertEquals(TaskEvent.End::class, event::class) + } + + @Test + fun taskTerminated() { + val result = + json.decodeFromString( + """{"key":"task_terminated","value":{}}""", + ) + + val event = subject(result)!! + + assertEquals(TaskEvent.TaskTerminated::class, event::class) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1b69c83c..c0479a30 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,4 +1,5 @@ [versions] + agp = "8.3.2" # Max compatible version https://kotlinlang.org/docs/multiplatform-compatibility-guide.html#version-compatibility android-compileSdk = "34" @@ -9,6 +10,7 @@ compose-plugin = "1.6.11" kotlin = "2.0.0" [plugins] + androidApplication = { id = "com.android.application", version.ref = "agp" } androidLibrary = { id = "com.android.library", version.ref = "agp" } jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" } @@ -19,6 +21,11 @@ cocoapods = { id = "org.jetbrains.kotlin.native.cocoapods", version.ref = "kotli ktlint = { id = "org.jlleitschuh.gradle.ktlint", version = "12.1.1" } [libraries] + +# Kotlin +kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.7.1" } +kotlin-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version = "0.6.0" } + # UI android-activity = { module = "androidx.activity:activity-ktx", version = "1.9.1" } lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version = "2.8.0" } @@ -27,13 +34,15 @@ navigation = { module = "org.jetbrains.androidx.navigation:navigation-compose", # Engine android-oonimkall = { module = "org.ooni:oonimkall", version = "2024.05.22-092559" } -# Serialization -kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.7.1" } - # Logging kermit = { module = "co.touchlab:kermit", version = "2.0.4" } [bundles] + +kotlin = [ + "kotlin-serialization", + "kotlin-datetime", +] ui = [ "lifecycle-viewmodel-compose", "navigation",