diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_world.xml b/composeApp/src/commonMain/composeResources/drawable/ic_world.xml new file mode 100644 index 00000000..1b7a62d6 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_world.xml @@ -0,0 +1,10 @@ + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/video_quality.xml b/composeApp/src/commonMain/composeResources/drawable/video_quality.xml new file mode 100644 index 00000000..c20f164d --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/video_quality.xml @@ -0,0 +1,12 @@ + + + diff --git a/composeApp/src/commonMain/composeResources/values/strings-common.xml b/composeApp/src/commonMain/composeResources/values/strings-common.xml index 2fc62fec..0046955b 100644 --- a/composeApp/src/commonMain/composeResources/values/strings-common.xml +++ b/composeApp/src/commonMain/composeResources/values/strings-common.xml @@ -46,6 +46,18 @@ Tests Networks Data Usage + %1$s blocked + %1$s blocked + %1$s tested + %1$s tested + %1$s blocked + %1$s blocked + %1$s accessible + %1$s accessible + %1$s blocked + %1$s blocked + %1$s available + %1$s available Date & Time Network Country @@ -58,6 +70,12 @@ N/A Upload Download + + + Gbit/s + Mbit/s + kbit/s + Re-run test You are about to re-test %1$s websites. Run @@ -302,13 +320,13 @@ Unsupported URL Measurement - - %1$d measurement - %1$d measurements - Failed OK Anomaly + %1$d measured + %1$d measured + %1$d failed + %1$d failed All Types All Sources diff --git a/composeApp/src/commonMain/composeResources/values/untranslatable.xml b/composeApp/src/commonMain/composeResources/values/untranslatable.xml new file mode 100644 index 00000000..6e5157b1 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/values/untranslatable.xml @@ -0,0 +1,74 @@ + + bugs@openobservatory.org + [bug-report] OONI Probe %1$s + %1$s: %2$s + + + @string/Common_Minutes_One + @string/Common_Minutes_Other + + + @string/Common_Hour_One + @string/Common_Hour_Other + + + + @string/Dashboard_RunTests_RunButton_Label_One + @string/Dashboard_RunTests_RunButton_Label_Other + + + + @string/TestResults_Overview_Websites_Blocked_Singular + @string/TestResults_Overview_Websites_Blocked_Plural + + + + @string/TestResults_Overview_Websites_Tested_Singular + @string/TestResults_Overview_Websites_Tested_Plural + + + + @string/TestResults_Overview_InstantMessaging_Blocked_Singular + @string/TestResults_Overview_InstantMessaging_Blocked_Plural + + + + @string/TestResults_Overview_InstantMessaging_Available_Singular + @string/TestResults_Overview_InstantMessaging_Available_Plural + + + + @string/TestResults_Overview_Circumvention_Blocked_Singular + @string/TestResults_Overview_Circumvention_Blocked_Plural + + + + @string/TestResults_Overview_Circumvention_Available_Singular + @string/TestResults_Overview_Circumvention_Available_Plural + + + + @string/Measurements_Count_One + @string/Measurements_Count_Other + + + + @string/Measurements_Failed_One + @string/Measurements_Failed_Other + + + 240p + 360p + 480p + 720p + 720p (HD) + 1080p + 1080p (full HD) + 1440p + 1440p (2k) + 2160p + 2160p (4k) + + %1$s %2$s + + diff --git a/composeApp/src/commonMain/composeResources/values/untraslatable.xml b/composeApp/src/commonMain/composeResources/values/untraslatable.xml deleted file mode 100644 index 6914822c..00000000 --- a/composeApp/src/commonMain/composeResources/values/untraslatable.xml +++ /dev/null @@ -1,25 +0,0 @@ - - bugs@openobservatory.org - [bug-report] OONI Probe %1$s - %1$s: %2$s - - - @string/Common_Minutes_One - @string/Common_Minutes_Other - - - @string/Common_Hour_One - @string/Common_Hour_Other - - - - @string/Dashboard_RunTests_RunButton_Label_One - @string/Dashboard_RunTests_RunButton_Label_Other - - - - @string/Measurements_Count_One - @string/Measurements_Count_Other - - - diff --git a/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TestGroup.kt b/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TestGroup.kt new file mode 100644 index 00000000..390c1cdd --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TestGroup.kt @@ -0,0 +1,52 @@ +package org.ooni.engine.models + +import kotlin.reflect.KClass + +sealed class TestGroup( + vararg tests: KClass, +) { + val tests: List> = tests.toList() + + data object Websites : TestGroup(TestType.WebConnectivity::class) + + data object InstantMessaging : TestGroup( + TestType.Whatsapp::class, + TestType.Telegram::class, + TestType.FacebookMessenger::class, + TestType.Signal::class, + ) + + data object Circumvention : TestGroup( + TestType.Psiphon::class, + TestType.Tor::class, + ) + + data object Performance : TestGroup( + TestType.Ndt::class, + TestType.Dash::class, + TestType.HttpHeaderFieldManipulation::class, + TestType.HttpInvalidRequestLine::class, + ) + + data object Experimental : TestGroup(TestType.Experimental::class) + + data object Unknown : TestGroup() + + companion object { + fun fromTests(tests: List): TestGroup { + if (tests.isEmpty()) return Unknown + + return listOf( + Websites, + InstantMessaging, + Circumvention, + Performance, + Experimental, + ) + .firstOrNull { group -> + tests.all { test -> group.tests.any { it.isInstance(test) } } + } + ?: Unknown + } + } +} diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/MeasurementCounts.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/MeasurementCounts.kt new file mode 100644 index 00000000..2e69402a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/MeasurementCounts.kt @@ -0,0 +1,10 @@ +package org.ooni.probe.data.models + +data class MeasurementCounts( + val done: Long, + val failed: Long, + val anomaly: Long, +) { + val success get() = done - failed - anomaly + val tested get() = done - failed +} diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/ResultListItem.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/ResultListItem.kt index c9e8d759..1118d405 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/ResultListItem.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/ResultListItem.kt @@ -1,12 +1,43 @@ package org.ooni.probe.data.models +import androidx.compose.runtime.Composable +import ooniprobe.composeapp.generated.resources.Res +import ooniprobe.composeapp.generated.resources.twoParam +import org.jetbrains.compose.resources.stringResource +import org.ooni.engine.models.TestType + data class ResultListItem( val result: ResultModel, val descriptor: Descriptor, val network: NetworkModel?, - val measurementsCount: Long, + val measurementCounts: MeasurementCounts, val allMeasurementsUploaded: Boolean, val anyMeasurementUploadFailed: Boolean, + val testKeys: List?, ) { - val idOrThrow get() = result.idOrThrow + val idOrThrow + get() = result.idOrThrow + + val videoQuality + get() = testKeys?.firstOrNull { TestType.Dash.name == it.testName }?.testKeys?.getVideoQuality(extended = false) + + val upload + @Composable + get() = testKeys?.firstOrNull { TestType.Ndt.name == it.testName }?.testKeys?.let { testKey -> + return@let testKey.summary?.upload?.let { + val upload = setFractionalDigits(getScaledValue(it)) + val unit = getUnit(it) + stringResource(Res.string.twoParam, upload, stringResource(unit)) + } + } + + val download + @Composable + get() = testKeys?.firstOrNull { TestType.Ndt.name == it.testName }?.testKeys?.let { testKey -> + return@let testKey.summary?.download?.let { + val download = setFractionalDigits(getScaledValue(it)) + val unit = getUnit(it) + stringResource(Res.string.twoParam, download, stringResource(unit)) + } + } } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/ResultWithNetworkAndAggregates.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/ResultWithNetworkAndAggregates.kt index 73ec52c7..1bb6b278 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/ResultWithNetworkAndAggregates.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/ResultWithNetworkAndAggregates.kt @@ -3,7 +3,7 @@ package org.ooni.probe.data.models data class ResultWithNetworkAndAggregates( val result: ResultModel, val network: NetworkModel?, - val measurementsCount: Long, + val measurementCounts: MeasurementCounts, val allMeasurementsUploaded: Boolean, val anyMeasurementUploadFailed: Boolean, ) diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/TestKeysWithResultId.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/TestKeysWithResultId.kt new file mode 100644 index 00000000..20fb0248 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/TestKeysWithResultId.kt @@ -0,0 +1,96 @@ +package org.ooni.probe.data.models + +import ooniprobe.composeapp.generated.resources.Res +import ooniprobe.composeapp.generated.resources.TestResults_Gbps +import ooniprobe.composeapp.generated.resources.TestResults_Kbps +import ooniprobe.composeapp.generated.resources.TestResults_Mbps +import ooniprobe.composeapp.generated.resources.TestResults_NotAvailable +import ooniprobe.composeapp.generated.resources.r1080p +import ooniprobe.composeapp.generated.resources.r1080p_ext +import ooniprobe.composeapp.generated.resources.r1440p +import ooniprobe.composeapp.generated.resources.r1440p_ext +import ooniprobe.composeapp.generated.resources.r2160p +import ooniprobe.composeapp.generated.resources.r2160p_ext +import ooniprobe.composeapp.generated.resources.r240p +import ooniprobe.composeapp.generated.resources.r360p +import ooniprobe.composeapp.generated.resources.r480p +import ooniprobe.composeapp.generated.resources.r720p +import ooniprobe.composeapp.generated.resources.r720p_ext +import org.jetbrains.compose.resources.StringResource +import org.ooni.engine.models.TestKeys +import org.ooni.probe.ui.shared.format + +data class TestKeysWithResultId( + val id: MeasurementModel.Id, + val testName: String?, + val testKeys: TestKeys?, + val resultId: ResultModel.Id, + val testGroupName: String?, + val descriptorRunId: InstalledTestDescriptorModel.Id?, +) + +fun TestKeys.getVideoQuality(extended: Boolean): StringResource { + return simple?.medianBitrate?.let { + return minimumBitrateForVideo(it, extended) + } ?: Res.string.TestResults_NotAvailable +} + +private fun minimumBitrateForVideo( + videoQuality: Double, + extended: Boolean, +): StringResource { + return if (videoQuality < 600) { + Res.string.r240p + } else if (videoQuality < 1000) { + Res.string.r360p + } else if (videoQuality < 2500) { + Res.string.r480p + } else if (videoQuality < 5000) { + if (extended) { + Res.string.r720p_ext + } else { + Res.string.r720p + } + } else if (videoQuality < 8000) { + if (extended) { + Res.string.r1080p_ext + } else { + Res.string.r1080p + } + } else if (videoQuality < 16000) { + if (extended) { + Res.string.r1440p_ext + } else { + Res.string.r1440p + } + } else if (extended) { + Res.string.r2160p_ext + } else { + Res.string.r2160p + } +} + +fun getScaledValue(value: Double): Double { + return if (value < 1000) { + value + } else if (value < 1000 * 1000) { + value / 1000 + } else { + value / 1000 * 1000 + } +} + +fun setFractionalDigits(value: Double): String { + return if (value < 10) value.format(1) else value.format(2) +} + +fun getUnit(value: Double): StringResource { + // We assume there is no Tbit/s (for now!) + return if (value < 1000) { + Res.string.TestResults_Kbps + } else if (value < 1000 * 1000) { + Res.string.TestResults_Mbps + } else { + Res.string.TestResults_Gbps + } +} diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/MeasurementRepository.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/MeasurementRepository.kt index 815144ab..fddc6d35 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/MeasurementRepository.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/MeasurementRepository.kt @@ -5,15 +5,20 @@ import app.cash.sqldelight.coroutines.mapToList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import org.ooni.engine.models.TestKeys import org.ooni.engine.models.TestType import org.ooni.probe.Database import org.ooni.probe.data.Measurement import org.ooni.probe.data.SelectByResultIdWithUrl +import org.ooni.probe.data.SelectTestKeysByResultId import org.ooni.probe.data.Url import org.ooni.probe.data.models.InstalledTestDescriptorModel import org.ooni.probe.data.models.MeasurementModel import org.ooni.probe.data.models.MeasurementWithUrl +import org.ooni.probe.data.models.ResultFilter import org.ooni.probe.data.models.ResultModel +import org.ooni.probe.data.models.TestKeysWithResultId import org.ooni.probe.data.models.UrlModel import org.ooni.probe.shared.toEpoch import org.ooni.probe.shared.toLocalDateTime @@ -21,6 +26,7 @@ import kotlin.coroutines.CoroutineContext class MeasurementRepository( private val database: Database, + private val json: Json, private val backgroundContext: CoroutineContext, ) { fun list(): Flow> = @@ -54,6 +60,17 @@ class MeasurementRepository( .mapToList(backgroundContext) .map { list -> list.mapNotNull { it.toModel() } } + fun selectTestKeysByResultId(filter: ResultFilter): Flow> { + val descriptorFilter = (filter.descriptor as? ResultFilter.Type.One)?.value + return database.measurementQueries + .selectTestKeysByResultId( + descriptorKey = descriptorFilter?.key, + ) + .asFlow() + .mapToList(backgroundContext) + .map { list -> list.mapNotNull { it.toModel() } } + } + suspend fun createOrUpdate(model: MeasurementModel): MeasurementModel.Id = withContext(backgroundContext) { database.transactionWithResult { @@ -142,4 +159,15 @@ class MeasurementRepository( }, ) } + + private fun SelectTestKeysByResultId.toModel(): TestKeysWithResultId? { + return TestKeysWithResultId( + id = MeasurementModel.Id(id), + resultId = result_id?.let(ResultModel::Id) ?: return null, + testName = test_name, + testKeys = test_keys?.let { json.decodeFromString(it) }, + testGroupName = test_group_name, + descriptorRunId = descriptor_runId?.let(InstalledTestDescriptorModel::Id), + ) + } } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/ResultRepository.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/ResultRepository.kt index 93bb2b5b..6c83620f 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/ResultRepository.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/ResultRepository.kt @@ -15,6 +15,7 @@ import org.ooni.probe.data.Result import org.ooni.probe.data.SelectAllWithNetwork import org.ooni.probe.data.SelectByIdWithNetwork import org.ooni.probe.data.models.InstalledTestDescriptorModel +import org.ooni.probe.data.models.MeasurementCounts import org.ooni.probe.data.models.NetworkModel import org.ooni.probe.data.models.ResultFilter import org.ooni.probe.data.models.ResultModel @@ -151,7 +152,7 @@ class ResultRepository( private fun SelectAllWithNetwork.toModel(): ResultWithNetworkAndAggregates? { return ResultWithNetworkAndAggregates( result = Result( - id = id, + id = id ?: return null, test_group_name = test_group_name, start_time = start_time, is_viewed = is_viewed, @@ -163,7 +164,7 @@ class ResultRepository( network_id = network_id, descriptor_runId = descriptor_runId, ).toModel() ?: return null, - network = id_?.let { networkId -> + network = network_id_inner?.let { networkId -> Network( id = networkId, network_name = network_name, @@ -173,7 +174,11 @@ class ResultRepository( network_type = network_type, ).toModel() }, - measurementsCount = measurementsCount, + measurementCounts = MeasurementCounts( + done = doneMeasurementsCount ?: 0, + failed = failedMeasurementsCount ?: 0, + anomaly = anomalyMeasurementsCount ?: 0, + ), allMeasurementsUploaded = allMeasurementsUploaded, anyMeasurementUploadFailed = anyMeasurementUploadFailed, ) 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 25185e9e..c06a62fe 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt @@ -127,7 +127,7 @@ class Dependencies( private val database by lazy { buildDatabase(databaseDriverFactory) } private val measurementRepository by lazy { - MeasurementRepository(database, backgroundContext) + MeasurementRepository(database, json, backgroundContext) } private val networkRepository by lazy { NetworkRepository(database, backgroundContext) } @@ -257,6 +257,7 @@ class Dependencies( GetResults( resultRepository::list, getTestDescriptors::invoke, + measurementRepository::selectTestKeysByResultId, ) } private val getResult by lazy { diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/EvaluateMeasurementKeys.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/EvaluateMeasurementKeys.kt index 50f088b1..e7d91232 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/EvaluateMeasurementKeys.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/EvaluateMeasurementKeys.kt @@ -92,3 +92,17 @@ fun evaluateMeasurementKeys( keys?.registrationServerStatus == TestKeys.BLOCKED_VALUE, ) } + +fun extractTestKeysPropertiesToJson(testKeys: TestKeys): Map> { + return mapOf( + "simple" to mapOf( + "median_bitrate" to testKeys.simple?.medianBitrate, + "upload" to testKeys.simple?.medianBitrate, + "download" to testKeys.simple?.medianBitrate, + ), + "summary" to mapOf( + "upload" to testKeys.summary?.upload, + "download" to testKeys.summary?.download, + ), + ) +} diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetResults.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetResults.kt index 0a3a283d..1b50a6b3 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetResults.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetResults.kt @@ -7,24 +7,28 @@ import org.ooni.probe.data.models.ResultFilter import org.ooni.probe.data.models.ResultListItem import org.ooni.probe.data.models.ResultModel import org.ooni.probe.data.models.ResultWithNetworkAndAggregates +import org.ooni.probe.data.models.TestKeysWithResultId class GetResults( private val getResults: (ResultFilter) -> Flow>, private val getDescriptors: () -> Flow>, + private val getTestKeys: (ResultFilter) -> Flow>, ) { operator fun invoke(filter: ResultFilter): Flow> = combine( getResults(filter), getDescriptors(), - ) { results, descriptors -> + getTestKeys(filter), + ) { results, descriptors, testKeys -> results.mapNotNull { item -> ResultListItem( result = item.result, descriptor = descriptors.forResult(item.result) ?: return@mapNotNull null, network = item.network, - measurementsCount = item.measurementsCount, + measurementCounts = item.measurementCounts, allMeasurementsUploaded = item.allMeasurementsUploaded, anyMeasurementUploadFailed = item.anyMeasurementUploadFailed, + testKeys = testKeys.forResult(item.result), ) } } @@ -38,3 +42,11 @@ fun List.forResult(result: ResultModel): Descriptor? = } } ?: firstOrNull { it.name == result.testGroupName } + +fun List.forResult(result: ResultModel): List? = + result.id + ?.let { resultId -> + filter { + it.resultId.value == resultId.value + } + } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/RunNetTest.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/RunNetTest.kt index 38fc49b0..36fffabc 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/RunNetTest.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/RunNetTest.kt @@ -165,6 +165,14 @@ class RunNetTest( ) } + event.result.testKeys?.let { + val testKeys = extractTestKeysPropertiesToJson(it) + Logger.d("testKeys: ${measurement.id}, $testKeys") + measurement = measurement.copy( + testKeys = json.encodeToString(testKeys), + ) + } + val evaluation = evaluateMeasurementKeys(spec.netTest.test, event.result.testKeys) measurement = measurement.copy( diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/shared/ResourceExt.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/shared/ResourceExt.kt index 6579c836..acf5a869 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/shared/ResourceExt.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/shared/ResourceExt.kt @@ -21,41 +21,43 @@ import ooniprobe.composeapp.generated.resources.Dashboard_RunTests_RunButton_Lab import ooniprobe.composeapp.generated.resources.Dashboard_RunTests_RunButton_Label_Other import ooniprobe.composeapp.generated.resources.Measurements_Count_One import ooniprobe.composeapp.generated.resources.Measurements_Count_Other +import ooniprobe.composeapp.generated.resources.Measurements_Failed_One +import ooniprobe.composeapp.generated.resources.Measurements_Failed_Other import ooniprobe.composeapp.generated.resources.Res +import ooniprobe.composeapp.generated.resources.TestResults_Overview_Circumvention_Available_Plural +import ooniprobe.composeapp.generated.resources.TestResults_Overview_Circumvention_Available_Singular +import ooniprobe.composeapp.generated.resources.TestResults_Overview_Circumvention_Blocked_Plural +import ooniprobe.composeapp.generated.resources.TestResults_Overview_Circumvention_Blocked_Singular +import ooniprobe.composeapp.generated.resources.TestResults_Overview_InstantMessaging_Available_Plural +import ooniprobe.composeapp.generated.resources.TestResults_Overview_InstantMessaging_Available_Singular +import ooniprobe.composeapp.generated.resources.TestResults_Overview_InstantMessaging_Blocked_Plural +import ooniprobe.composeapp.generated.resources.TestResults_Overview_InstantMessaging_Blocked_Singular +import ooniprobe.composeapp.generated.resources.TestResults_Overview_Websites_Blocked_Plural +import ooniprobe.composeapp.generated.resources.TestResults_Overview_Websites_Blocked_Singular +import ooniprobe.composeapp.generated.resources.TestResults_Overview_Websites_Tested_Plural +import ooniprobe.composeapp.generated.resources.TestResults_Overview_Websites_Tested_Singular import org.jetbrains.compose.resources.PluralStringResource import org.jetbrains.compose.resources.getPluralString import org.jetbrains.compose.resources.getString import org.jetbrains.compose.resources.pluralStringResource import org.jetbrains.compose.resources.stringResource -val stringMap = mapOf( - "@string/Common_Minutes_One" to Res.string.Common_Minutes_One, - "@string/Common_Minutes_Other" to Res.string.Common_Minutes_Other, - "@string/Common_Hour_One" to Res.string.Common_Hour_One, - "@string/Common_Hour_Other" to Res.string.Common_Hour_Other, - "@string/Dashboard_RunTests_RunButton_Label_One" to Res.string.Dashboard_RunTests_RunButton_Label_One, - "@string/Dashboard_RunTests_RunButton_Label_Other" to Res.string.Dashboard_RunTests_RunButton_Label_Other, - "@string/Measurements_Count_One" to Res.string.Measurements_Count_One, - "@string/Measurements_Count_Other" to Res.string.Measurements_Count_Other, -) - @Composable -fun stringMonthArrayResource(): List { - return listOf( - stringResource(Res.string.Common_Months_January), - stringResource(Res.string.Common_Months_February), - stringResource(Res.string.Common_Months_March), - stringResource(Res.string.Common_Months_April), - stringResource(Res.string.Common_Months_May), - stringResource(Res.string.Common_Months_June), - stringResource(Res.string.Common_Months_July), - stringResource(Res.string.Common_Months_August), - stringResource(Res.string.Common_Months_September), - stringResource(Res.string.Common_Months_October), - stringResource(Res.string.Common_Months_November), - stringResource(Res.string.Common_Months_December), - ) -} +fun stringMonthArrayResource(): List = + listOf( + Res.string.Common_Months_January, + Res.string.Common_Months_February, + Res.string.Common_Months_March, + Res.string.Common_Months_April, + Res.string.Common_Months_May, + Res.string.Common_Months_June, + Res.string.Common_Months_July, + Res.string.Common_Months_August, + Res.string.Common_Months_September, + Res.string.Common_Months_October, + Res.string.Common_Months_November, + Res.string.Common_Months_December, + ).map { stringResource(it) } @Composable fun pluralStringResourceItem( @@ -77,3 +79,50 @@ suspend fun getPluralStringResourceItem( return getString(it, *formatArgs) } ?: "" } + +private val stringMap = mapOf( + "@string/Common_Minutes_One" + to Res.string.Common_Minutes_One, + "@string/Common_Minutes_Other" + to Res.string.Common_Minutes_Other, + "@string/Common_Hour_One" + to Res.string.Common_Hour_One, + "@string/Common_Hour_Other" + to Res.string.Common_Hour_Other, + "@string/Dashboard_RunTests_RunButton_Label_One" + to Res.string.Dashboard_RunTests_RunButton_Label_One, + "@string/Dashboard_RunTests_RunButton_Label_Other" + to Res.string.Dashboard_RunTests_RunButton_Label_Other, + "@string/Measurements_Count_One" + to Res.string.Measurements_Count_One, + "@string/Measurements_Count_Other" + to Res.string.Measurements_Count_Other, + "@string/Measurements_Failed_One" + to Res.string.Measurements_Failed_One, + "@string/Measurements_Failed_Other" + to Res.string.Measurements_Failed_Other, + "@string/TestResults_Overview_Websites_Blocked_Singular" + to Res.string.TestResults_Overview_Websites_Blocked_Singular, + "@string/TestResults_Overview_Websites_Blocked_Plural" + to Res.string.TestResults_Overview_Websites_Blocked_Plural, + "@string/TestResults_Overview_Websites_Tested_Singular" + to Res.string.TestResults_Overview_Websites_Tested_Singular, + "@string/TestResults_Overview_Websites_Tested_Plural" + to Res.string.TestResults_Overview_Websites_Tested_Plural, + "@string/TestResults_Overview_InstantMessaging_Blocked_Singular" + to Res.string.TestResults_Overview_InstantMessaging_Blocked_Singular, + "@string/TestResults_Overview_InstantMessaging_Blocked_Plural" + to Res.string.TestResults_Overview_InstantMessaging_Blocked_Plural, + "@string/TestResults_Overview_InstantMessaging_Available_Singular" + to Res.string.TestResults_Overview_InstantMessaging_Available_Singular, + "@string/TestResults_Overview_InstantMessaging_Available_Plural" + to Res.string.TestResults_Overview_InstantMessaging_Available_Plural, + "@string/TestResults_Overview_Circumvention_Blocked_Singular" + to Res.string.TestResults_Overview_Circumvention_Blocked_Singular, + "@string/TestResults_Overview_Circumvention_Blocked_Plural" + to Res.string.TestResults_Overview_Circumvention_Blocked_Plural, + "@string/TestResults_Overview_Circumvention_Available_Singular" + to Res.string.TestResults_Overview_Circumvention_Available_Singular, + "@string/TestResults_Overview_Circumvention_Available_Plural" + to Res.string.TestResults_Overview_Circumvention_Available_Plural, +) diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/results/ResultCell.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/results/ResultCell.kt index 0ed90f10..d32dd783 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/results/ResultCell.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/results/ResultCell.kt @@ -16,6 +16,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import ooniprobe.composeapp.generated.resources.Measurements_Count @@ -24,22 +25,41 @@ import ooniprobe.composeapp.generated.resources.Res import ooniprobe.composeapp.generated.resources.Snackbar_ResultsNotUploaded_Text import ooniprobe.composeapp.generated.resources.TaskOrigin_AutoRun import ooniprobe.composeapp.generated.resources.TaskOrigin_Manual +import ooniprobe.composeapp.generated.resources.TestResults_Overview_Circumvention_Available +import ooniprobe.composeapp.generated.resources.TestResults_Overview_Circumvention_Blocked +import ooniprobe.composeapp.generated.resources.TestResults_Overview_InstantMessaging_Available +import ooniprobe.composeapp.generated.resources.TestResults_Overview_InstantMessaging_Blocked +import ooniprobe.composeapp.generated.resources.TestResults_Overview_Websites_Blocked +import ooniprobe.composeapp.generated.resources.TestResults_Overview_Websites_Tested +import ooniprobe.composeapp.generated.resources.TestResults_Summary_Performance_Hero_Download +import ooniprobe.composeapp.generated.resources.TestResults_Summary_Performance_Hero_Upload import ooniprobe.composeapp.generated.resources.TestResults_UnknownASN import ooniprobe.composeapp.generated.resources.ic_cloud_off +import ooniprobe.composeapp.generated.resources.ic_download +import ooniprobe.composeapp.generated.resources.ic_history +import ooniprobe.composeapp.generated.resources.ic_measurement_anomaly +import ooniprobe.composeapp.generated.resources.ic_measurement_failed +import ooniprobe.composeapp.generated.resources.ic_measurement_ok +import ooniprobe.composeapp.generated.resources.ic_upload +import ooniprobe.composeapp.generated.resources.ic_world +import ooniprobe.composeapp.generated.resources.video_quality +import org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import org.ooni.engine.models.TaskOrigin +import org.ooni.engine.models.TestGroup import org.ooni.probe.data.models.ResultListItem import org.ooni.probe.shared.pluralStringResourceItem import org.ooni.probe.ui.dashboard.TestDescriptorLabel import org.ooni.probe.ui.shared.relativeDateTime +import org.ooni.probe.ui.theme.LocalCustomColors @Composable fun ResultCell( item: ResultListItem, onResultClick: () -> Unit, ) { - val hasError = item.result.isDone && item.measurementsCount == 0L + val hasError = item.result.isDone && item.measurementCounts.done == 0L Surface( color = if (item.result.isViewed || hasError) { @@ -49,9 +69,7 @@ fun ResultCell( }, ) { Row( - verticalAlignment = Alignment.Bottom, - modifier = Modifier - .fillMaxWidth() + modifier = Modifier.fillMaxWidth() .run { if (!hasError) clickable { onResultClick() } else this } .padding(horizontal = 16.dp, vertical = 8.dp), ) { @@ -72,9 +90,9 @@ fun ResultCell( ) } - if (hasError) { + if (hasError && !item.result.failureMessage.isNullOrEmpty()) { Text( - item.result.failureMessage.orEmpty().lines().first(), + item.result.failureMessage.lines().first(), maxLines = 2, color = MaterialTheme.colorScheme.error, ) @@ -89,40 +107,21 @@ fun ResultCell( } Text( - item.result.startTime.relativeDateTime(), + item.result.startTime.relativeDateTime() + " – " + item.sourceText, style = MaterialTheme.typography.labelMedium, ) } Column( - horizontalAlignment = Alignment.End, - modifier = Modifier.weight(0.34f), + horizontalAlignment = Alignment.Start, + modifier = Modifier.padding(start = 8.dp, top = 24.dp).weight(0.35f), ) { if (!item.result.isDone) { CircularProgressIndicator( - modifier = Modifier.padding(bottom = 4.dp) - .size(24.dp), + modifier = Modifier.padding(bottom = 4.dp).size(24.dp), ) } - Text( - stringResource( - when (item.result.taskOrigin) { - TaskOrigin.AutoRun -> Res.string.TaskOrigin_AutoRun - TaskOrigin.OoniRun -> Res.string.TaskOrigin_Manual - }, - ), - style = MaterialTheme.typography.labelLarge, - modifier = Modifier.padding(bottom = 2.dp), - ) if (!hasError) { - Text( - pluralStringResourceItem( - Res.plurals.Measurements_Count, - item.measurementsCount.toInt(), - item.measurementsCount, - ), - style = MaterialTheme.typography.labelLarge, - modifier = Modifier.padding(bottom = 2.dp), - ) + ResultCounts(item) } if (!item.allMeasurementsUploaded) { Row( @@ -136,9 +135,7 @@ fun ResultCell( } else { LocalContentColor.current }, - modifier = Modifier - .size(16.dp) - .padding(end = 4.dp), + modifier = Modifier.size(20.dp).padding(end = 4.dp), ) Text( stringResource( @@ -156,3 +153,191 @@ fun ResultCell( } } } + +@Composable +private fun ResultCounts(item: ResultListItem) { + val testGroup = TestGroup.fromTests(item.descriptor.netTests.map { it.test }) + val counts = item.measurementCounts + + Column { + if (counts.failed > 0) { + ResultCountItem( + icon = Res.drawable.ic_measurement_failed, + text = pluralStringResourceItem( + Res.plurals.TestResults_Overview_Websites_Blocked, + counts.failed.toInt(), + counts.failed, + ), + color = MaterialTheme.colorScheme.error, + ) + } + + when (testGroup) { + TestGroup.Websites -> { + ResultCountItem( + icon = Res.drawable.ic_measurement_anomaly, + text = pluralStringResourceItem( + Res.plurals.TestResults_Overview_Websites_Blocked, + counts.anomaly.toInt(), + counts.anomaly, + ), + color = if (counts.anomaly > 0) { + LocalCustomColors.current.logWarn + } else { + LocalContentColor.current + }, + ) + ResultCountItem( + icon = Res.drawable.ic_world, + text = pluralStringResourceItem( + Res.plurals.TestResults_Overview_Websites_Tested, + counts.tested.toInt(), + counts.tested, + ), + ) + } + + TestGroup.InstantMessaging -> { + ResultCountItem( + icon = Res.drawable.ic_measurement_anomaly, + text = pluralStringResourceItem( + Res.plurals.TestResults_Overview_InstantMessaging_Blocked, + counts.anomaly.toInt(), + counts.anomaly, + ), + color = if (counts.anomaly > 0) { + LocalCustomColors.current.logWarn + } else { + LocalContentColor.current + }, + ) + ResultCountItem( + icon = Res.drawable.ic_measurement_ok, + text = pluralStringResourceItem( + Res.plurals.TestResults_Overview_InstantMessaging_Available, + counts.success.toInt(), + counts.success, + ), + ) + } + + TestGroup.Circumvention -> { + ResultCountItem( + icon = Res.drawable.ic_measurement_anomaly, + text = pluralStringResourceItem( + Res.plurals.TestResults_Overview_Circumvention_Blocked, + counts.anomaly.toInt(), + counts.anomaly, + ), + color = if (counts.anomaly > 0) { + LocalCustomColors.current.logWarn + } else { + LocalContentColor.current + }, + ) + ResultCountItem( + icon = Res.drawable.ic_measurement_ok, + text = pluralStringResourceItem( + Res.plurals.TestResults_Overview_Circumvention_Available, + counts.success.toInt(), + counts.success, + ), + ) + } + + TestGroup.Performance -> { + // TODO: Performance aggregated data: download, upload and video + + Column { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painterResource(Res.drawable.ic_download), + contentDescription = stringResource(Res.string.TestResults_Summary_Performance_Hero_Download), + modifier = Modifier.size(24.dp).padding(end = 4.dp), + ) + item.download?.let { + Text( + text = it, + ) + } + } + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painterResource(Res.drawable.ic_upload), + contentDescription = stringResource(Res.string.TestResults_Summary_Performance_Hero_Upload), + modifier = Modifier.size(24.dp).padding(end = 4.dp), + ) + item.upload?.let { + Text( + text = it, + ) + } + } + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painterResource(Res.drawable.video_quality), + contentDescription = stringResource(Res.string.TestResults_Summary_Performance_Hero_Upload), + modifier = Modifier.size(24.dp).padding(end = 4.dp), + ) + item.videoQuality?.let { + Text( + text = stringResource(it), + ) + } + } + } + } + + TestGroup.Experimental, + TestGroup.Unknown, + -> { + ResultCountItem( + icon = Res.drawable.ic_history, + text = pluralStringResourceItem( + Res.plurals.Measurements_Count, + counts.done.toInt(), + counts.done, + ), + ) + } + } + } +} + +@Composable +private fun ResultCountItem( + icon: DrawableResource, + text: String, + color: Color = LocalContentColor.current, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(bottom = 2.dp), + ) { + Icon( + painter = painterResource(icon), + tint = color.copy(alpha = 0.66f), + contentDescription = null, + modifier = Modifier.padding(end = 2.dp).size(16.dp), + ) + Text( + text = text, + style = MaterialTheme.typography.labelLarge, + color = color, + ) + } +} + +private val ResultListItem.sourceText + @Composable get() = stringResource( + when (result.taskOrigin) { + TaskOrigin.AutoRun -> Res.string.TaskOrigin_AutoRun + TaskOrigin.OoniRun -> Res.string.TaskOrigin_Manual + }, + ) diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/shared/DataUsageFormats.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/shared/DataUsageFormats.kt index 4a44fdb3..f2a62fd1 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/shared/DataUsageFormats.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/shared/DataUsageFormats.kt @@ -11,7 +11,7 @@ fun Long.formatDataUsage(): String { return (this / 1024.0.pow(digitGroups.toDouble())).format() + " " + units[digitGroups] } -private fun Double.format(decimalChars: Int = 2): String { +fun Double.format(decimalChars: Int = 2): String { val absoluteValue = abs(this).toInt() val decimalValue = abs((this - absoluteValue) * 10.0.pow(decimalChars)).toInt() return if (decimalValue == 0) absoluteValue.toString() else "$absoluteValue.$decimalValue" diff --git a/composeApp/src/commonMain/sqldelight/migrations/8.sqm b/composeApp/src/commonMain/sqldelight/migrations/8.sqm new file mode 100644 index 00000000..11df3978 --- /dev/null +++ b/composeApp/src/commonMain/sqldelight/migrations/8.sqm @@ -0,0 +1,2 @@ +CREATE INDEX idx_measure_is_failed ON Measurement (is_failed); +CREATE INDEX idx_measure_is_anomaly ON Measurement (is_anomaly); diff --git a/composeApp/src/commonMain/sqldelight/org/ooni/probe/data/Measurement.sq b/composeApp/src/commonMain/sqldelight/org/ooni/probe/data/Measurement.sq index 86e50388..641c810f 100644 --- a/composeApp/src/commonMain/sqldelight/org/ooni/probe/data/Measurement.sq +++ b/composeApp/src/commonMain/sqldelight/org/ooni/probe/data/Measurement.sq @@ -75,3 +75,15 @@ WHERE Measurement.result_id IN ( SELECT Result.id FROM Result WHERE Result.descriptor_runId = ? ); + +selectTestKeysByResultId: +SELECT + Measurement.id, + Measurement.test_name, + Measurement.result_id, + Measurement.test_keys, + Result.test_group_name, + Result.descriptor_runId +FROM Measurement +JOIN Result ON Measurement.result_id = Result.id +WHERE Result.test_group_name = :descriptorKey OR Result.descriptor_runId = :descriptorKey; diff --git a/composeApp/src/commonMain/sqldelight/org/ooni/probe/data/Result.sq b/composeApp/src/commonMain/sqldelight/org/ooni/probe/data/Result.sq index 44392151..4910f9f2 100644 --- a/composeApp/src/commonMain/sqldelight/org/ooni/probe/data/Result.sq +++ b/composeApp/src/commonMain/sqldelight/org/ooni/probe/data/Result.sq @@ -44,32 +44,52 @@ selectLastInsertedRowId: SELECT last_insert_rowid(); selectAllWithNetwork: -SELECT - Result.*, - Network.*, - ( - SELECT COUNT(Measurement.id) FROM Measurement - WHERE Measurement.result_id = Result.id - ) AS measurementsCount, - ( - SELECT COUNT(Measurement.id) FROM Measurement - WHERE Measurement.is_done = 1 - AND (Measurement.is_uploaded = 0 OR Measurement.report_id IS NULL) - AND Measurement.result_id = Result.id - ) == 0 AS allMeasurementsUploaded, - ( - SELECT COUNT(Measurement.id) FROM Measurement - WHERE Measurement.is_done = 1 - AND (Measurement.is_uploaded = 0 OR Measurement.report_id IS NULL) - AND Measurement.is_upload_failed = 1 - AND Measurement.result_id = Result.id - ) > 0 AS anyMeasurementUploadFailed -FROM Result -LEFT JOIN Network ON Result.network_id = Network.id -WHERE (:filterByDescriptor = 0 OR Result.test_group_name = :descriptorKey OR Result.descriptor_runId = :descriptorKey) -AND (:filterByTaskOrigin = 0 OR Result.task_origin = :taskOrigin) -ORDER BY Result.start_time DESC -LIMIT :limit; +SELECT *, + notUploadedMeasurements == 0 AS allMeasurementsUploaded, + uploadFailCount > 0 AS anyMeasurementUploadFailed +FROM ( + SELECT + MAX(Result.id) AS id, + MAX(Result.test_group_name) AS test_group_name, + MAX(Result.start_time) AS start_time, + MAX(Result.is_viewed) AS is_viewed, + MAX(Result.is_done) AS is_done, + MAX(Result.data_usage_up) AS data_usage_up, + MAX(Result.data_usage_down) AS data_usage_down, + MAX(Result.failure_msg) AS failure_msg, + MAX(Result.task_origin) AS task_origin, + MAX(Result.network_id) AS network_id, + MAX(Result.descriptor_runId) AS descriptor_runId, + MAX(Network.id) AS network_id_inner, + MAX(Network.network_name) AS network_name, + MAX(Network.ip) AS ip, + MAX(Network.asn) AS asn, + MAX(Network.country_code) AS country_code, + MAX(Network.network_type) AS network_type, + COUNT(Measurement.id) AS measurementsCount, + SUM( + CASE WHEN Measurement.is_done = 1 + AND (Measurement.is_uploaded = 0 OR Measurement.report_id IS NULL) + AND Measurement.is_upload_failed = 1 + THEN 1 ELSE 0 END + ) AS uploadFailCount, + SUM( + CASE WHEN Measurement.is_done = 1 + AND (Measurement.is_uploaded = 0 OR Measurement.report_id IS NULL) + THEN 1 ELSE 0 END + ) AS notUploadedMeasurements, + SUM(CASE WHEN Measurement.is_done = 1 THEN 1 ELSE 0 END) AS doneMeasurementsCount, + SUM(CASE WHEN Measurement.is_failed = 1 THEN 1 ELSE 0 END) AS failedMeasurementsCount, + SUM(CASE WHEN Measurement.is_anomaly = 1 THEN 1 ELSE 0 END) AS anomalyMeasurementsCount + FROM Result + LEFT JOIN Network ON Result.network_id = Network.id + LEFT JOIN Measurement ON Measurement.result_id = Result.id + WHERE (:filterByDescriptor = 0 OR Result.test_group_name = :descriptorKey OR Result.descriptor_runId = :descriptorKey) + AND (:filterByTaskOrigin = 0 OR Result.task_origin = :taskOrigin) + GROUP BY Result.id + ORDER BY Result.start_time DESC + LIMIT :limit +); selectByIdWithNetwork: SELECT Result.*, Network.* @@ -90,13 +110,16 @@ ORDER BY start_time DESC LIMIT 1; countMissingUpload: -SELECT COUNT(Result.id) FROM Result -WHERE ( - SELECT COUNT(Measurement.id) FROM Measurement - WHERE Measurement.is_done = 1 - AND (Measurement.is_uploaded = 0 OR Measurement.report_id IS NULL) - AND Measurement.result_id = Result.id -) > 0; +SELECT COUNT(failed_measurements > 0) +FROM ( + SELECT SUM(Measurement.id) AS failed_measurements + FROM Result + LEFT JOIN Measurement ON Measurement.result_id = Result.id + WHERE Measurement.is_done = 1 + AND (Measurement.is_uploaded = 0 OR Measurement.report_id IS NULL) + AND Measurement.result_id = Result.id + GROUP BY Result.id +); deleteByRunId: DELETE FROM Result WHERE descriptor_runId = ?; diff --git a/composeApp/src/commonTest/kotlin/org/ooni/probe/data/models/TestGroupTest.kt b/composeApp/src/commonTest/kotlin/org/ooni/probe/data/models/TestGroupTest.kt new file mode 100644 index 00000000..af4bedb1 --- /dev/null +++ b/composeApp/src/commonTest/kotlin/org/ooni/probe/data/models/TestGroupTest.kt @@ -0,0 +1,28 @@ +package org.ooni.probe.data.models + +import org.ooni.engine.models.TestGroup +import org.ooni.engine.models.TestType +import kotlin.test.Test +import kotlin.test.assertEquals + +class TestGroupTest { + @Test + fun fromTests() { + assertEquals( + TestGroup.Unknown, + TestGroup.fromTests(emptyList()), + ) + assertEquals( + TestGroup.Websites, + TestGroup.fromTests(listOf(TestType.WebConnectivity)), + ) + assertEquals( + TestGroup.Unknown, + TestGroup.fromTests(listOf(TestType.WebConnectivity, TestType.Ndt)), + ) + assertEquals( + TestGroup.InstantMessaging, + TestGroup.fromTests(listOf(TestType.FacebookMessenger, TestType.Whatsapp)), + ) + } +} diff --git a/composeApp/src/commonTest/kotlin/org/ooni/probe/ui/results/ResultsScreenTest.kt b/composeApp/src/commonTest/kotlin/org/ooni/probe/ui/results/ResultsScreenTest.kt index 4f1a7699..59666de7 100644 --- a/composeApp/src/commonTest/kotlin/org/ooni/probe/ui/results/ResultsScreenTest.kt +++ b/composeApp/src/commonTest/kotlin/org/ooni/probe/ui/results/ResultsScreenTest.kt @@ -10,6 +10,7 @@ import ooniprobe.composeapp.generated.resources.Modal_Delete import ooniprobe.composeapp.generated.resources.Res import ooniprobe.composeapp.generated.resources.TestResults_Overview_NoTestsHaveBeenRun import org.jetbrains.compose.resources.getString +import org.ooni.probe.data.models.MeasurementCounts import org.ooni.probe.data.models.ResultListItem import org.ooni.testing.factories.DescriptorFactory import org.ooni.testing.factories.NetworkModelFactory @@ -130,8 +131,12 @@ class ResultsScreenTest { result = ResultModelFactory.build(), descriptor = DescriptorFactory.buildDescriptorWithInstalled(), network = NetworkModelFactory.build(), - measurementsCount = 4, - allMeasurementsUploaded = false, + measurementCounts = MeasurementCounts( + done = 4, + failed = 0, + anomaly = 0, + ), + allMeasurementsUploaded = true, anyMeasurementUploadFailed = false, ) } diff --git a/composeApp/src/iosMain/kotlin/org/ooni/probe/SetupDependencies.kt b/composeApp/src/iosMain/kotlin/org/ooni/probe/SetupDependencies.kt index 6ac437eb..38376377 100644 --- a/composeApp/src/iosMain/kotlin/org/ooni/probe/SetupDependencies.kt +++ b/composeApp/src/iosMain/kotlin/org/ooni/probe/SetupDependencies.kt @@ -194,7 +194,7 @@ class SetupDependencies( } } - private fun presentViewController(uiViewController: UIViewController): Boolean { + private fun presentViewController(uiViewController: UIViewController): Boolean { return findCurrentViewController()?.let { viewController -> if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e35229ea..e0ae0b06 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -54,7 +54,7 @@ sqldelight-android = { module = "app.cash.sqldelight:android-driver", version.re sqldelight-native = { module = "app.cash.sqldelight:native-driver", version.ref = "sqldelight" } # Files -okio = { module = "com.squareup.okio:okio", version = "3.9.0" } +okio = { module = "com.squareup.okio:okio", version = "3.9.1" } # Lottie animations kottie = { module = "io.github.alexzhirkevich:compottie", version = "2.0.0-rc01" } # 2.0.0 not supported yet @@ -79,7 +79,7 @@ android-orchestrator = { module = "androidx.test:orchestrator", version = "1.5.1 androidx-junit-ktx = { group = "androidx.test.ext", name = "junit-ktx", version.ref = "junitKtx" } android-test-runner = { module = "androidx.test:runner", version = "1.6.2" } android-test-rules = { module = "androidx.test:rules", version = "1.6.1" } -androidx-compose-test-android = { module = "androidx.compose.ui:ui-test-junit4-android", version = "1.7.5" } +androidx-compose-test-android = { module = "androidx.compose.ui:ui-test-junit4-android", version = "1.7.6" } androidx-espresso-web = { module = "androidx.test.espresso:espresso-web", version = "3.6.1" } [bundles]