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]