diff --git a/composeApp/src/commonMain/composeResources/drawable/.gitignore b/composeApp/src/commonMain/composeResources/drawable/.gitignore
index 4d576f73..f9d23428 100644
--- a/composeApp/src/commonMain/composeResources/drawable/.gitignore
+++ b/composeApp/src/commonMain/composeResources/drawable/.gitignore
@@ -1,2 +1,7 @@
-logo.xml
\ No newline at end of file
+logo.xml
+test_experimental.xml
+test_performance.xml
+test_instant_messaging.xml
+test_websites.xml
+test_circumvention.xml
\ No newline at end of file
diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_chevron_right.xml b/composeApp/src/commonMain/composeResources/drawable/ic_chevron_right.xml
new file mode 100644
index 00000000..41195f17
--- /dev/null
+++ b/composeApp/src/commonMain/composeResources/drawable/ic_chevron_right.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/composeApp/src/commonMain/composeResources/values/strings-common.xml b/composeApp/src/commonMain/composeResources/values/strings-common.xml
index 272df4c9..0e77e0f4 100644
--- a/composeApp/src/commonMain/composeResources/values/strings-common.xml
+++ b/composeApp/src/commonMain/composeResources/values/strings-common.xml
@@ -4,7 +4,11 @@
Dashboard
Settings
+ OONI Tests
+ OONI Run Links
+
Test Results
Test Results
Unknown
+ N/A
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/engine/Engine.kt b/composeApp/src/commonMain/kotlin/org/ooni/engine/Engine.kt
index 498f4715..f7550ee8 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/engine/Engine.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/engine/Engine.kt
@@ -16,7 +16,7 @@ import org.ooni.engine.models.TaskLogLevel
import org.ooni.engine.models.TaskOrigin
import org.ooni.engine.models.TaskSettings
import org.ooni.engine.models.resultOf
-import org.ooni.probe.config.Config
+import org.ooni.probe.config.org.ooni.probe.Config
import org.ooni.probe.shared.Platform
import org.ooni.probe.shared.PlatformInfo
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/DefaultTestDescriptor.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/DefaultTestDescriptor.kt
new file mode 100644
index 00000000..1fec1980
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/DefaultTestDescriptor.kt
@@ -0,0 +1,18 @@
+package org.ooni.probe.data.models
+
+import androidx.compose.ui.graphics.Color
+import org.jetbrains.compose.resources.DrawableResource
+import org.jetbrains.compose.resources.StringResource
+
+data class DefaultTestDescriptor(
+ val label: String,
+ val title: StringResource,
+ val shortDescription: StringResource,
+ val description: StringResource,
+ val icon: DrawableResource,
+ val color: Color,
+ val animation: String,
+ val dataUsage: StringResource,
+ var netTests: List,
+ var longRunningTests: List = emptyList(),
+)
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
new file mode 100644
index 00000000..11e28049
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/Descriptor.kt
@@ -0,0 +1,25 @@
+package org.ooni.probe.data.models
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+import org.jetbrains.compose.resources.DrawableResource
+
+data class Descriptor(
+ val name: String,
+ val title: @Composable () -> String,
+ val shortDescription: @Composable () -> String?,
+ val description: @Composable () -> String?,
+ val icon: DrawableResource?,
+ val color: Color?,
+ val animation: String?,
+ val dataUsage: @Composable () -> String?,
+ val netTests: List,
+ val longRunningTests: List? = null,
+ val source: Source,
+) {
+ sealed interface Source {
+ data class Default(val value: DefaultTestDescriptor) : Source
+
+ data class Installed(val value: InstalledTestDescriptorModel) : Source
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/TestDescriptorModel.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/InstalledTestDescriptorModel.kt
similarity index 87%
rename from composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/TestDescriptorModel.kt
rename to composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/InstalledTestDescriptorModel.kt
index 24da8f23..7cbd705f 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/TestDescriptorModel.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/InstalledTestDescriptorModel.kt
@@ -2,9 +2,9 @@ package org.ooni.probe.data.models
import kotlinx.datetime.Instant
-data class TestDescriptorModel(
+data class InstalledTestDescriptorModel(
val id: Id,
- val name: String?,
+ val name: String,
val shortDescription: String?,
val description: String?,
val author: String?,
@@ -19,7 +19,6 @@ data class TestDescriptorModel(
val dateCreated: Instant?,
val dateUpdated: Instant?,
val revision: String?,
- val previousRevision: String?,
val isExpired: Boolean,
val autoUpdate: Boolean,
) {
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/LocalizationString.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/LocalizationString.kt
index bc54578f..c0da51c2 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/LocalizationString.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/LocalizationString.kt
@@ -1,3 +1,7 @@
package org.ooni.probe.data.models
+import androidx.compose.ui.text.intl.Locale
+
typealias LocalizationString = Map
+
+fun LocalizationString.getCurrent() = get(Locale.current.language)
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/NetTest.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/NetTest.kt
index 5ff2ea2e..4c043d5c 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/NetTest.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/NetTest.kt
@@ -7,5 +7,5 @@ import kotlinx.serialization.Serializable
data class NetTest(
@SerialName("test_name")
val name: String,
- val inputs: List?,
+ val inputs: List? = null,
)
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/ResultModel.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/ResultModel.kt
index 4d6c827c..5650438a 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/ResultModel.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/ResultModel.kt
@@ -12,7 +12,7 @@ data class ResultModel(
val dataUsageDown: Long?,
val failureMessage: String?,
val networkId: NetworkModel.Id?,
- val testDescriptorId: TestDescriptorModel.Id?,
+ val testDescriptorId: InstalledTestDescriptorModel.Id?,
) {
data class Id(
val value: Long,
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 8c890972..9ba0d3bc 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
@@ -10,10 +10,10 @@ 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.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.TestDescriptorModel
import org.ooni.probe.shared.toEpoch
import org.ooni.probe.shared.toLocalDateTime
@@ -71,7 +71,7 @@ private fun Result.toModel(): ResultModel? {
dataUsageDown = data_usage_down,
failureMessage = failure_msg,
networkId = network_id?.let(NetworkModel::Id),
- testDescriptorId = descriptor_runId?.let(TestDescriptorModel::Id),
+ testDescriptorId = descriptor_runId?.let(InstalledTestDescriptorModel::Id),
)
}
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/TestDescriptorRepository.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/TestDescriptorRepository.kt
index 088c93e4..9023fef6 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/TestDescriptorRepository.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/TestDescriptorRepository.kt
@@ -10,7 +10,7 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.ooni.probe.Database
import org.ooni.probe.data.TestDescriptor
-import org.ooni.probe.data.models.TestDescriptorModel
+import org.ooni.probe.data.models.InstalledTestDescriptorModel
class TestDescriptorRepository(
private val database: Database,
@@ -24,7 +24,7 @@ class TestDescriptorRepository(
.mapToList(backgroundDispatcher)
.map { list -> list.mapNotNull { it.toModel() } }
- suspend fun create(model: TestDescriptorModel) {
+ suspend fun create(model: InstalledTestDescriptorModel) {
withContext(backgroundDispatcher) {
database.testDescriptorQueries.insert(
runId = model.id.value,
@@ -43,17 +43,17 @@ class TestDescriptorRepository(
date_created = model.dateCreated?.toEpochMilliseconds(),
date_updated = model.dateUpdated?.toEpochMilliseconds(),
revision = model.revision,
- previous_revision = model.previousRevision,
+ previous_revision = null,
is_expired = if (model.isExpired) 1 else 0,
auto_update = if (model.autoUpdate) 1 else 0,
)
}
}
- private fun TestDescriptor.toModel(): TestDescriptorModel? {
- return TestDescriptorModel(
- id = runId?.let(TestDescriptorModel::Id) ?: return null,
- name = name,
+ private fun TestDescriptor.toModel(): InstalledTestDescriptorModel? {
+ return InstalledTestDescriptorModel(
+ id = runId?.let(InstalledTestDescriptorModel::Id) ?: return null,
+ name = name.orEmpty(),
shortDescription = short_description,
description = description,
author = author,
@@ -68,7 +68,6 @@ class TestDescriptorRepository(
dateCreated = date_created?.let(Instant::fromEpochMilliseconds),
dateUpdated = date_updated?.let(Instant::fromEpochMilliseconds),
revision = revision,
- previousRevision = previous_revision,
isExpired = is_expired == 1L,
autoUpdate = auto_update == 1L,
)
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 7090a9ba..3fce7a7e 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt
@@ -12,8 +12,11 @@ import org.ooni.engine.TaskEventMapper
import org.ooni.probe.Database
import org.ooni.probe.data.models.ResultModel
import org.ooni.probe.data.repositories.ResultRepository
+import org.ooni.probe.data.repositories.TestDescriptorRepository
+import org.ooni.probe.domain.GetDefaultTestDescriptors
import org.ooni.probe.domain.GetResult
import org.ooni.probe.domain.GetResults
+import org.ooni.probe.domain.GetTestDescriptors
import org.ooni.probe.shared.PlatformInfo
import org.ooni.probe.ui.dashboard.DashboardViewModel
import org.ooni.probe.ui.result.ResultViewModel
@@ -36,6 +39,9 @@ class Dependencies(
private val json by lazy { buildJson() }
private val database by lazy { buildDatabase(databaseDriverFactory) }
private val resultRepository by lazy { ResultRepository(database, backgroundDispatcher) }
+ private val testDescriptorRepository by lazy {
+ TestDescriptorRepository(database, json, backgroundDispatcher)
+ }
// Engine
@@ -56,12 +62,24 @@ class Dependencies(
// Domain
+ private val getDefaultTestDescriptors by lazy { GetDefaultTestDescriptors() }
private val getResults by lazy { GetResults(resultRepository) }
private val getResult by lazy { GetResult(resultRepository) }
+ private val getTestDescriptors by lazy {
+ GetTestDescriptors(
+ getDefaultTestDescriptors = getDefaultTestDescriptors::invoke,
+ listInstalledTestDescriptors = testDescriptorRepository::list,
+ )
+ }
// ViewModels
- val dashboardViewModel get() = DashboardViewModel(engine)
+ val dashboardViewModel
+ get() =
+ DashboardViewModel(
+ engine = engine,
+ getTestDescriptors = getTestDescriptors::invoke,
+ )
fun resultsViewModel(goToResult: (ResultModel.Id) -> Unit) = ResultsViewModel(goToResult, getResults::invoke)
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetTestDescriptors.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetTestDescriptors.kt
new file mode 100644
index 00000000..cdbc6f25
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetTestDescriptors.kt
@@ -0,0 +1,59 @@
+package org.ooni.probe.domain
+
+import androidx.compose.ui.graphics.Color
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.asFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.map
+import org.jetbrains.compose.resources.stringResource
+import org.ooni.probe.data.models.DefaultTestDescriptor
+import org.ooni.probe.data.models.Descriptor
+import org.ooni.probe.data.models.InstalledTestDescriptorModel
+import org.ooni.probe.data.models.getCurrent
+
+class GetTestDescriptors(
+ private val getDefaultTestDescriptors: () -> List,
+ private val listInstalledTestDescriptors: () -> Flow>,
+) {
+ operator fun invoke(): Flow> {
+ return suspend {
+ getDefaultTestDescriptors()
+ .map { it.toDescriptor() }
+ }.asFlow()
+ .flatMapLatest { defaultDescriptors ->
+ listInstalledTestDescriptors()
+ .map { list -> list.map { it.toDescriptor() } }
+ .map { defaultDescriptors + it }
+ }
+ }
+
+ private fun DefaultTestDescriptor.toDescriptor() =
+ Descriptor(
+ name = label,
+ title = { stringResource(title) },
+ shortDescription = { stringResource(shortDescription) },
+ description = { stringResource(description) },
+ icon = icon,
+ color = color,
+ animation = animation,
+ dataUsage = { stringResource(dataUsage) },
+ netTests = netTests,
+ longRunningTests = longRunningTests,
+ source = Descriptor.Source.Default(this),
+ )
+
+ private fun InstalledTestDescriptorModel.toDescriptor() =
+ Descriptor(
+ name = name,
+ title = { nameIntl?.getCurrent() ?: name },
+ shortDescription = { shortDescriptionIntl?.getCurrent() ?: shortDescription },
+ description = { descriptionIntl?.getCurrent() ?: description },
+ // TODO: fetch drawable resource from path
+ icon = null,
+ color = color?.filter { it != '#' }?.toIntOrNull()?.let { Color(it) },
+ animation = animation,
+ dataUsage = { null },
+ netTests = netTests.orEmpty(),
+ source = Descriptor.Source.Installed(this),
+ )
+}
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 c014ef46..8a67eec2 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
@@ -4,6 +4,8 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
@@ -49,6 +51,19 @@ fun DashboardScreen(
Text(stringResource(Res.string.run_tests))
}
+ LazyColumn {
+ state.tests.forEach { (type, tests) ->
+ if (state.tests.keys.size > 1 && tests.isNotEmpty()) {
+ item(type) {
+ TestDescriptorItem(type)
+ }
+ }
+ items(tests) { test ->
+ TestDescriptorItem(test)
+ }
+ }
+ }
+
Text(
text = state.log,
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/DashboardViewModel.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/DashboardViewModel.kt
index 76d788bc..6589409b 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/DashboardViewModel.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/DashboardViewModel.kt
@@ -3,6 +3,7 @@ package org.ooni.probe.ui.dashboard
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -16,9 +17,11 @@ import kotlinx.coroutines.flow.update
import org.ooni.engine.Engine
import org.ooni.engine.models.TaskEvent
import org.ooni.engine.models.TaskOrigin
+import org.ooni.probe.data.models.Descriptor
class DashboardViewModel(
private val engine: Engine,
+ getTestDescriptors: () -> Flow>,
) : ViewModel() {
private val events = MutableSharedFlow(extraBufferCapacity = 1)
@@ -26,49 +29,16 @@ class DashboardViewModel(
val state = _state.asStateFlow()
init {
+ getTestDescriptors()
+ .onEach { tests ->
+ _state.update { it.copy(tests = tests.groupByType()) }
+ }
+ .launchIn(viewModelScope)
+
events
.flatMapLatest { event ->
when (event) {
- Event.StartClick -> {
- if (_state.value.isRunning) return@flatMapLatest emptyFlow()
-
- _state.value = _state.value.copy(isRunning = true)
-
- engine.httpDo(
- method = "GET",
- url = "https://api.dev.ooni.io/api/v2/oonirun/links/10426",
- )
- .onSuccess { Logger.d(it.orEmpty()) }
- .onFailure { Logger.e("httpDo failed", it) }
-
- val checkInResults =
- engine.checkIn(
- categories = listOf("NEWS"),
- taskOrigin = TaskOrigin.OoniRun,
- )
- .onFailure { Logger.e("checkIn failed", it) }
- .get() ?: return@flatMapLatest emptyFlow()
-
- engine
- .startTask(
- name = "web_connectivity",
- inputs = checkInResults.urls.map { it.url },
- taskOrigin = TaskOrigin.OoniRun,
- ).onEach { taskEvent ->
- _state.update { state ->
- // Can't print the Measurement event,
- // it's too long and halts the main thread
- if (taskEvent is TaskEvent.Measurement) return@update state
-
- state.copy(log = state.log + "\n" + taskEvent)
- }
- }.onCompletion {
- _state.update { it.copy(isRunning = false) }
- }
- .catch {
- Logger.e("startTask failed", it)
- }
- }
+ Event.StartClick -> runTest()
}
}.launchIn(viewModelScope)
}
@@ -77,7 +47,60 @@ class DashboardViewModel(
events.tryEmit(event)
}
+ private fun List.groupByType() =
+ mapOf(
+ DescriptorType.Default to filter { it.source is Descriptor.Source.Default },
+ DescriptorType.Installed to filter { it.source is Descriptor.Source.Installed },
+ )
+
+ private suspend fun runTest(): Flow {
+ if (_state.value.isRunning) return emptyFlow()
+
+ _state.value = _state.value.copy(isRunning = true)
+
+ engine.httpDo(
+ method = "GET",
+ url = "https://api.dev.ooni.io/api/v2/oonirun/links/10426",
+ )
+ .onSuccess { Logger.d(it.orEmpty()) }
+ .onFailure { Logger.e("httpDo failed", it) }
+
+ val checkInResults =
+ engine.checkIn(
+ categories = listOf("NEWS"),
+ taskOrigin = TaskOrigin.OoniRun,
+ )
+ .onFailure { Logger.e("checkIn failed", it) }
+ .get() ?: return emptyFlow()
+
+ return engine
+ .startTask(
+ name = "web_connectivity",
+ inputs = checkInResults.urls.map { it.url },
+ taskOrigin = TaskOrigin.OoniRun,
+ ).onEach { taskEvent ->
+ _state.update { state ->
+ // Can't print the Measurement event,
+ // it's too long and halts the main thread
+ if (taskEvent is TaskEvent.Measurement) return@update state
+
+ state.copy(log = state.log + "\n" + taskEvent)
+ }
+ }.onCompletion {
+ _state.update { it.copy(isRunning = false) }
+ }
+ .catch {
+ Logger.e("startTask failed", it)
+ }
+ }
+
+ enum class DescriptorType {
+ Default,
+ Installed,
+ }
+
data class State(
+ val tests: Map> = emptyMap(),
val isRunning: Boolean = false,
val log: String = "",
)
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
new file mode 100644
index 00000000..722ef0c5
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/TestDescriptorItem.kt
@@ -0,0 +1,69 @@
+package org.ooni.probe.ui.dashboard
+
+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
+import androidx.compose.material3.Text
+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.ic_chevron_right
+import ooniprobe.composeapp.generated.resources.logo
+import org.jetbrains.compose.resources.painterResource
+import org.ooni.probe.data.models.Descriptor
+
+@Composable
+fun TestDescriptorItem(descriptor: Descriptor) {
+ Card(
+ Modifier
+ .padding(horizontal = 16.dp, vertical = 4.dp),
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .padding(8.dp),
+ ) {
+ Column(
+ modifier = Modifier.weight(1f),
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(bottom = 2.dp),
+ ) {
+ Icon(
+ painter = painterResource(descriptor.icon ?: Res.drawable.logo),
+ contentDescription = null,
+ tint = descriptor.color ?: MaterialTheme.colorScheme.onSurface,
+ modifier =
+ Modifier
+ .size(24.dp)
+ .padding(end = 4.dp),
+ )
+ Text(
+ descriptor.title(),
+ style = MaterialTheme.typography.titleLarge,
+ )
+ }
+ descriptor.shortDescription()?.let { shortDescription ->
+ Text(
+ shortDescription,
+ style = MaterialTheme.typography.bodySmall,
+ )
+ }
+ }
+ Icon(
+ painter = painterResource(Res.drawable.ic_chevron_right),
+ contentDescription = null,
+ )
+ }
+ }
+}
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
new file mode 100644
index 00000000..390cf112
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/TestDescriptorLabel.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 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),
+ )
+}
diff --git a/composeApp/src/commonTest/kotlin/org/ooni/probe/data/repositories/TestDescriptorRepositoryTest.kt b/composeApp/src/commonTest/kotlin/org/ooni/probe/data/repositories/TestDescriptorRepositoryTest.kt
index 41ce9526..236b6925 100644
--- a/composeApp/src/commonTest/kotlin/org/ooni/probe/data/repositories/TestDescriptorRepositoryTest.kt
+++ b/composeApp/src/commonTest/kotlin/org/ooni/probe/data/repositories/TestDescriptorRepositoryTest.kt
@@ -6,7 +6,7 @@ import kotlinx.coroutines.test.runTest
import org.ooni.probe.data.models.NetTest
import org.ooni.probe.di.Dependencies
import org.ooni.testing.createTestDatabaseDriver
-import org.ooni.testing.factories.TestDescriptorModelFactory
+import org.ooni.testing.factories.DescriptorFactory
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
@@ -28,7 +28,7 @@ class TestDescriptorRepositoryTest {
fun createAndGet() =
runTest {
val model =
- TestDescriptorModelFactory.build(
+ DescriptorFactory.buildInstalledModel(
netTests =
listOf(
NetTest("web_connectivity", inputs = listOf("https://ooni.org")),
diff --git a/composeApp/src/commonTest/kotlin/org/ooni/probe/ui/dashboard/DashboardScreenTest.kt b/composeApp/src/commonTest/kotlin/org/ooni/probe/ui/dashboard/DashboardScreenTest.kt
new file mode 100644
index 00000000..bea86f9a
--- /dev/null
+++ b/composeApp/src/commonTest/kotlin/org/ooni/probe/ui/dashboard/DashboardScreenTest.kt
@@ -0,0 +1,28 @@
+package org.ooni.probe.ui.dashboard
+
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.runComposeUiTest
+import org.ooni.testing.factories.DescriptorFactory
+import kotlin.test.Test
+
+class DashboardScreenTest {
+ @Test
+ fun showTestDescriptors() =
+ runComposeUiTest {
+ val descriptor = DescriptorFactory.buildDescriptorWithInstalled()
+ lateinit var title: String
+
+ setContent {
+ DashboardScreen(
+ state =
+ DashboardViewModel.State(
+ tests = mapOf(DashboardViewModel.DescriptorType.Installed to listOf(descriptor)),
+ ),
+ onEvent = {},
+ )
+ title = descriptor.title()
+ }
+
+ onNodeWithText(title).assertExists()
+ }
+}
diff --git a/composeApp/src/commonTest/kotlin/org/ooni/testing/factories/TestDescriptorModelFactory.kt b/composeApp/src/commonTest/kotlin/org/ooni/testing/factories/DescriptorFactory.kt
similarity index 56%
rename from composeApp/src/commonTest/kotlin/org/ooni/testing/factories/TestDescriptorModelFactory.kt
rename to composeApp/src/commonTest/kotlin/org/ooni/testing/factories/DescriptorFactory.kt
index e56a22e8..47a6aa16 100644
--- a/composeApp/src/commonTest/kotlin/org/ooni/testing/factories/TestDescriptorModelFactory.kt
+++ b/composeApp/src/commonTest/kotlin/org/ooni/testing/factories/DescriptorFactory.kt
@@ -1,17 +1,46 @@
package org.ooni.testing.factories
+import androidx.compose.ui.graphics.Color
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
+import org.jetbrains.compose.resources.DrawableResource
+import org.ooni.probe.data.models.Descriptor
+import org.ooni.probe.data.models.InstalledTestDescriptorModel
import org.ooni.probe.data.models.LocalizationString
import org.ooni.probe.data.models.NetTest
-import org.ooni.probe.data.models.TestDescriptorModel
import kotlin.math.absoluteValue
import kotlin.random.Random
-object TestDescriptorModelFactory {
- fun build(
- id: TestDescriptorModel.Id = TestDescriptorModel.Id(Random.nextLong().absoluteValue),
- name: String? = "Test",
+object DescriptorFactory {
+ fun buildDescriptorWithInstalled(
+ name: String = "test",
+ title: String = "Test",
+ shortDescription: String? = null,
+ description: String? = null,
+ icon: DrawableResource? = null,
+ color: Color? = null,
+ animation: String? = null,
+ dataUsage: String? = null,
+ netTests: List = emptyList(),
+ longRunningTests: List? = null,
+ installedTestDescriptorModel: InstalledTestDescriptorModel = buildInstalledModel(),
+ ) = Descriptor(
+ name = name,
+ title = { title },
+ shortDescription = { shortDescription },
+ description = { description },
+ icon = icon,
+ color = color,
+ animation = animation,
+ dataUsage = { dataUsage },
+ netTests = netTests,
+ longRunningTests = longRunningTests,
+ source = Descriptor.Source.Installed(installedTestDescriptorModel),
+ )
+
+ fun buildInstalledModel(
+ id: InstalledTestDescriptorModel.Id = InstalledTestDescriptorModel.Id(Random.nextLong().absoluteValue),
+ name: String = "Test",
shortDescription: String? = null,
description: String? = null,
author: String? = null,
@@ -31,11 +60,10 @@ object TestDescriptorModelFactory {
),
dateUpdated: Instant? = null,
revision: String? = null,
- previousRevision: String? = null,
isExpired: Boolean = false,
autoUpdate: Boolean = false,
) =
- TestDescriptorModel(
+ InstalledTestDescriptorModel(
id = id,
name = name,
shortDescription = shortDescription,
@@ -52,7 +80,6 @@ object TestDescriptorModelFactory {
dateCreated = dateCreated,
dateUpdated = dateUpdated,
revision = revision,
- previousRevision = previousRevision,
isExpired = isExpired,
autoUpdate = autoUpdate,
)
diff --git a/composeApp/src/commonTest/kotlin/org/ooni/testing/factories/ResultModelFactory.kt b/composeApp/src/commonTest/kotlin/org/ooni/testing/factories/ResultModelFactory.kt
index 6adacaf1..3a511e82 100644
--- a/composeApp/src/commonTest/kotlin/org/ooni/testing/factories/ResultModelFactory.kt
+++ b/composeApp/src/commonTest/kotlin/org/ooni/testing/factories/ResultModelFactory.kt
@@ -3,9 +3,9 @@ package org.ooni.testing.factories
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.atTime
+import org.ooni.probe.data.models.InstalledTestDescriptorModel
import org.ooni.probe.data.models.NetworkModel
import org.ooni.probe.data.models.ResultModel
-import org.ooni.probe.data.models.TestDescriptorModel
import org.ooni.probe.shared.today
object ResultModelFactory {
@@ -19,7 +19,7 @@ object ResultModelFactory {
dataUsageDown: Long? = null,
failureMessage: String? = null,
networkId: NetworkModel.Id? = null,
- testDescriptorId: TestDescriptorModel.Id? = null,
+ testDescriptorId: InstalledTestDescriptorModel.Id? = null,
) = ResultModel(
id = id,
testGroupName = testGroupName,
diff --git a/composeApp/src/dwMain/kotlin/Config.kt b/composeApp/src/dwMain/kotlin/org/ooni/probe/Config.kt
similarity index 100%
rename from composeApp/src/dwMain/kotlin/Config.kt
rename to composeApp/src/dwMain/kotlin/org/ooni/probe/Config.kt
diff --git a/composeApp/src/dwMain/kotlin/org/ooni/probe/domain/GetDefaultTestDescriptors.kt b/composeApp/src/dwMain/kotlin/org/ooni/probe/domain/GetDefaultTestDescriptors.kt
new file mode 100644
index 00000000..90e38c84
--- /dev/null
+++ b/composeApp/src/dwMain/kotlin/org/ooni/probe/domain/GetDefaultTestDescriptors.kt
@@ -0,0 +1,7 @@
+package org.ooni.probe.domain
+
+import org.ooni.probe.data.models.DefaultTestDescriptor
+
+class GetDefaultTestDescriptors {
+ operator fun invoke(): List = emptyList()
+}
diff --git a/composeApp/src/ooniMain/kotlin/Config.kt b/composeApp/src/ooniMain/kotlin/org/ooni/probe/Config.kt
similarity index 82%
rename from composeApp/src/ooniMain/kotlin/Config.kt
rename to composeApp/src/ooniMain/kotlin/org/ooni/probe/Config.kt
index e511cc05..e2ff084f 100644
--- a/composeApp/src/ooniMain/kotlin/Config.kt
+++ b/composeApp/src/ooniMain/kotlin/org/ooni/probe/Config.kt
@@ -1,4 +1,4 @@
-package org.ooni.probe.config
+package org.ooni.probe.config.org.ooni.probe
object Config {
const val OONI_API_BASE_URL: String = "https://api.prod.ooni.io"
diff --git a/composeApp/src/ooniMain/kotlin/org/ooni/probe/domain/GetDefaultTestDescriptors.kt b/composeApp/src/ooniMain/kotlin/org/ooni/probe/domain/GetDefaultTestDescriptors.kt
new file mode 100644
index 00000000..06e69d77
--- /dev/null
+++ b/composeApp/src/ooniMain/kotlin/org/ooni/probe/domain/GetDefaultTestDescriptors.kt
@@ -0,0 +1,138 @@
+package org.ooni.probe.domain
+
+import androidx.compose.ui.graphics.Color
+import ooniprobe.composeapp.generated.resources.Dashboard_Circumvention_Card_Description
+import ooniprobe.composeapp.generated.resources.Dashboard_Circumvention_Overview_Paragraph
+import ooniprobe.composeapp.generated.resources.Dashboard_Experimental_Card_Description
+import ooniprobe.composeapp.generated.resources.Dashboard_Experimental_Overview_Paragraph
+import ooniprobe.composeapp.generated.resources.Dashboard_InstantMessaging_Card_Description
+import ooniprobe.composeapp.generated.resources.Dashboard_InstantMessaging_Overview_Paragraph
+import ooniprobe.composeapp.generated.resources.Dashboard_Performance_Card_Description
+import ooniprobe.composeapp.generated.resources.Dashboard_Performance_Overview_Paragraph
+import ooniprobe.composeapp.generated.resources.Dashboard_Websites_Card_Description
+import ooniprobe.composeapp.generated.resources.Dashboard_Websites_Overview_Paragraph
+import ooniprobe.composeapp.generated.resources.Res
+import ooniprobe.composeapp.generated.resources.TestResults_NotAvailable
+import ooniprobe.composeapp.generated.resources.Test_Circumvention_Fullname
+import ooniprobe.composeapp.generated.resources.Test_Experimental_Fullname
+import ooniprobe.composeapp.generated.resources.Test_InstantMessaging_Fullname
+import ooniprobe.composeapp.generated.resources.Test_Performance_Fullname
+import ooniprobe.composeapp.generated.resources.Test_Websites_Fullname
+import ooniprobe.composeapp.generated.resources.performance_datausage
+import ooniprobe.composeapp.generated.resources.small_datausage
+import ooniprobe.composeapp.generated.resources.test_circumvention
+import ooniprobe.composeapp.generated.resources.test_experimental
+import ooniprobe.composeapp.generated.resources.test_instant_messaging
+import ooniprobe.composeapp.generated.resources.test_performance
+import ooniprobe.composeapp.generated.resources.test_websites
+import ooniprobe.composeapp.generated.resources.websites_datausage
+import org.ooni.probe.data.models.DefaultTestDescriptor
+import org.ooni.probe.data.models.NetTest
+
+class GetDefaultTestDescriptors {
+ operator fun invoke(): List =
+ listOf(
+ WEBSITES,
+ INSTANT_MESSAGING,
+ CIRCUMVENTION,
+ PERFORMANCE,
+ EXPERIMENTAL,
+ )
+
+ companion object {
+ private val WEBSITES =
+ DefaultTestDescriptor(
+ label = "websites",
+ title = Res.string.Test_Websites_Fullname,
+ shortDescription = Res.string.Dashboard_Websites_Card_Description,
+ description = Res.string.Dashboard_Websites_Overview_Paragraph,
+ icon = Res.drawable.test_websites,
+ color = Color(0xFF4c6ef5),
+ animation = "anim/websites.json",
+ dataUsage = Res.string.websites_datausage,
+ netTests =
+ listOf(
+ NetTest(name = "web_connectivity"),
+ ),
+ )
+
+ private val INSTANT_MESSAGING =
+ DefaultTestDescriptor(
+ label = "instant_messaging",
+ title = Res.string.Test_InstantMessaging_Fullname,
+ shortDescription = Res.string.Dashboard_InstantMessaging_Card_Description,
+ description = Res.string.Dashboard_InstantMessaging_Overview_Paragraph,
+ icon = Res.drawable.test_instant_messaging,
+ color = Color(0xFF15aabf),
+ animation = "anim/instant_messaging.json",
+ dataUsage = Res.string.small_datausage,
+ netTests =
+ listOf(
+ NetTest(name = "whatsapp"),
+ NetTest(name = "telegram"),
+ NetTest(name = "facebook_messenger"),
+ NetTest(name = "signal"),
+ ),
+ )
+
+ private val CIRCUMVENTION =
+ DefaultTestDescriptor(
+ label = "circumvention",
+ title = Res.string.Test_Circumvention_Fullname,
+ shortDescription = Res.string.Dashboard_Circumvention_Card_Description,
+ description = Res.string.Dashboard_Circumvention_Overview_Paragraph,
+ icon = Res.drawable.test_circumvention,
+ color = Color(0xFFe64980),
+ animation = "anim/circumvention.json",
+ dataUsage = Res.string.small_datausage,
+ netTests =
+ listOf(
+ NetTest(name = "psiphon"),
+ NetTest(name = "tor"),
+ ),
+ )
+
+ private val PERFORMANCE =
+ DefaultTestDescriptor(
+ label = "performance",
+ title = Res.string.Test_Performance_Fullname,
+ shortDescription = Res.string.Dashboard_Performance_Card_Description,
+ description = Res.string.Dashboard_Performance_Overview_Paragraph,
+ icon = Res.drawable.test_performance,
+ color = Color(0xFFbe4bdb),
+ animation = "anim/performance.json",
+ dataUsage = Res.string.performance_datausage,
+ netTests =
+ listOf(
+ NetTest(name = "ndt"),
+ NetTest(name = "dash"),
+ NetTest(name = "http_header_field_manipulation"),
+ NetTest(name = "http_invalid_request_line"),
+ ),
+ )
+
+ private val EXPERIMENTAL =
+ DefaultTestDescriptor(
+ label = "experimental",
+ title = Res.string.Test_Experimental_Fullname,
+ shortDescription = Res.string.Dashboard_Experimental_Card_Description,
+ description = Res.string.Dashboard_Experimental_Overview_Paragraph,
+ icon = Res.drawable.test_experimental,
+ color = Color(0xFF495057),
+ animation = "anim/experimental.json",
+ dataUsage = Res.string.TestResults_NotAvailable,
+ netTests =
+ listOf(
+ NetTest(name = "stunreachability"),
+ NetTest(name = "dnscheck"),
+ NetTest(name = "riseupvpn"),
+ NetTest(name = "echcheck"),
+ ),
+ longRunningTests =
+ listOf(
+ NetTest(name = "torsf"),
+ NetTest(name = "vanilla_tor"),
+ ),
+ )
+ }
+}
diff --git a/composeApp/src/ooniMain/resources/drawable/test_circumvention.xml b/composeApp/src/ooniMain/resources/drawable/test_circumvention.xml
new file mode 100644
index 00000000..88b86e1a
--- /dev/null
+++ b/composeApp/src/ooniMain/resources/drawable/test_circumvention.xml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/composeApp/src/ooniMain/resources/drawable/test_experimental.xml b/composeApp/src/ooniMain/resources/drawable/test_experimental.xml
new file mode 100644
index 00000000..0f39bc33
--- /dev/null
+++ b/composeApp/src/ooniMain/resources/drawable/test_experimental.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/composeApp/src/ooniMain/resources/drawable/test_instant_messaging.xml b/composeApp/src/ooniMain/resources/drawable/test_instant_messaging.xml
new file mode 100644
index 00000000..3d6b6672
--- /dev/null
+++ b/composeApp/src/ooniMain/resources/drawable/test_instant_messaging.xml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/composeApp/src/ooniMain/resources/drawable/test_performance.xml b/composeApp/src/ooniMain/resources/drawable/test_performance.xml
new file mode 100644
index 00000000..4c2c5916
--- /dev/null
+++ b/composeApp/src/ooniMain/resources/drawable/test_performance.xml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/composeApp/src/ooniMain/resources/drawable/test_websites.xml b/composeApp/src/ooniMain/resources/drawable/test_websites.xml
new file mode 100644
index 00000000..d86e92a0
--- /dev/null
+++ b/composeApp/src/ooniMain/resources/drawable/test_websites.xml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/composeApp/src/ooniMain/resources/values/strings-organization.xml b/composeApp/src/ooniMain/resources/values/strings-organization.xml
index 96cb7ce4..4504147e 100644
--- a/composeApp/src/ooniMain/resources/values/strings-organization.xml
+++ b/composeApp/src/ooniMain/resources/values/strings-organization.xml
@@ -1,3 +1,27 @@
OONI Probe
+
+ Websites
+ Instant Messaging
+ Middleboxes
+ Performance
+ Circumvention
+ Experimental
+
+ Test the blocking of websites
+ Check whether websites are blocked using OONI\'s [Web Connectivity test](https://ooni.org/nettest/web-connectivity/).\n\nEvery time you tap Run, you test different websites from the Citizen Lab\'s [global](https://github.com/citizenlab/test-lists/blob/master/lists/global.csv) and [country-specific](https://github.com/citizenlab/test-lists/tree/master/lists) test lists.\n\nTo test the sites of your choice, tap the Choose websites button or select categories of sites via the settings of this card. \n\nThis test measures whether websites are blocked by means of DNS tampering, TCP/IP blocking or by a transparent HTTP proxy.\n\nYour results will be published on [OONI Explorer](https://explorer.ooni.org/world/) and [OONI API](https://api.ooni.io/).
+ Test your network speed and performance
+ Measure the speed and performance of your network using the [NDT](https://ooni.org/nettest/ndt/) test.\n\nMeasure video streaming performance using the [DASH](https://ooni.org/nettest/dash/) test.\n\nThese tests consume data depending on your network speed.\n\nYour results will be published on [OONI Explorer](https://explorer.ooni.org/world/) and [OONI API](https://api.ooni.io/).\n\nDisclaimer: These tests rely on third party servers. We therefore cannot guarantee that your IP address will not be collected.
+ Detect middleboxes in your network
+ Internet Service Providers often use network appliances (middleboxes) for various networking purposes (such as caching). Sometimes these middleboxes are used to implement internet censorship and/or surveillance.\n\nFind middleboxes in your network using OONI\'s [HTTP Invalid Request Line](https://ooni.org/nettest/http-invalid-request-line/) and [HTTP Header Field Manipulation](https://ooni.org/nettest/http-header-field-manipulation/) tests.\n\nYour results will be published on [OONI Explorer](https://explorer.ooni.org/world/) and [OONI API](https://api.ooni.io/).
+ Test the blocking of instant messaging apps
+ Check whether [WhatsApp](https://ooni.org/nettest/whatsapp/), [Facebook Messenger](https://ooni.org/nettest/facebook-messenger/), [Telegram](https://ooni.org/nettest/telegram/), and [Signal](https://ooni.org/nettest/signal) are blocked.\n\nYour results will be published on [OONI Explorer](https://explorer.ooni.org/world/) and [OONI API](https://api.ooni.io/).
+ Test the blocking of censorship circumvention tools
+ Check whether [Psiphon](https://ooni.org/nettest/psiphon/), [Tor](https://ooni.org/nettest/tor/) or [RiseupVPN](https://ooni.org/nettest/riseupvpn/) are blocked.\n\nYour results will be published on [OONI Explorer](https://explorer.ooni.org/) and [OONI API](https://api.ooni.io/).
+ Run new experimental tests
+ Run the following new experimental tests developed by the OONI team:\n%1$s\n\nYour results will be published on [OONI Explorer](https://explorer.ooni.org/) and [OONI API](https://api.ooni.io/).
+
+ ~ 8 MB
+ < 1 MB
+ 5 - 200 MB