Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show measurement #71

Merged
merged 2 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,9 @@ kotlin {
all {
languageSettings {
optIn("kotlin.ExperimentalStdlibApi")
optIn("kotlinx.cinterop.ExperimentalForeignApi")
optIn("kotlin.io.encoding.ExperimentalEncodingApi")
optIn("kotlinx.cinterop.BetaInteropApi")
optIn("kotlinx.cinterop.ExperimentalForeignApi")
optIn("kotlinx.coroutines.ExperimentalCoroutinesApi")
optIn("androidx.compose.foundation.ExperimentalFoundationApi")
optIn("androidx.compose.material3.ExperimentalMaterial3Api")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.ooni.probe.ui.shared

import android.app.Activity
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat

@Composable
actual fun LightStatusBars(value: Boolean) {
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = value
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@

<!-- New Strings -->
<string name="back">Back</string>
<string name="refresh">refresh</string>
<string name="measurement">Measurement</string>
<plurals name="measurements_count">
<item quantity="one">%1$d measurement</item>
<item quantity="other">%1$d measurements</item>
Expand Down
2 changes: 1 addition & 1 deletion composeApp/src/commonMain/kotlin/org/ooni/engine/Engine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ class Engine(
options =
TaskSettings.Options(
// TODO: fetch from preferences
noCollector = true,
noCollector = false,
softwareName = buildSoftwareName(taskOrigin),
softwareVersion = platformInfo.version,
maxRuntime = maxRuntime?.inWholeSeconds?.toInt() ?: -1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ data class MeasurementModel(
val uploadFailureMessage: String? = null,
val isRerun: Boolean = false,
val isAnomaly: Boolean = false,
val reportId: String?,
val reportId: ReportId?,
val testKeys: String? = null,
val rerunNetwork: String? = null,
val urlId: UrlModel.Id?,
Expand All @@ -29,6 +29,10 @@ data class MeasurementModel(
val value: Long,
)

data class ReportId(
val value: String,
)

val idOrThrow get() = id ?: throw IllegalStateException("Id no available")

val logFilePath: Path
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class MeasurementRepository(
upload_failure_msg = model.uploadFailureMessage,
is_rerun = if (model.isRerun) 1 else 0,
is_anomaly = if (model.isAnomaly) 1 else 0,
report_id = model.reportId,
report_id = model.reportId?.value,
test_keys = model.testKeys,
rerun_network = model.rerunNetwork,
url_id = model.urlId?.value,
Expand All @@ -79,7 +79,7 @@ class MeasurementRepository(
uploadFailureMessage = upload_failure_msg,
isRerun = is_rerun == 1L,
isAnomaly = is_anomaly == 1L,
reportId = report_id,
reportId = report_id?.let(MeasurementModel::ReportId),
testKeys = test_keys,
rerunNetwork = rerun_network,
urlId = url_id?.let(UrlModel::Id),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import org.ooni.probe.data.disk.DeleteFile
import org.ooni.probe.data.disk.DeleteFileOkio
import org.ooni.probe.data.disk.WriteFile
import org.ooni.probe.data.disk.WriteFileOkio
import org.ooni.probe.data.models.MeasurementModel
import org.ooni.probe.data.models.PreferenceCategoryKey
import org.ooni.probe.data.models.ResultModel
import org.ooni.probe.data.models.SettingsCategoryItem
Expand Down Expand Up @@ -201,7 +202,8 @@ class Dependencies(
fun resultViewModel(
resultId: ResultModel.Id,
onBack: () -> Unit,
) = ResultViewModel(resultId, onBack, getResult::invoke)
goToMeasurement: (MeasurementModel.ReportId, String?) -> Unit,
) = ResultViewModel(resultId, onBack, goToMeasurement, getResult::invoke)

companion object {
@VisibleForTesting
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class DownloadUrls(
suspend operator fun invoke(taskOrigin: TaskOrigin): Result<List<UrlModel>, Engine.MkException> =
engineCheckIn(taskOrigin)
.map { results ->
val urls = results.urls.map { it.toModel() }
val urls = results.urls.map { it.toModel() }.take(5)
sdsantos marked this conversation as resolved.
Show resolved Hide resolved
storeUrlsByUrl(urls)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ class RunNetTest(
event.index,
MeasurementModel(
test = spec.netTest.test,
reportId = reportId,
reportId = reportId?.let(MeasurementModel::ReportId),
resultId = result.id ?: return,
urlId = if (event.url.isNullOrEmpty()) {
null
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.ooni.probe.shared

import kotlin.io.encoding.Base64

fun String?.encodeUrlToBase64() = Base64.UrlSafe.encode(orEmpty().encodeToByteArray())

fun String?.decodeUrlFromBase64() = this?.ifEmpty { null }?.let { Base64.UrlSafe.decode(it).decodeToString() }
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package org.ooni.probe.ui.measurement

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.multiplatform.webview.web.LoadingState
import com.multiplatform.webview.web.WebView
import com.multiplatform.webview.web.rememberWebViewNavigator
import com.multiplatform.webview.web.rememberWebViewState
import ooniprobe.composeapp.generated.resources.Res
import ooniprobe.composeapp.generated.resources.back
import ooniprobe.composeapp.generated.resources.measurement
import ooniprobe.composeapp.generated.resources.refresh
import org.jetbrains.compose.resources.stringResource
import org.ooni.probe.data.models.MeasurementModel

@Composable
fun MeasurementScreen(
reportId: MeasurementModel.ReportId,
input: String?,
onBack: () -> Unit,
) {
val inputSuffix = input?.let { "?input=$it" } ?: ""
val url = "https://explorer.ooni.org/measurement/${reportId.value}$inputSuffix"

val webViewState = rememberWebViewState(url)
val webViewNavigator = rememberWebViewNavigator()

Column {
TopAppBar(
title = {
Text(stringResource(Res.string.measurement))
},
navigationIcon = {
IconButton(onClick = { onBack() }) {
Icon(
Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(Res.string.back),
)
}
},
actions = {
IconButton(
onClick = { webViewNavigator.reload() },
enabled = webViewState.loadingState is LoadingState.Finished,
) {
Icon(
Icons.Default.Refresh,
contentDescription = stringResource(Res.string.refresh),
)
}
},
)

LinearProgressIndicator(
progress = {
val loadingState = webViewState.loadingState
if (loadingState is LoadingState.Loading) loadingState.progress else 1f
},
color = MaterialTheme.colorScheme.background,
trackColor = MaterialTheme.colorScheme.primary,
modifier = Modifier
.fillMaxWidth()
.height(1.dp),
)

WebView(
state = webViewState,
navigator = webViewNavigator,
modifier = Modifier.fillMaxSize(),
)
aanorbel marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import org.ooni.probe.data.models.MeasurementModel
import org.ooni.probe.data.models.PreferenceCategoryKey
import org.ooni.probe.data.models.ResultModel
import org.ooni.probe.data.models.SettingsCategoryItem
import org.ooni.probe.di.Dependencies
import org.ooni.probe.shared.decodeUrlFromBase64
import org.ooni.probe.ui.dashboard.DashboardScreen
import org.ooni.probe.ui.measurement.MeasurementScreen
import org.ooni.probe.ui.result.ResultScreen
import org.ooni.probe.ui.results.ResultsScreen
import org.ooni.probe.ui.settings.SettingsScreen
Expand Down Expand Up @@ -68,12 +71,28 @@ fun Navigation(
dependencies.resultViewModel(
resultId = ResultModel.Id(resultId),
onBack = { navController.navigateUp() },
goToMeasurement = { reportId, input ->
navController.navigate(Screen.Measurement(reportId, input).route)
},
)
}
val state by viewModel.state.collectAsState()
ResultScreen(state, viewModel::onEvent)
}

composable(
route = Screen.Measurement.NAV_ROUTE,
arguments = Screen.Measurement.ARGUMENTS,
) { entry ->
val reportId = entry.arguments?.getString("reportId") ?: return@composable
val input = entry.arguments?.getString("input").decodeUrlFromBase64()
MeasurementScreen(
reportId = MeasurementModel.ReportId(reportId),
input = input,
onBack = { navController.navigateUp() },
)
}

composable(
route = Screen.SettingsCategory.NAV_ROUTE,
arguments = Screen.SettingsCategory.ARGUMENTS,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package org.ooni.probe.ui.navigation

import androidx.navigation.NavType
import androidx.navigation.navArgument
import org.ooni.probe.data.models.MeasurementModel
import org.ooni.probe.data.models.PreferenceCategoryKey
import org.ooni.probe.data.models.ResultModel
import org.ooni.probe.shared.encodeUrlToBase64

sealed class Screen(
val route: String,
Expand All @@ -23,7 +25,26 @@ sealed class Screen(
}
}

data class SettingsCategory(val category: PreferenceCategoryKey) : Screen("settings/${category.name}") {
data class Measurement(
val measurementReportId: MeasurementModel.ReportId,
val input: String?,
) : Screen("measurements/${measurementReportId.value}?input=${input.encodeUrlToBase64()}") {
companion object {
const val NAV_ROUTE = "measurements/{reportId}?input={input}"
val ARGUMENTS = listOf(
navArgument("reportId") { type = NavType.StringType },
navArgument("input") {
type = NavType.StringType
defaultValue = null
nullable = true
},
)
}
}

data class SettingsCategory(
val category: PreferenceCategoryKey,
) : Screen("settings/${category.name}") {
companion object {
const val NAV_ROUTE = "settings/{category}"
val ARGUMENTS = listOf(navArgument("category") { type = NavType.StringType })
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package org.ooni.probe.ui.result

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
Expand All @@ -17,13 +19,15 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.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.MeasurementModel
import org.ooni.probe.data.models.MeasurementWithUrl

@Composable
Expand All @@ -32,6 +36,7 @@ fun ResultScreen(
onEvent: (ResultViewModel.Event) -> Unit,
) {
Column {
val descriptorColor = state.result?.descriptor?.color ?: MaterialTheme.colorScheme.primary
TopAppBar(
title = {
Text(state.result?.descriptor?.title?.invoke().orEmpty())
Expand All @@ -45,30 +50,45 @@ 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,
titleContentColor = descriptorColor,
navigationIconContentColor = descriptorColor,
actionIconContentColor = descriptorColor,
),
)

if (state.result == null) return@Column

LazyColumn {
items(state.result.measurements, key = { it.measurement.idOrThrow.value }) { item ->
ResultMeasurementItem(item)
ResultMeasurementItem(
item = item,
onClick = { reportId, input ->
onEvent(ResultViewModel.Event.MeasurementClicked(reportId, input))
},
)
}
}
}
}

@Composable
fun ResultMeasurementItem(item: MeasurementWithUrl) {
private fun ResultMeasurementItem(
item: MeasurementWithUrl,
onClick: (MeasurementModel.ReportId, String?) -> Unit,
) {
val test = item.measurement.test
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(16.dp),
modifier = Modifier
.fillMaxWidth()
.run {
if (item.measurement.isUploaded && item.measurement.reportId != null) {
clickable { onClick(item.measurement.reportId, item.url?.url) }
} else {
alpha(0.5f)
}
}
.padding(16.dp),
) {
Icon(
// TODO: Better fallback for nettest icon
Expand Down
Loading