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