diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_cloud_off.xml b/composeApp/src/commonMain/composeResources/drawable/ic_cloud_off.xml
new file mode 100644
index 00000000..555e1f72
--- /dev/null
+++ b/composeApp/src/commonMain/composeResources/drawable/ic_cloud_off.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 32fc6c04..16c349df 100644
--- a/composeApp/src/commonMain/composeResources/values/strings-common.xml
+++ b/composeApp/src/commonMain/composeResources/values/strings-common.xml
@@ -1,15 +1,12 @@
- Run Test
- Back
- Dashboard
- Settings
-
+ Dashboard
OONI Tests
OONI Run Links
Stopping test…
Finishing the currently pending tests, please wait…
+ Run
Stop test
Websites
@@ -35,6 +32,7 @@
Unknown
N/A
+ Settings
Notifications
Enabled
Interested in running OONI Probe tests during emergent censorship events? Enable notifications to receive a message when we hear of internet censorship near you.
@@ -131,5 +129,14 @@
Intergovernmental organizations including The United Nations
Sites that haven\'t been categorized yet
+ Not uploaded
+
Unable to download URL list. Please try again.
+
+
+ Back
+
+ - %1$d measurement
+ - %1$d measurements
+
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/engine/TaskEventMapper.kt b/composeApp/src/commonMain/kotlin/org/ooni/engine/TaskEventMapper.kt
index 4015fa55..09a4ba99 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/engine/TaskEventMapper.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/engine/TaskEventMapper.kt
@@ -98,15 +98,10 @@ class TaskEventMapper(
TaskEvent.MeasurementDone(index = value?.idx ?: 0)
"status.measurement_start" ->
- value?.input?.ifEmpty { null }?.let { url ->
- TaskEvent.MeasurementStart(
- index = value.idx,
- url = url,
- )
- } ?: run {
- Logger.d("Task Event $key missing 'input'")
- null
- }
+ TaskEvent.MeasurementStart(
+ index = value?.idx ?: 0,
+ url = value?.input,
+ )
"status.measurement_submission" ->
TaskEvent.MeasurementSubmissionSuccessful(index = value?.idx ?: 0)
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TaskEvent.kt b/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TaskEvent.kt
index cad4bd91..3b35dfa0 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TaskEvent.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TaskEvent.kt
@@ -35,7 +35,7 @@ sealed interface TaskEvent {
data class MeasurementStart(
val index: Int,
- val url: String,
+ val url: String?,
) : TaskEvent
data class MeasurementSubmissionSuccessful(
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TestType.kt b/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TestType.kt
index 69707d1f..45b72e35 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TestType.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TestType.kt
@@ -139,19 +139,22 @@ sealed class TestType {
}
companion object {
- private val ALL_NAMED = listOf(
- Dash,
- FacebookMessenger,
- HttpHeaderFieldManipulation,
- HttpInvalidRequestLine,
- Ndt,
- Psiphon,
- Signal,
- Telegram,
- Tor,
- WebConnectivity,
- Whatsapp,
- )
+ // Lazy due to https://youtrack.jetbrains.com/issue/KT-8970/Object-is-uninitialized-null-when-accessed-from-static-context-ex.-companion-object-with-initialization-loop
+ private val ALL_NAMED by lazy {
+ listOf(
+ Dash,
+ FacebookMessenger,
+ HttpHeaderFieldManipulation,
+ HttpInvalidRequestLine,
+ Ndt,
+ Psiphon,
+ Signal,
+ Telegram,
+ Tor,
+ WebConnectivity,
+ Whatsapp,
+ )
+ }
fun fromName(name: String) = ALL_NAMED.firstOrNull { it.name == name } ?: Experimental(name)
}
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/App.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/App.kt
index c4d71e9e..8cf5d415 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/probe/App.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/App.kt
@@ -1,6 +1,8 @@
package org.ooni.probe
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
@@ -37,11 +39,15 @@ fun App(dependencies: Dependencies) {
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) },
bottomBar = { BottomNavigationBar(navController) },
- ) {
- Navigation(
- navController = navController,
- dependencies = dependencies,
- )
+ ) { paddingValues ->
+ Box(
+ modifier = Modifier.padding(bottom = paddingValues.calculateBottomPadding()),
+ ) {
+ Navigation(
+ navController = navController,
+ dependencies = dependencies,
+ )
+ }
}
}
}
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/Descriptor.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/Descriptor.kt
index e62da673..2c3549ea 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/Descriptor.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/Descriptor.kt
@@ -27,4 +27,7 @@ data class Descriptor(
}
val isExpired get() = expirationDate != null && expirationDate < LocalDateTime.now()
+
+ companion object {
+ }
}
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/MeasurementModel.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/MeasurementModel.kt
index a64c1d46..c62a6612 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/MeasurementModel.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/MeasurementModel.kt
@@ -29,6 +29,8 @@ data class MeasurementModel(
val value: Long,
)
+ val idOrThrow get() = id ?: throw IllegalStateException("Id no available")
+
val logFilePath: Path
get() = logFilePath(resultId, test)
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/MeasurementWithUrl.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/MeasurementWithUrl.kt
new file mode 100644
index 00000000..8a21a3cb
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/MeasurementWithUrl.kt
@@ -0,0 +1,6 @@
+package org.ooni.probe.data.models
+
+data class MeasurementWithUrl(
+ val measurement: MeasurementModel,
+ val url: UrlModel?,
+)
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/ResultItem.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/ResultItem.kt
new file mode 100644
index 00000000..44a478a4
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/ResultItem.kt
@@ -0,0 +1,8 @@
+package org.ooni.probe.data.models
+
+data class ResultItem(
+ val result: ResultModel,
+ val descriptor: Descriptor,
+ val network: NetworkModel?,
+ val measurements: List,
+)
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 f36e81dd..ff4c11dc 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
@@ -2,8 +2,10 @@ package org.ooni.probe.data.models
data class ResultListItem(
val result: ResultModel,
+ val descriptor: Descriptor,
val network: NetworkModel?,
val measurementsCount: Long,
+ val allMeasurementsUploaded: Boolean,
) {
val idOrThrow get() = result.idOrThrow
}
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
new file mode 100644
index 00000000..e0e079e1
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/ResultWithNetworkAndAggregates.kt
@@ -0,0 +1,8 @@
+package org.ooni.probe.data.models
+
+data class ResultWithNetworkAndAggregates(
+ val result: ResultModel,
+ val network: NetworkModel?,
+ val measurementsCount: Long,
+ val allMeasurementsUploaded: Boolean,
+)
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 a440b053..e8ba2802 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
@@ -9,7 +9,10 @@ import kotlinx.coroutines.withContext
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.Url
import org.ooni.probe.data.models.MeasurementModel
+import org.ooni.probe.data.models.MeasurementWithUrl
import org.ooni.probe.data.models.ResultModel
import org.ooni.probe.data.models.UrlModel
import org.ooni.probe.shared.toEpoch
@@ -26,6 +29,13 @@ class MeasurementRepository(
.mapToList(backgroundDispatcher)
.map { list -> list.mapNotNull { it.toModel() } }
+ fun listByResultId(id: ResultModel.Id) =
+ database.measurementQueries
+ .selectByResultIdWithUrl(id.value)
+ .asFlow()
+ .mapToList(backgroundDispatcher)
+ .map { list -> list.mapNotNull { it.toModel() } }
+
suspend fun createOrUpdate(model: MeasurementModel): MeasurementModel.Id =
withContext(backgroundDispatcher) {
database.transactionWithResult {
@@ -76,4 +86,36 @@ class MeasurementRepository(
resultId = result_id?.let(ResultModel::Id) ?: return null,
)
}
+
+ private fun SelectByResultIdWithUrl.toModel(): MeasurementWithUrl? {
+ return MeasurementWithUrl(
+ measurement = Measurement(
+ id = id,
+ test_name = test_name,
+ start_time = start_time,
+ runtime = runtime,
+ is_done = is_done,
+ is_uploaded = is_uploaded,
+ is_failed = is_failed,
+ failure_msg = failure_msg,
+ is_upload_failed = is_upload_failed,
+ upload_failure_msg = upload_failure_msg,
+ is_rerun = is_rerun,
+ is_anomaly = is_anomaly,
+ report_id = report_id,
+ test_keys = test_keys,
+ rerun_network = rerun_network,
+ url_id = url_id,
+ result_id = result_id,
+ ).toModel() ?: return null,
+ url = id_?.let { urlId ->
+ Url(
+ id = urlId,
+ url = url,
+ country_code = country_code,
+ category_code = category_code,
+ ).toModel()
+ },
+ )
+ }
}
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 82d406a7..8f9d98f8 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
@@ -2,6 +2,7 @@ package org.ooni.probe.data.repositories
import app.cash.sqldelight.coroutines.asFlow
import app.cash.sqldelight.coroutines.mapToList
+import app.cash.sqldelight.coroutines.mapToOneOrNull
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
@@ -10,10 +11,11 @@ import org.ooni.probe.Database
import org.ooni.probe.data.Network
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.NetworkModel
-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.shared.toEpoch
import org.ooni.probe.shared.toLocalDateTime
@@ -28,19 +30,19 @@ class ResultRepository(
.mapToList(backgroundDispatcher)
.map { list -> list.mapNotNull { it.toModel() } }
- fun listWithNetwork(): Flow> =
+ fun listWithNetwork(): Flow> =
database.resultQueries
.selectAllWithNetwork()
.asFlow()
.mapToList(backgroundDispatcher)
.map { list -> list.mapNotNull { it.toModel() } }
- fun getById(resultId: ResultModel.Id): Flow =
+ fun getById(resultId: ResultModel.Id): Flow?> =
database.resultQueries
- .selectById(resultId.value)
+ .selectByIdWithNetwork(resultId.value)
.asFlow()
- .mapToList(backgroundDispatcher)
- .map { it.firstOrNull()?.toModel() }
+ .mapToOneOrNull(backgroundDispatcher)
+ .map { it?.toModel() }
suspend fun createOrUpdate(model: ResultModel): ResultModel.Id =
withContext(backgroundDispatcher) {
@@ -79,33 +81,59 @@ class ResultRepository(
)
}
- private fun SelectAllWithNetwork.toModel(): ResultListItem? {
- return ResultListItem(
- result =
- Result(
- id = id,
- test_group_name = test_group_name,
- start_time = start_time,
- is_viewed = is_viewed,
- is_done = is_done,
- data_usage_up = data_usage_up,
- data_usage_down = data_usage_down,
- failure_msg = failure_msg,
- network_id = network_id,
- descriptor_runId = descriptor_runId,
- ).toModel() ?: return null,
- network =
- id_?.let { networkId ->
- Network(
- id = networkId,
- network_name = network_name,
- ip = ip,
- asn = asn,
- country_code = country_code,
- network_type = network_type,
- ).toModel()
- },
+ private fun SelectAllWithNetwork.toModel(): ResultWithNetworkAndAggregates? {
+ return ResultWithNetworkAndAggregates(
+ result = Result(
+ id = id,
+ test_group_name = test_group_name,
+ start_time = start_time,
+ is_viewed = is_viewed,
+ is_done = is_done,
+ data_usage_up = data_usage_up,
+ data_usage_down = data_usage_down,
+ failure_msg = failure_msg,
+ network_id = network_id,
+ descriptor_runId = descriptor_runId,
+ ).toModel() ?: return null,
+ network = id_?.let { networkId ->
+ Network(
+ id = networkId,
+ network_name = network_name,
+ ip = ip,
+ asn = asn,
+ country_code = country_code,
+ network_type = network_type,
+ ).toModel()
+ },
measurementsCount = measurementsCount,
+ allMeasurementsUploaded = allMeasurementsUploaded,
+ )
+ }
+
+ private fun SelectByIdWithNetwork.toModel(): Pair? {
+ return Pair(
+ Result(
+ id = id,
+ test_group_name = test_group_name,
+ start_time = start_time,
+ is_viewed = is_viewed,
+ is_done = is_done,
+ data_usage_up = data_usage_up,
+ data_usage_down = data_usage_down,
+ failure_msg = failure_msg,
+ network_id = network_id,
+ descriptor_runId = descriptor_runId,
+ ).toModel() ?: return null,
+ id_?.let { networkId ->
+ Network(
+ id = networkId,
+ network_name = network_name,
+ ip = ip,
+ asn = asn,
+ country_code = country_code,
+ network_type = network_type,
+ ).toModel()
+ },
)
}
}
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/UrlRepository.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/UrlRepository.kt
index 81bf86c1..af448d34 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/UrlRepository.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/UrlRepository.kt
@@ -82,13 +82,13 @@ class UrlRepository(
fun getByUrl(url: String): Flow =
listByUrls(listOf(url))
.map { it.firstOrNull() }
+}
- private fun Url.toModel(): UrlModel? {
- return UrlModel(
- id = UrlModel.Id(id),
- url = url ?: return null,
- countryCode = country_code,
- category = category_code?.let(WebConnectivityCategory::fromCode),
- )
- }
+fun Url.toModel(): UrlModel? {
+ return UrlModel(
+ id = UrlModel.Id(id),
+ url = url ?: return null,
+ countryCode = country_code,
+ category = category_code?.let(WebConnectivityCategory::fromCode),
+ )
}
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 ac6fbaba..0eda2206 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt
@@ -118,8 +118,19 @@ class Dependencies(
GetBootstrapTestDescriptors(readAssetFile, json, backgroundDispatcher)
}
private val getDefaultTestDescriptors by lazy { GetDefaultTestDescriptors() }
- private val getResults by lazy { GetResults(resultRepository) }
- private val getResult by lazy { GetResult(resultRepository) }
+ private val getResults by lazy {
+ GetResults(
+ resultRepository.listWithNetwork(),
+ getTestDescriptors.invoke(),
+ )
+ }
+ private val getResult by lazy {
+ GetResult(
+ getResultById = resultRepository::getById,
+ getTestDescriptors = getTestDescriptors.invoke(),
+ getMeasurementsByResultId = measurementRepository::listByResultId,
+ )
+ }
private val getTestDescriptors by lazy {
GetTestDescriptors(
getDefaultTestDescriptors = getDefaultTestDescriptors::invoke,
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetResult.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetResult.kt
index e77466e3..ddfed53d 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetResult.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetResult.kt
@@ -1,10 +1,30 @@
package org.ooni.probe.domain
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import org.ooni.probe.data.models.Descriptor
+import org.ooni.probe.data.models.MeasurementWithUrl
+import org.ooni.probe.data.models.NetworkModel
+import org.ooni.probe.data.models.ResultItem
import org.ooni.probe.data.models.ResultModel
-import org.ooni.probe.data.repositories.ResultRepository
class GetResult(
- private val resultRepository: ResultRepository,
+ private val getResultById: (ResultModel.Id) -> Flow?>,
+ private val getTestDescriptors: Flow>,
+ private val getMeasurementsByResultId: (ResultModel.Id) -> Flow>,
) {
- operator fun invoke(resultId: ResultModel.Id) = resultRepository.getById(resultId)
+ operator fun invoke(resultId: ResultModel.Id): Flow =
+ combine(
+ getResultById(resultId),
+ getTestDescriptors,
+ getMeasurementsByResultId(resultId),
+ ) { resultWithNetwork, descriptors, measurements ->
+ val result = resultWithNetwork?.first ?: return@combine null
+ ResultItem(
+ result = result,
+ descriptor = descriptors.forResult(result) ?: return@combine null,
+ network = resultWithNetwork.second,
+ measurements = measurements,
+ )
+ }
}
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 446dd63e..5f9883e8 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetResults.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetResults.kt
@@ -1,9 +1,38 @@
package org.ooni.probe.domain
-import org.ooni.probe.data.repositories.ResultRepository
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import org.ooni.probe.data.models.Descriptor
+import org.ooni.probe.data.models.ResultListItem
+import org.ooni.probe.data.models.ResultModel
+import org.ooni.probe.data.models.ResultWithNetworkAndAggregates
class GetResults(
- private val resultRepository: ResultRepository,
+ private val getResultsWithNetwork: Flow>,
+ private val getDescriptors: Flow>,
) {
- operator fun invoke() = resultRepository.listWithNetwork()
+ operator fun invoke(): Flow> =
+ combine(
+ getResultsWithNetwork,
+ getDescriptors,
+ ) { results, descriptors ->
+ results.mapNotNull { item ->
+ ResultListItem(
+ result = item.result,
+ descriptor = descriptors.forResult(item.result) ?: return@mapNotNull null,
+ network = item.network,
+ measurementsCount = item.measurementsCount,
+ allMeasurementsUploaded = item.allMeasurementsUploaded,
+ )
+ }
+ }
}
+
+fun List.forResult(result: ResultModel): Descriptor? =
+ result.testDescriptorId
+ ?.let { descriptorId ->
+ firstOrNull {
+ it.source is Descriptor.Source.Installed && it.source.value.id == descriptorId
+ }
+ }
+ ?: firstOrNull { it.name == result.testGroupName }
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 3368031b..d692772e 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/RunNetTest.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/RunNetTest.kt
@@ -93,12 +93,11 @@ class RunNetTest(
test = spec.netTest.test,
reportId = reportId,
resultId = result.id ?: return,
- urlId =
- if (event.url.isNotEmpty()) {
- getUrlByUrl(event.url).first()?.id
- } else {
- null
- },
+ urlId = if (event.url.isNullOrEmpty()) {
+ null
+ } else {
+ getUrlByUrl(event.url).first()?.id
+ },
),
)
}
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/DashboardScreen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/DashboardScreen.kt
index 0e5bad24..c99562b7 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/DashboardScreen.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/DashboardScreen.kt
@@ -22,10 +22,10 @@ import ooniprobe.composeapp.generated.resources.Dashboard_Running_Stopping_Notic
import ooniprobe.composeapp.generated.resources.Dashboard_Running_Stopping_Title
import ooniprobe.composeapp.generated.resources.Modal_Error_CantDownloadURLs
import ooniprobe.composeapp.generated.resources.Notification_StopTest
+import ooniprobe.composeapp.generated.resources.OONIRun_Run
import ooniprobe.composeapp.generated.resources.Res
import ooniprobe.composeapp.generated.resources.app_name
import ooniprobe.composeapp.generated.resources.logo
-import ooniprobe.composeapp.generated.resources.run_tests
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
import org.jetbrains.compose.ui.tooling.preview.Preview
@@ -58,7 +58,7 @@ fun DashboardScreen(
Button(
onClick = { onEvent(DashboardViewModel.Event.RunTestsClick) },
) {
- Text(stringResource(Res.string.run_tests))
+ Text(stringResource(Res.string.OONIRun_Run))
}
}
@@ -113,7 +113,7 @@ fun DashboardScreen(
state.tests.forEach { (type, tests) ->
if (allSectionsHaveValues && tests.isNotEmpty()) {
item(type) {
- TestDescriptorItem(type)
+ TestDescriptorSection(type)
}
}
items(tests) { test ->
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/TestDescriptorItem.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/TestDescriptorItem.kt
index 7029077f..a8031a47 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/TestDescriptorItem.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/TestDescriptorItem.kt
@@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@@ -15,7 +14,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import ooniprobe.composeapp.generated.resources.Res
import ooniprobe.composeapp.generated.resources.ic_chevron_right
-import ooniprobe.composeapp.generated.resources.ic_settings
import org.jetbrains.compose.resources.painterResource
import org.ooni.probe.data.models.Descriptor
@@ -35,25 +33,8 @@ fun TestDescriptorItem(descriptor: Descriptor) {
Column(
modifier = Modifier.weight(1f),
) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.padding(bottom = 2.dp),
- ) {
- Icon(
- // TODO: pick better fallback icon
- painter = painterResource(descriptor.icon ?: Res.drawable.ic_settings),
- contentDescription = null,
- tint = descriptor.color ?: MaterialTheme.colorScheme.onSurface,
- modifier =
- Modifier
- .size(24.dp)
- .padding(end = 4.dp),
- )
- Text(
- descriptor.title(),
- style = MaterialTheme.typography.titleMedium,
- )
- }
+ TestDescriptorLabel(descriptor)
+
descriptor.shortDescription()?.let { shortDescription ->
Text(
shortDescription,
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/TestDescriptorLabel.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/TestDescriptorLabel.kt
index 390cf112..fa72daa1 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/TestDescriptorLabel.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/TestDescriptorLabel.kt
@@ -1,31 +1,41 @@
package org.ooni.probe.ui.dashboard
-import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
-import ooniprobe.composeapp.generated.resources.Dashboard_RunV2_Ooni_Title
-import ooniprobe.composeapp.generated.resources.Dashboard_RunV2_Title
import ooniprobe.composeapp.generated.resources.Res
-import org.jetbrains.compose.resources.stringResource
+import ooniprobe.composeapp.generated.resources.ic_settings
+import org.jetbrains.compose.resources.painterResource
+import org.ooni.probe.data.models.Descriptor
@Composable
-fun TestDescriptorItem(type: DashboardViewModel.DescriptorType) {
- Text(
- stringResource(
- when (type) {
- DashboardViewModel.DescriptorType.Default -> Res.string.Dashboard_RunV2_Ooni_Title
- DashboardViewModel.DescriptorType.Installed -> Res.string.Dashboard_RunV2_Title
- },
- ).uppercase(),
- style = MaterialTheme.typography.labelLarge,
- modifier =
- Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp)
- .padding(top = 16.dp, bottom = 4.dp),
- )
+fun TestDescriptorLabel(descriptor: Descriptor) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(bottom = 2.dp),
+ ) {
+ Icon(
+ // TODO: pick better fallback icon
+ painter = painterResource(descriptor.icon ?: Res.drawable.ic_settings),
+ contentDescription = null,
+ tint = descriptor.color ?: Color.Unspecified,
+ modifier =
+ Modifier
+ .size(24.dp)
+ .padding(end = 4.dp),
+ )
+ Text(
+ descriptor.title(),
+ style = MaterialTheme.typography.titleMedium,
+ color = descriptor.color ?: Color.Unspecified,
+ )
+ }
}
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/TestDescriptorSection.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/TestDescriptorSection.kt
new file mode 100644
index 00000000..32fcbf42
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/TestDescriptorSection.kt
@@ -0,0 +1,31 @@
+package org.ooni.probe.ui.dashboard
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import ooniprobe.composeapp.generated.resources.Dashboard_RunV2_Ooni_Title
+import ooniprobe.composeapp.generated.resources.Dashboard_RunV2_Title
+import ooniprobe.composeapp.generated.resources.Res
+import org.jetbrains.compose.resources.stringResource
+
+@Composable
+fun TestDescriptorSection(type: DashboardViewModel.DescriptorType) {
+ Text(
+ stringResource(
+ when (type) {
+ DashboardViewModel.DescriptorType.Default -> Res.string.Dashboard_RunV2_Ooni_Title
+ DashboardViewModel.DescriptorType.Installed -> Res.string.Dashboard_RunV2_Title
+ },
+ ).uppercase(),
+ style = MaterialTheme.typography.labelLarge,
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp)
+ .padding(top = 16.dp, bottom = 4.dp),
+ )
+}
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/BottomNavigationBar.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/BottomNavigationBar.kt
index 633fc229..592c1219 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/BottomNavigationBar.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/BottomNavigationBar.kt
@@ -9,13 +9,13 @@ import androidx.compose.runtime.getValue
import androidx.navigation.NavController
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.currentBackStackEntryAsState
+import ooniprobe.composeapp.generated.resources.Dashboard_Tab_Label
import ooniprobe.composeapp.generated.resources.Res
+import ooniprobe.composeapp.generated.resources.Settings_Title
import ooniprobe.composeapp.generated.resources.TestResults_Overview_Tab_Label
-import ooniprobe.composeapp.generated.resources.dashboard
import ooniprobe.composeapp.generated.resources.ic_dashboard
import ooniprobe.composeapp.generated.resources.ic_history
import ooniprobe.composeapp.generated.resources.ic_settings
-import ooniprobe.composeapp.generated.resources.settings
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
@@ -63,9 +63,9 @@ fun BottomNavigationBar(navController: NavController) {
private val Screen.titleRes
get() =
when (this) {
- Screen.Dashboard -> Res.string.dashboard
+ Screen.Dashboard -> Res.string.Dashboard_Tab_Label
Screen.Results -> Res.string.TestResults_Overview_Tab_Label
- Screen.Settings -> Res.string.settings
+ Screen.Settings -> Res.string.Settings_Title
else -> throw IllegalArgumentException("Only main screens allowed in bottom navigation")
}
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultScreen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultScreen.kt
index d9364bf6..f017e80c 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultScreen.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultScreen.kt
@@ -1,16 +1,30 @@
package org.ooni.probe.ui.result
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
import ooniprobe.composeapp.generated.resources.Res
import ooniprobe.composeapp.generated.resources.back
+import ooniprobe.composeapp.generated.resources.ic_settings
+import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
+import org.ooni.engine.models.TestType
+import org.ooni.probe.data.models.MeasurementWithUrl
@Composable
fun ResultScreen(
@@ -20,7 +34,7 @@ fun ResultScreen(
Column {
TopAppBar(
title = {
- Text(state.result?.testGroupName.orEmpty())
+ Text(state.result?.descriptor?.title?.invoke().orEmpty())
},
navigationIcon = {
IconButton(onClick = { onEvent(ResultViewModel.Event.BackClicked) }) {
@@ -30,6 +44,50 @@ fun ResultScreen(
)
}
},
+ colors = TopAppBarDefaults.topAppBarColors(
+ containerColor = state.result?.descriptor?.color
+ ?: MaterialTheme.colorScheme.primary,
+ titleContentColor = MaterialTheme.colorScheme.onPrimary,
+ navigationIconContentColor = MaterialTheme.colorScheme.onPrimary,
+ actionIconContentColor = MaterialTheme.colorScheme.onPrimary,
+ ),
+ )
+
+ if (state.result == null) return@Column
+
+ LazyColumn {
+ items(state.result.measurements, key = { it.measurement.idOrThrow.value }) { item ->
+ ResultMeasurementItem(item)
+ }
+ }
+ }
+}
+
+@Composable
+fun ResultMeasurementItem(item: MeasurementWithUrl) {
+ val test = item.measurement.test
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(16.dp),
+ ) {
+ Icon(
+ // TODO: Better fallback for nettest icon
+ // TODO: Web categories icon
+ painterResource(test.iconRes ?: Res.drawable.ic_settings),
+ contentDescription = null,
+ modifier = Modifier
+ .padding(end = 16.dp)
+ .size(24.dp),
+ )
+ Text(
+ text = if (test == TestType.WebConnectivity && item.url != null) {
+ item.url.url
+ } else if (test is TestType.Experimental) {
+ test.name
+ } else {
+ stringResource(test.labelRes)
+ },
+ maxLines = 1,
)
}
}
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultViewModel.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultViewModel.kt
index f3c2ca4f..0ec06a91 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultViewModel.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultViewModel.kt
@@ -10,12 +10,14 @@ import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
+import org.ooni.probe.data.models.MeasurementModel
+import org.ooni.probe.data.models.ResultItem
import org.ooni.probe.data.models.ResultModel
class ResultViewModel(
resultId: ResultModel.Id,
onBack: () -> Unit,
- getResult: (ResultModel.Id) -> Flow,
+ getResult: (ResultModel.Id) -> Flow,
) : ViewModel() {
private val events = MutableSharedFlow(extraBufferCapacity = 1)
@@ -38,10 +40,12 @@ class ResultViewModel(
}
data class State(
- val result: ResultModel?,
+ val result: ResultItem?,
)
sealed interface Event {
data object BackClicked : Event
+
+ data class MeasurementClicked(val measurementId: MeasurementModel.Id) : Event
}
}
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/results/ResultsScreen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/results/ResultsScreen.kt
index 15fd6652..7a7581d3 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/results/ResultsScreen.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/results/ResultsScreen.kt
@@ -1,27 +1,40 @@
package org.ooni.probe.ui.results
+import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDate.Companion.Format
+import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.format
+import kotlinx.datetime.format.MonthNames
import kotlinx.datetime.format.char
import ooniprobe.composeapp.generated.resources.Res
+import ooniprobe.composeapp.generated.resources.Snackbar_ResultsNotUploaded_Text
import ooniprobe.composeapp.generated.resources.TestResults_Overview_Title
import ooniprobe.composeapp.generated.resources.TestResults_UnknownASN
+import ooniprobe.composeapp.generated.resources.ic_cloud_off
+import ooniprobe.composeapp.generated.resources.measurements_count
+import org.jetbrains.compose.resources.painterResource
+import org.jetbrains.compose.resources.pluralStringResource
import org.jetbrains.compose.resources.stringResource
import org.ooni.probe.data.models.ResultListItem
+import org.ooni.probe.ui.dashboard.TestDescriptorLabel
@Composable
fun ResultsScreen(
@@ -56,15 +69,16 @@ fun ResultDateHeader(date: LocalDate) {
Text(
date.format(
Format {
+ monthName(MonthNames.ENGLISH_FULL) // TODO: localize months
+ char(' ')
year()
- char('-')
- monthNumber()
},
),
style = MaterialTheme.typography.labelLarge,
modifier =
Modifier
.fillMaxWidth()
+ .background(MaterialTheme.colorScheme.background)
.padding(horizontal = 16.dp, vertical = 4.dp),
)
}
@@ -83,16 +97,69 @@ fun ResultItem(
},
modifier = Modifier.padding(top = 1.dp),
) {
- Column(
- modifier =
- Modifier
- .fillMaxWidth()
- .clickable { onResultClick() }
- .padding(horizontal = 16.dp, vertical = 8.dp),
+ Row(
+ verticalAlignment = Alignment.Bottom,
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable { onResultClick() }
+ .padding(horizontal = 16.dp, vertical = 8.dp),
) {
- Text(item.result.testGroupName.orEmpty())
- Text(item.network?.networkName ?: stringResource(Res.string.TestResults_UnknownASN))
- Text(item.result.startTime.toString())
+ Column(
+ modifier = Modifier.weight(0.66f),
+ ) {
+ TestDescriptorLabel(item.descriptor)
+
+ Text(
+ item.network?.networkName ?: stringResource(Res.string.TestResults_UnknownASN),
+ style = MaterialTheme.typography.titleLarge,
+ )
+
+ Text(
+ item.result.startTime.format(
+ LocalDateTime.Format {
+ date(LocalDate.Formats.ISO)
+ char(' ')
+ hour()
+ char(':')
+ minute()
+ char(':')
+ second()
+ },
+ ),
+ )
+ }
+ Column(
+ horizontalAlignment = Alignment.End,
+ modifier = Modifier.weight(0.34f),
+ ) {
+ Text(
+ pluralStringResource(
+ Res.plurals.measurements_count,
+ item.measurementsCount.toInt(),
+ item.measurementsCount,
+ ),
+ style = MaterialTheme.typography.labelLarge,
+ modifier = Modifier.padding(bottom = 2.dp),
+ )
+ if (!item.allMeasurementsUploaded) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Icon(
+ painterResource(Res.drawable.ic_cloud_off),
+ tint = MaterialTheme.typography.labelLarge.color,
+ contentDescription = null,
+ modifier = Modifier
+ .size(16.dp)
+ .padding(end = 2.dp),
+ )
+ Text(
+ stringResource(Res.string.Snackbar_ResultsNotUploaded_Text).lowercase(),
+ style = MaterialTheme.typography.labelLarge,
+ )
+ }
+ }
+ }
}
}
}
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/results/ResultsViewModel.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/results/ResultsViewModel.kt
index 8268eefe..02a9c569 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/results/ResultsViewModel.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/results/ResultsViewModel.kt
@@ -48,10 +48,9 @@ class ResultsViewModel(
}
private val ResultListItem.monthAndYear
- get() =
- result.startTime.let { startTime ->
- LocalDate(year = startTime.year, month = startTime.month, dayOfMonth = 1)
- }
+ get() = result.startTime.let { startTime ->
+ LocalDate(year = startTime.year, month = startTime.month, dayOfMonth = 1)
+ }
data class State(
val results: Map>,
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsScreen.kt
index afa7580c..7ef971c4 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsScreen.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsScreen.kt
@@ -12,7 +12,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import ooniprobe.composeapp.generated.resources.Res
-import ooniprobe.composeapp.generated.resources.settings
+import ooniprobe.composeapp.generated.resources.Settings_Title
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.painterResource
@@ -24,7 +24,7 @@ fun SettingsScreen(onNavigateToSettingsCategory: (SettingsViewModel.Event) -> Un
Column {
TopAppBar(
title = {
- Text(stringResource(Res.string.settings))
+ Text(stringResource(Res.string.Settings_Title))
},
)
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryScreen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryScreen.kt
index 38ae604d..ba051157 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryScreen.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryScreen.kt
@@ -24,8 +24,8 @@ import androidx.compose.ui.draw.scale
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import ooniprobe.composeapp.generated.resources.Res
+import ooniprobe.composeapp.generated.resources.Settings_Title
import ooniprobe.composeapp.generated.resources.back
-import ooniprobe.composeapp.generated.resources.settings
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
@@ -101,7 +101,7 @@ fun SettingsCategoryScreen(
Button(
onClick = {},
) {
- Text(stringResource(Res.string.settings))
+ Text(stringResource(Res.string.Settings_Title))
}
},
)
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 2e98bf26..23c34ee0 100644
--- a/composeApp/src/commonMain/sqldelight/org/ooni/probe/data/Measurement.sq
+++ b/composeApp/src/commonMain/sqldelight/org/ooni/probe/data/Measurement.sq
@@ -46,3 +46,8 @@ SELECT last_insert_rowid();
selectAll:
SELECT * FROM Measurement;
+
+selectByResultIdWithUrl:
+SELECT * FROM Measurement
+LEFT JOIN Url ON Measurement.url_id = Url.id
+WHERE Measurement.result_id = ?;
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 3ddeb3b9..d6d3c22d 100644
--- a/composeApp/src/commonMain/sqldelight/org/ooni/probe/data/Result.sq
+++ b/composeApp/src/commonMain/sqldelight/org/ooni/probe/data/Result.sq
@@ -30,14 +30,24 @@ selectLastInsertedRowId:
SELECT last_insert_rowid();
selectAll:
-SELECT * FROM Result ORDER BY start_time DESC;
+SELECT * FROM Result
+ORDER BY start_time DESC
+LIMIT 100;
selectAllWithNetwork:
-SELECT Result.*, Network.*, (SELECT COUNT(Measurement.id) FROM Measurement WHERE Measurement.result_id = Result.id) AS measurementsCount
+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_uploaded = 0) == 0 AS allMeasurementsUploaded
FROM Result
LEFT JOIN Network ON Result.network_id = Network.id
-ORDER BY Result.start_time DESC;
-
-selectById:
-SELECT Result.* FROM Result WHERE id = ? LIMIT 1;
+ORDER BY Result.start_time DESC
+LIMIT 100;
+selectByIdWithNetwork:
+SELECT Result.*, Network.*
+FROM Result
+LEFT JOIN Network ON Result.network_id = Network.id
+WHERE Result.id = ?
+LIMIT 1;
diff --git a/composeApp/src/commonTest/kotlin/org/ooni/probe/ui/result/ResultScreenTest.kt b/composeApp/src/commonTest/kotlin/org/ooni/probe/ui/result/ResultScreenTest.kt
index 85d47103..e4ed44c7 100644
--- a/composeApp/src/commonTest/kotlin/org/ooni/probe/ui/result/ResultScreenTest.kt
+++ b/composeApp/src/commonTest/kotlin/org/ooni/probe/ui/result/ResultScreenTest.kt
@@ -4,6 +4,8 @@ import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.runComposeUiTest
+import org.ooni.probe.data.models.ResultItem
+import org.ooni.testing.factories.DescriptorFactory
import org.ooni.testing.factories.ResultModelFactory
import kotlin.test.Test
import kotlin.test.assertEquals
@@ -12,25 +14,38 @@ class ResultScreenTest {
@Test
fun showResult() =
runComposeUiTest {
- val result = ResultModelFactory.build()
+ val item = ResultItem(
+ result = ResultModelFactory.build(),
+ network = null,
+ descriptor = DescriptorFactory.buildDescriptorWithInstalled(),
+ measurements = emptyList(),
+ )
+ var title: String? = null
setContent {
ResultScreen(
- state = ResultViewModel.State(result),
+ state = ResultViewModel.State(item),
onEvent = {},
)
+
+ title = item.descriptor.title()
}
- onNodeWithText(result.testGroupName!!).assertExists()
+ onNodeWithText(title!!).assertExists()
}
@Test
fun pressBack() =
runComposeUiTest {
val events = mutableListOf()
- val result = ResultModelFactory.build()
+ val item = ResultItem(
+ result = ResultModelFactory.build(),
+ network = null,
+ descriptor = DescriptorFactory.buildDescriptorWithInstalled(),
+ measurements = emptyList(),
+ )
setContent {
ResultScreen(
- state = ResultViewModel.State(result),
+ state = ResultViewModel.State(item),
onEvent = events::add,
)
}
diff --git a/composeApp/src/commonTest/kotlin/org/ooni/probe/ui/result/ResultViewModelTest.kt b/composeApp/src/commonTest/kotlin/org/ooni/probe/ui/result/ResultViewModelTest.kt
index 93b33a7a..11a33e9f 100644
--- a/composeApp/src/commonTest/kotlin/org/ooni/probe/ui/result/ResultViewModelTest.kt
+++ b/composeApp/src/commonTest/kotlin/org/ooni/probe/ui/result/ResultViewModelTest.kt
@@ -4,7 +4,9 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
+import org.ooni.probe.data.models.ResultItem
import org.ooni.probe.data.models.ResultModel
+import org.ooni.testing.factories.DescriptorFactory
import org.ooni.testing.factories.ResultModelFactory
import kotlin.test.Test
import kotlin.test.assertEquals
@@ -23,16 +25,21 @@ class ResultViewModelTest {
@Test
fun getResult() =
runTest {
- val result = ResultModelFactory.build()
- val viewModel = buildViewModel(getResult = { flowOf(result) })
+ val item = ResultItem(
+ result = ResultModelFactory.build(),
+ network = null,
+ descriptor = DescriptorFactory.buildDescriptorWithInstalled(),
+ measurements = emptyList(),
+ )
+ val viewModel = buildViewModel(getResult = { flowOf(item) })
- assertEquals(result, viewModel.state.first().result)
+ assertEquals(item, viewModel.state.first().result)
}
private fun buildViewModel(
resultId: ResultModel.Id = ResultModel.Id(1234),
onBack: () -> Unit = {},
- getResult: (ResultModel.Id) -> Flow = { flowOf(null) },
+ getResult: (ResultModel.Id) -> Flow = { flowOf(null) },
) = ResultViewModel(
resultId = resultId,
onBack = onBack,
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 1542deff..10e906f5 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
@@ -5,6 +5,7 @@ import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.runComposeUiTest
import kotlinx.datetime.LocalDate
import org.ooni.probe.data.models.ResultListItem
+import org.ooni.testing.factories.DescriptorFactory
import org.ooni.testing.factories.NetworkModelFactory
import org.ooni.testing.factories.ResultModelFactory
import kotlin.test.Test
@@ -14,28 +15,31 @@ class ResultsScreenTest {
@Test
fun showResults() =
runComposeUiTest {
- val item =
- ResultListItem(
- result = ResultModelFactory.build(),
- network = NetworkModelFactory.build(),
- measurementsCount = 4,
- )
+ val item = ResultListItem(
+ result = ResultModelFactory.build(),
+ descriptor = DescriptorFactory.buildDescriptorWithInstalled(),
+ network = NetworkModelFactory.build(),
+ measurementsCount = 4,
+ allMeasurementsUploaded = false,
+ )
+ var title: String? = null
+
setContent {
ResultsScreen(
- state =
- ResultsViewModel.State(
- results =
- mapOf(
- LocalDate(2024, 1, 1) to listOf(item),
- ),
- isLoading = false,
+ state = ResultsViewModel.State(
+ results = mapOf(
+ LocalDate(2024, 1, 1) to listOf(item),
),
+ isLoading = false,
+ ),
onEvent = {},
)
+
+ title = item.descriptor.title()
}
- onNodeWithText("2024-01").assertExists()
- onNodeWithText(item.result.testGroupName!!).assertExists()
+ onNodeWithText("January 2024").assertExists()
+ onNodeWithText(title!!).assertExists()
onNodeWithText(item.network!!.networkName!!).assertExists()
}
@@ -43,27 +47,30 @@ class ResultsScreenTest {
fun recordClick() =
runComposeUiTest {
val events = mutableListOf()
- val item =
- ResultListItem(
- result = ResultModelFactory.build(),
- network = NetworkModelFactory.build(),
- measurementsCount = 4,
- )
+ val item = ResultListItem(
+ result = ResultModelFactory.build(),
+ descriptor = DescriptorFactory.buildDescriptorWithInstalled(),
+ network = NetworkModelFactory.build(),
+ measurementsCount = 4,
+ allMeasurementsUploaded = false,
+ )
+ var title: String? = null
+
setContent {
ResultsScreen(
- state =
- ResultsViewModel.State(
- results =
- mapOf(
- LocalDate(2024, 1, 1) to listOf(item),
- ),
- isLoading = false,
+ state = ResultsViewModel.State(
+ results = mapOf(
+ LocalDate(2024, 1, 1) to listOf(item),
),
+ isLoading = false,
+ ),
onEvent = events::add,
)
+
+ title = item.descriptor.title()
}
- onNodeWithText(item.result.testGroupName!!).performClick()
+ onNodeWithText(title!!).performClick()
assertEquals(1, events.size)
assertEquals(ResultsViewModel.Event.ResultClick(item), events.first())
}