Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,15 @@
<string name="CategoryCode_MISC_Description">Sites that haven\'t been categorized yet</string>

<string name="Snackbar_ResultsNotUploaded_Text">Not uploaded</string>
<string name="Snackbar_ResultsSomeNotUploaded_Text">Some not uploaded</string>
<string name="Snackbar_ResultsSomeNotUploaded_UploadAll">Upload All</string>
<string name="Modal_ResultsNotUploaded_Uploading">Uploading %1$s ...</string>
<string name="Toast_ResultsUploaded">Upload successful</string>
<string name="Modal_UploadFailed_Title">Upload failed</string>
<string name="Modal_UploadFailed_Paragraph">We have failed to upload %1$s/%2$s measurements. The failure log has been shared with OONI developers.</string>
<string name="Modal_Retry">Retry</string>
<string name="Modal_Cancel">Cancel</string>
<string name="Modal_OK">OK</string>

<string name="Modal_Error_CantDownloadURLs">Unable to download URL list. Please try again.</string>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ interface OonimkallBridge {

data class SubmitMeasurementResults(
val updatedMeasurement: String?,
val updatedReportId: String?,
val updatedReportId: String,
)

data class CheckInConfig(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.ooni.probe.data.disk

import co.touchlab.kermit.Logger
import okio.FileSystem
import okio.IOException
import okio.Path
import okio.Path.Companion.toPath

interface ReadFile {
suspend operator fun invoke(path: Path): String?
}

class ReadFileOkio(
private val fileSystem: FileSystem,
private val baseFilesDir: String,
) : ReadFile {
override suspend fun invoke(path: Path): String? =
try {
fileSystem.read(baseFilesDir.toPath().resolve(path)) {
readUtf8()
}
} catch (e: IOException) {
Logger.w("Could not read $path", e)
null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ class MeasurementRepository(
.mapToList(backgroundDispatcher)
.map { list -> list.mapNotNull { it.toModel() } }

fun listNotUploaded() =
database.measurementQueries
.selectAllNotUploaded()
.asFlow()
.mapToList(backgroundDispatcher)
.map { list -> list.mapNotNull { it.toModel() } }

suspend fun createOrUpdate(model: MeasurementModel): MeasurementModel.Id =
withContext(backgroundDispatcher) {
database.transactionWithResult {
Expand Down
71 changes: 47 additions & 24 deletions composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import org.ooni.engine.TaskEventMapper
import org.ooni.probe.Database
import org.ooni.probe.data.disk.DeleteFile
import org.ooni.probe.data.disk.DeleteFileOkio
import org.ooni.probe.data.disk.ReadFile
import org.ooni.probe.data.disk.ReadFileOkio
import org.ooni.probe.data.disk.WriteFile
import org.ooni.probe.data.disk.WriteFileOkio
import org.ooni.probe.data.models.AutoRunParameters
Expand Down Expand Up @@ -50,6 +52,7 @@ 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.domain.UploadMissingMeasurements
import org.ooni.probe.shared.PlatformInfo
import org.ooni.probe.ui.dashboard.DashboardViewModel
import org.ooni.probe.ui.result.ResultViewModel
Expand All @@ -59,6 +62,7 @@ import org.ooni.probe.ui.settings.SettingsViewModel
import org.ooni.probe.ui.settings.about.AboutViewModel
import org.ooni.probe.ui.settings.category.SettingsCategoryViewModel
import org.ooni.probe.ui.settings.proxy.ProxyViewModel
import org.ooni.probe.ui.upload.UploadMeasurementsViewModel

class Dependencies(
val platformInfo: PlatformInfo,
Expand Down Expand Up @@ -95,6 +99,7 @@ class Dependencies(
}
private val urlRepository by lazy { UrlRepository(database, backgroundDispatcher) }

private val readFile: ReadFile by lazy { ReadFileOkio(FileSystem.SYSTEM, baseFileDir) }
private val writeFile: WriteFile by lazy { WriteFileOkio(FileSystem.SYSTEM, baseFileDir) }
private val deleteFile: DeleteFile by lazy { DeleteFileOkio(FileSystem.SYSTEM, baseFileDir) }

Expand Down Expand Up @@ -212,8 +217,20 @@ class Dependencies(

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

private val uploadMissingMeasurements by lazy {
UploadMissingMeasurements(
getMeasurementsNotUploaded = measurementRepository.listNotUploaded(),
submitMeasurement = engine::submitMeasurements,
readFile = readFile,
deleteFile = deleteFile,
updateMeasurement = measurementRepository::createOrUpdate,
)
}

// ViewModels

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

fun dashboardViewModel(
goToResults: () -> Unit,
goToRunningTest: () -> Unit,
Expand All @@ -226,7 +243,7 @@ class Dependencies(
observeTestRunErrors = testStateManager.observeError(),
)

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

fun runningViewModel(
onBack: () -> Unit,
Expand All @@ -239,23 +256,13 @@ class Dependencies(
cancelTestRun = testStateManager::cancelTestRun,
)

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

fun settingsCategoryViewModel(
goToSettingsForCategory: (PreferenceCategoryKey) -> Unit,
onBack: () -> Unit,
category: SettingsCategoryItem,
) = SettingsCategoryViewModel(
preferenceManager = preferenceRepository,
onBack = onBack,
goToSettingsForCategory = goToSettingsForCategory,
category = category,
fun resultsViewModel(
goToResult: (ResultModel.Id) -> Unit,
goToUpload: () -> Unit,
) = ResultsViewModel(
goToResult = goToResult,
goToUpload = goToUpload,
getResults = getResults::invoke,
)

fun resultViewModel(
Expand All @@ -270,14 +277,30 @@ class Dependencies(
markResultAsViewed = resultRepository::markAsViewed,
)

fun aboutViewModel(onBack: () -> Unit) =
AboutViewModel(onBack) {
launchUrl(it, emptyMap())
}
fun settingsCategoryViewModel(
goToSettingsForCategory: (PreferenceCategoryKey) -> Unit,
onBack: () -> Unit,
category: SettingsCategoryItem,
) = SettingsCategoryViewModel(
preferenceManager = preferenceRepository,
onBack = onBack,
goToSettingsForCategory = goToSettingsForCategory,
category = category,
)

fun sendSupportEmail(): (String, Map<String, String>) -> Unit = launchUrl
fun settingsViewModel(
goToSettingsForCategory: (PreferenceCategoryKey) -> Unit,
sendSupportEmail: suspend () -> Unit,
) = SettingsViewModel(
goToSettingsForCategory = goToSettingsForCategory,
sendSupportEmail = sendSupportEmail,
)

fun proxyViewModel(onBack: () -> Unit) = ProxyViewModel(onBack, preferenceRepository)
fun uploadMeasurementsViewModel(onClose: () -> Unit) =
UploadMeasurementsViewModel(
onClose = onClose,
uploadMissingMeasurements = uploadMissingMeasurements::invoke,
)

companion object {
@VisibleForTesting
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package org.ooni.probe.domain

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.first
import org.ooni.engine.Engine.MkException
import org.ooni.engine.OonimkallBridge.SubmitMeasurementResults
import org.ooni.engine.models.Result
import org.ooni.probe.data.disk.DeleteFile
import org.ooni.probe.data.disk.ReadFile
import org.ooni.probe.data.models.MeasurementModel

class UploadMissingMeasurements(
private val getMeasurementsNotUploaded: Flow<List<MeasurementModel>>,
private val submitMeasurement: suspend (String) -> Result<SubmitMeasurementResults, MkException>,
private val readFile: ReadFile,
private val deleteFile: DeleteFile,
private val updateMeasurement: suspend (MeasurementModel) -> Unit,
) {
operator fun invoke(): Flow<State> =
channelFlow {
send(State.Starting)

val measurements = getMeasurementsNotUploaded.first()
val total = measurements.size
var uploaded = 0
var failedToUpload = 0

measurements.forEach { measurement ->
send(State.Uploading(uploaded, failedToUpload, total))

val reportFilePath = measurement.reportFilePath ?: run {
failedToUpload++
return@forEach
}
val report = readFile(reportFilePath) ?: run {
failedToUpload++
return@forEach
}

submitMeasurement(report)
.onSuccess { submitResult ->
uploaded++
updateMeasurement(
measurement.copy(
isUploaded = true,
isUploadFailed = false,
uploadFailureMessage = null,
reportId = MeasurementModel.ReportId(submitResult.updatedReportId),
),
)
deleteFile(reportFilePath)
}
.onFailure { exception ->
failedToUpload++
updateMeasurement(
measurement.copy(
isUploadFailed = true,
uploadFailureMessage = exception.cause?.message,
),
)
}
}

send(State.Finished(uploaded, failedToUpload, total))
}

sealed interface State {
data object Starting : State

data class Uploading(
val uploaded: Int,
val failedToUpload: Int,
val total: Int,
) : State

data class Finished(
val uploaded: Int,
val failedToUpload: Int,
val total: Int,
) : State
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.dialog
import org.ooni.probe.data.models.MeasurementModel
import org.ooni.probe.data.models.PreferenceCategoryKey
import org.ooni.probe.data.models.ResultModel
Expand All @@ -24,6 +26,7 @@ import org.ooni.probe.ui.settings.SettingsScreen
import org.ooni.probe.ui.settings.about.AboutScreen
import org.ooni.probe.ui.settings.category.SettingsCategoryScreen
import org.ooni.probe.ui.settings.proxy.ProxyScreen
import org.ooni.probe.ui.upload.UploadMeasurementsDialog

@Composable
fun Navigation(
Expand All @@ -50,6 +53,7 @@ fun Navigation(
val viewModel = viewModel {
dependencies.resultsViewModel(
goToResult = { navController.navigate(Screen.Result(it).route) },
goToUpload = { navController.navigate(Screen.UploadMeasurements.route) },
)
}
val state by viewModel.state.collectAsState()
Expand Down Expand Up @@ -128,6 +132,7 @@ fun Navigation(
onEvent = viewModel::onEvent,
)
}

else -> {
val viewModel = viewModel {
dependencies.settingsCategoryViewModel(
Expand Down Expand Up @@ -163,5 +168,19 @@ fun Navigation(
val state by viewModel.state.collectAsState()
RunningScreen(state, viewModel::onEvent)
}

dialog(
route = Screen.UploadMeasurements.route,
dialogProperties = DialogProperties(
dismissOnBackPress = false,
dismissOnClickOutside = false,
),
) {
val viewModel = viewModel {
dependencies.uploadMeasurementsViewModel(onClose = { navController.popBackStack() })
}
val state by viewModel.state.collectAsState()
UploadMeasurementsDialog(state, viewModel::onEvent)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,6 @@ sealed class Screen(
}

data object RunningTest : Screen("running")

data object UploadMeasurements : Screen("upload")
}
Loading