Skip to content

Commit

Permalink
Merge pull request #80 from ooni/running-screen
Browse files Browse the repository at this point in the history
Running test screen
  • Loading branch information
sdsantos authored Sep 2, 2024
2 parents d2ed513 + 6f40915 commit 8a6d6ac
Show file tree
Hide file tree
Showing 20 changed files with 515 additions and 127 deletions.
28 changes: 25 additions & 3 deletions composeApp/src/commonMain/kotlin/org/ooni/probe/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,17 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import co.touchlab.kermit.Logger
import org.jetbrains.compose.ui.tooling.preview.Preview
import org.ooni.probe.di.Dependencies
import org.ooni.probe.ui.navigation.BottomNavigationBar
import org.ooni.probe.ui.navigation.Navigation
import org.ooni.probe.ui.navigation.Screen
import org.ooni.probe.ui.theme.AppTheme

@Composable
Expand All @@ -28,20 +31,37 @@ fun App(dependencies: Dependencies) {
val navController = rememberNavController()
val snackbarHostState = remember { SnackbarHostState() }

val currentNavEntry by navController.currentBackStackEntryAsState()
val currentRoute = currentNavEntry?.destination?.route
val isMainScreen = MAIN_NAVIGATION_SCREENS.map { it.route }.contains(currentRoute)

CompositionLocalProvider(
values = arrayOf(LocalSnackbarHostState provides snackbarHostState),
) {
AppTheme {
AppTheme(
currentRoute = currentRoute,
) {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background,
) {
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) },
bottomBar = { BottomNavigationBar(navController) },
bottomBar = {
if (isMainScreen) {
BottomNavigationBar(navController)
}
},
) { paddingValues ->
Box(
modifier = Modifier.padding(bottom = paddingValues.calculateBottomPadding()),
modifier = Modifier
.run {
if (isMainScreen) {
padding(bottom = paddingValues.calculateBottomPadding())
} else {
this
}
},
) {
Navigation(
navController = navController,
Expand Down Expand Up @@ -73,3 +93,5 @@ private fun logAppStart(dependencies: Dependencies) {
}

val LocalSnackbarHostState = compositionLocalOf<SnackbarHostState?> { null }

val MAIN_NAVIGATION_SCREENS = listOf(Screen.Dashboard, Screen.Results, Screen.Settings)
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ sealed interface TestRunState {
) : TestRunState

data class Running(
val descriptorName: String? = null,
val descriptor: Descriptor? = null,
val testType: TestType? = null,
val estimatedRuntime: List<Duration>? = null,
val testProgress: Double = 0.0,
Expand Down
51 changes: 34 additions & 17 deletions composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,13 @@ import org.ooni.probe.domain.GetTestDescriptors
import org.ooni.probe.domain.GetTestDescriptorsBySpec
import org.ooni.probe.domain.RunDescriptors
import org.ooni.probe.domain.RunNetTest
import org.ooni.probe.domain.SendSupportEmail
import org.ooni.probe.domain.TestRunStateManager
import org.ooni.probe.shared.PlatformInfo
import org.ooni.probe.ui.dashboard.DashboardViewModel
import org.ooni.probe.ui.result.ResultViewModel
import org.ooni.probe.ui.results.ResultsViewModel
import org.ooni.probe.ui.running.RunningViewModel
import org.ooni.probe.ui.settings.SettingsViewModel
import org.ooni.probe.ui.settings.about.AboutViewModel
import org.ooni.probe.ui.settings.category.SettingsCategoryViewModel
Expand Down Expand Up @@ -176,27 +178,49 @@ class Dependencies(
)
}

val sendSupportEmail by lazy { SendSupportEmail(platformInfo, launchUrl) }

private val testStateManager by lazy { TestRunStateManager(resultRepository.getLatest()) }

// ViewModels

fun dashboardViewModel(goToResults: () -> Unit) =
DashboardViewModel(
goToResults = goToResults,
getTestDescriptors = getTestDescriptors::invoke,
runDescriptors = runDescriptors::invoke,
observeTestRunState = testStateManager.observeState(),
observeTestRunErrors = testStateManager.observeError(),
fun aboutViewModel(onBack: () -> Unit) =
AboutViewModel(
onBack = onBack,
launchUrl = { launchUrl(it, emptyMap()) },
)

fun dashboardViewModel(
goToResults: () -> Unit,
goToRunningTest: () -> Unit,
) = DashboardViewModel(
goToResults = goToResults,
goToRunningTest = goToRunningTest,
getTestDescriptors = getTestDescriptors::invoke,
runDescriptors = runDescriptors::invoke,
observeTestRunState = testStateManager.observeState(),
observeTestRunErrors = testStateManager.observeError(),
)

fun resultsViewModel(goToResult: (ResultModel.Id) -> Unit) = ResultsViewModel(goToResult, getResults::invoke)

fun runningViewModel(
onBack: () -> Unit,
goToResults: () -> Unit,
) = RunningViewModel(
onBack = onBack,
goToResults = goToResults,
observeTestRunState = testStateManager.observeState(),
observeTestRunErrors = testStateManager.observeError(),
cancelTestRun = testStateManager::cancelTestRun,
)

fun settingsViewModel(
goToSettingsForCategory: (PreferenceCategoryKey) -> Unit,
sendSupportEmail: () -> Unit,
sendSupportEmail: suspend () -> Unit,
) = SettingsViewModel(
goToSettingsForCategory,
sendSupportEmail,
goToSettingsForCategory = goToSettingsForCategory,
sendSupportEmail = sendSupportEmail,
)

fun settingsCategoryViewModel(
Expand All @@ -222,13 +246,6 @@ class Dependencies(
markResultAsViewed = resultRepository::markAsViewed,
)

fun aboutViewModel(onBack: () -> Unit) =
AboutViewModel(onBack) {
launchUrl(it, emptyMap())
}

fun sendSupportEmail(): (String, Map<String, String>) -> Unit = launchUrl

companion object {
@VisibleForTesting
fun buildJson() =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class RunNetTest(
setCurrentTestState {
if (it !is TestRunState.Running) return@setCurrentTestState it
it.copy(
descriptor = spec.descriptor,
testType = spec.netTest.test,
testProgress = spec.testIndex * progressStep,
testIndex = spec.testIndex,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.ooni.probe.domain

import ooniprobe.composeapp.generated.resources.Res
import ooniprobe.composeapp.generated.resources.Settings_SendEmail_Label
import ooniprobe.composeapp.generated.resources.Settings_SendEmail_Message
import ooniprobe.composeapp.generated.resources.shareEmailTo
import ooniprobe.composeapp.generated.resources.shareSubject
import org.jetbrains.compose.resources.getString
import org.ooni.probe.shared.PlatformInfo

class SendSupportEmail(
private val platformInfo: PlatformInfo,
private val launchUrl: (String, Map<String, String>?) -> Unit,
) {
suspend operator fun invoke() {
getString(Res.string.shareEmailTo)
val supportEmail = getString(Res.string.shareEmailTo)
val subject = getString(Res.string.shareSubject, platformInfo.version)
val chooserTitle = getString(Res.string.Settings_SendEmail_Label)
val body = getString(Res.string.Settings_SendEmail_Message) + "\n\n\n" +
"PLATFORM: ${platformInfo.platform}\n" +
"MODEL: ${platformInfo.model}\n" +
"OS Version: ${platformInfo.osVersion}"

launchUrl(
supportEmail,
mapOf(
"subject" to subject,
"body" to body,
"chooserTitle" to chooserTitle,
),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,8 @@ import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
Expand All @@ -33,7 +31,6 @@ import ooniprobe.composeapp.generated.resources.Dashboard_Running_EstimatedTimeL
import ooniprobe.composeapp.generated.resources.Dashboard_Running_Running
import ooniprobe.composeapp.generated.resources.Dashboard_Running_Stopping_Notice
import ooniprobe.composeapp.generated.resources.Dashboard_Running_Stopping_Title
import ooniprobe.composeapp.generated.resources.Modal_Error_CantDownloadURLs
import ooniprobe.composeapp.generated.resources.OONIRun_Run
import ooniprobe.composeapp.generated.resources.Res
import ooniprobe.composeapp.generated.resources.app_name
Expand All @@ -43,10 +40,9 @@ import ooniprobe.composeapp.generated.resources.ooni_empty_state
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
import org.jetbrains.compose.ui.tooling.preview.Preview
import org.ooni.probe.LocalSnackbarHostState
import org.ooni.probe.data.models.TestRunError
import org.ooni.probe.data.models.TestRunState
import org.ooni.probe.ui.customColors
import org.ooni.probe.ui.shared.TestRunErrorMessages
import org.ooni.probe.ui.shared.relativeDateTime
import org.ooni.probe.ui.shared.shortFormat
import org.ooni.probe.ui.theme.AppTheme
Expand Down Expand Up @@ -87,7 +83,10 @@ fun DashboardScreen(
}
}

ErrorMessages(state, onEvent)
TestRunErrorMessages(
errors = state.testRunErrors,
onErrorDisplayed = { onEvent(DashboardViewModel.Event.ErrorDisplayed(it)) },
)
}

@Composable
Expand Down Expand Up @@ -140,7 +139,7 @@ private fun TestRunStateSection(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.clickable { onEvent(DashboardViewModel.Event.RunningTestClick) }
.padding(horizontal = 16.dp, vertical = 4.dp),
.padding(horizontal = 16.dp, vertical = 8.dp),
) {
state.testType?.let { testType ->
Row {
Expand Down Expand Up @@ -212,25 +211,6 @@ private fun TestRunStateSection(
}
}

@Composable
private fun ErrorMessages(
state: DashboardViewModel.State,
onEvent: (DashboardViewModel.Event) -> Unit,
) {
val snackbarHostState = LocalSnackbarHostState.current ?: return
val errorMessage = when (state.testRunErrors.firstOrNull()) {
TestRunError.DownloadUrlsFailed -> stringResource(Res.string.Modal_Error_CantDownloadURLs)
null -> ""
}
LaunchedEffect(state.testRunErrors) {
val error = state.testRunErrors.firstOrNull() ?: return@LaunchedEffect
val result = snackbarHostState.showSnackbar(errorMessage)
if (result == SnackbarResult.Dismissed) {
onEvent(DashboardViewModel.Event.ErrorDisplayed(error))
}
}
}

@Preview
@Composable
fun DashboardScreenPreview() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package org.ooni.probe.ui.dashboard

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
Expand All @@ -20,6 +22,7 @@ import org.ooni.probe.data.models.TestRunState

class DashboardViewModel(
goToResults: () -> Unit,
goToRunningTest: () -> Unit,
getTestDescriptors: () -> Flow<List<Descriptor>>,
runDescriptors: suspend (RunSpecification) -> Unit,
observeTestRunState: Flow<TestRunState>,
Expand Down Expand Up @@ -52,19 +55,16 @@ class DashboardViewModel(
events
.filterIsInstance<Event.RunTestsClick>()
.onEach {
coroutineScope {
launch {
runDescriptors(buildRunSpecification())
}
// TODO: This will become a StartTestRun domain class to start a background service
CoroutineScope(Dispatchers.IO).launch {
runDescriptors(buildRunSpecification())
}
}
.launchIn(viewModelScope)

events
.filterIsInstance<Event.RunningTestClick>()
.onEach {
// TODO
}
.onEach { goToRunningTest() }
.launchIn(viewModelScope)

events
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package org.ooni.probe.ui.measurement

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Refresh
Expand Down Expand Up @@ -80,7 +84,9 @@ fun MeasurementScreen(
WebView(
state = webViewState,
navigator = webViewNavigator,
modifier = Modifier.fillMaxSize(),
modifier = Modifier
.fillMaxSize()
.padding(WindowInsets.navigationBars.asPaddingValues()),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,13 @@ import ooniprobe.composeapp.generated.resources.ic_history
import ooniprobe.composeapp.generated.resources.ic_settings
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
import org.ooni.probe.MAIN_NAVIGATION_SCREENS

@Composable
fun BottomNavigationBar(navController: NavController) {
val entry by navController.currentBackStackEntryAsState()
val currentRoute = entry?.destination?.route ?: return

// Only show the bottom app on the main screens
if (!MAIN_NAVIGATION_SCREENS.map { it.route }.contains(currentRoute)) return

NavigationBar {
MAIN_NAVIGATION_SCREENS.forEach { screen ->
NavigationBarItem(
Expand Down Expand Up @@ -79,5 +77,3 @@ private val Screen.iconRes
Screen.Settings -> Res.drawable.ic_settings
else -> throw IllegalArgumentException("Only main screens allowed in bottom navigation")
}

private val MAIN_NAVIGATION_SCREENS = listOf(Screen.Dashboard, Screen.Results, Screen.Settings)
Loading

0 comments on commit 8a6d6ac

Please sign in to comment.