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 @@ -122,6 +122,9 @@
<string name="Measurements_Count_Other">%1$d measured</string>
<string name="Measurements_Failed_One">%1$d failed</string>
<string name="Measurements_Failed_Other">%1$d failed</string>
<string name="Measurement_Raw_NotUploadedReasoning">This measurements was not uploaded. Upload it to view the analysis.</string>
<string name="Measurement_Raw_Share">Share measurement data</string>
<string name="Measurement_Raw_Upload">Upload</string>

<!-- Settings -->

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.launch
import org.ooni.probe.data.models.MeasurementsFilter
import org.ooni.probe.data.models.ResultModel
import org.ooni.probe.data.models.RunBackgroundState
import org.ooni.probe.data.models.RunSpecification
Expand All @@ -25,7 +26,7 @@ import org.ooni.probe.shared.monitoring.Instrumentation

class RunBackgroundTask(
private val getPreferenceValueByKey: (SettingsKey) -> Flow<Any?>,
private val uploadMissingMeasurements: (ResultModel.Id?) -> Flow<UploadMissingMeasurements.State>,
private val uploadMissingMeasurements: (MeasurementsFilter) -> Flow<UploadMissingMeasurements.State>,
private val checkAutoRunConstraints: suspend () -> Boolean,
private val getAutoRunSpecification: suspend () -> RunSpecification.Full,
private val runDescriptors: suspend (RunSpecification.Full) -> Unit,
Expand Down Expand Up @@ -54,7 +55,7 @@ class RunBackgroundTask(
if (isAutoRun && !checkAutoRunConstraints()) return@withTransaction

if (isAutoRun || spec is RunSpecification.OnlyUploadMissingResults) {
val uploadCancelled = uploadMissingResults()
val uploadCancelled = uploadMissingResults(MeasurementsFilter.All)
if (uploadCancelled) return@withTransaction
}

Expand All @@ -67,15 +68,15 @@ class RunBackgroundTask(

// When a test is cancelled, sometimes the last measurement isn't uploaded

getLatestResult().first()?.id.let { latestResultId ->
getLatestResult().first()?.id?.let { latestResultId ->
val idleState = getRunBackgroundState().first()
uploadMissingResults(resultId = latestResultId)
uploadMissingResults(MeasurementsFilter.Result(latestResultId))
updateState(idleState)
}
}
}

private suspend fun ProducerScope<RunBackgroundState>.uploadMissingResults(resultId: ResultModel.Id? = null): Boolean {
private suspend fun ProducerScope<RunBackgroundState>.uploadMissingResults(filter: MeasurementsFilter): Boolean {
val autoUpload = getPreferenceValueByKey(SettingsKey.UPLOAD_RESULTS).first() == true
if (!autoUpload) return false

Expand All @@ -84,7 +85,7 @@ class RunBackgroundTask(

coroutineScope {
val uploadJob = async {
uploadMissingMeasurements(resultId)
uploadMissingMeasurements(filter)
.collectLatest { uploadState ->
updateState(RunBackgroundState.UploadingMissingResults(uploadState))
}
Expand All @@ -101,7 +102,7 @@ class RunBackgroundTask(
try {
uploadJob.await()
} catch (e: CancellationException) {
Logger.i("Upload Missing Results (result=$resultId): cancelled")
Logger.i("Upload Missing Results (filter=$filter): cancelled")
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.ooni.probe.data.models

sealed interface MeasurementsFilter {
data object All : MeasurementsFilter

data class Result(val resultId: ResultModel.Id) : MeasurementsFilter

data class Measurement(val measurementId: MeasurementModel.Id) : MeasurementsFilter
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package org.ooni.probe.data.repositories

import app.cash.sqldelight.coroutines.asFlow
import app.cash.sqldelight.coroutines.mapToList
import app.cash.sqldelight.coroutines.mapToOne
import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
Expand All @@ -10,6 +11,7 @@ import kotlinx.serialization.json.Json
import org.ooni.engine.models.TestKeys
import org.ooni.engine.models.TestType
import org.ooni.probe.Database
import org.ooni.probe.data.GetById
import org.ooni.probe.data.Measurement
import org.ooni.probe.data.SelectByResultIdWithUrl
import org.ooni.probe.data.SelectTestKeysByDescriptorKey
Expand Down Expand Up @@ -81,6 +83,13 @@ class MeasurementRepository(
.map { list -> list.mapNotNull { it.toModel() } }
}

fun getById(measurementId: MeasurementModel.Id) =
database.measurementQueries
.getById(measurementId.value)
.asFlow()
.mapToOne(backgroundContext)
.map { it.toModel() }

suspend fun createOrUpdate(model: MeasurementModel): MeasurementModel.Id =
withContext(backgroundContext) {
database.transactionWithResult {
Expand Down Expand Up @@ -176,6 +185,38 @@ class MeasurementRepository(
)
}

private fun GetById.toModel(): MeasurementWithUrl? {
return MeasurementWithUrl(
measurement = Measurement(
id = id,
test_name = test_name,
start_time = start_time,
runtime = runtime,
is_done = is_done,
is_uploaded = is_uploaded,
is_failed = is_failed,
failure_msg = failure_msg,
is_upload_failed = is_upload_failed,
upload_failure_msg = upload_failure_msg,
is_rerun = is_rerun,
is_anomaly = is_anomaly,
report_id = report_id,
test_keys = test_keys,
rerun_network = rerun_network,
url_id = url_id,
result_id = result_id,
).toModel() ?: return null,
url = id_?.let { urlId ->
Url(
id = urlId,
url = url,
country_code = country_code,
category_code = category_code,
).toModel()
},
)
}

private fun SelectTestKeysByDescriptorKey.toModel(): TestKeysWithResultId? {
return TestKeysWithResultId(
id = MeasurementModel.Id(id),
Expand Down
34 changes: 29 additions & 5 deletions composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import org.ooni.probe.data.models.AutoRunParameters
import org.ooni.probe.data.models.BatteryState
import org.ooni.probe.data.models.InstalledTestDescriptorModel
import org.ooni.probe.data.models.MeasurementModel
import org.ooni.probe.data.models.MeasurementsFilter
import org.ooni.probe.data.models.PlatformAction
import org.ooni.probe.data.models.PreferenceCategoryKey
import org.ooni.probe.data.models.ResultModel
Expand All @@ -54,6 +55,7 @@ import org.ooni.probe.domain.GetBootstrapTestDescriptors
import org.ooni.probe.domain.GetDefaultTestDescriptors
import org.ooni.probe.domain.GetEnginePreferences
import org.ooni.probe.domain.GetFirstRun
import org.ooni.probe.domain.GetMeasurementsNotUploaded
import org.ooni.probe.domain.GetProxySettings
import org.ooni.probe.domain.GetResult
import org.ooni.probe.domain.GetResults
Expand Down Expand Up @@ -92,6 +94,7 @@ import org.ooni.probe.ui.descriptor.DescriptorViewModel
import org.ooni.probe.ui.descriptor.add.AddDescriptorViewModel
import org.ooni.probe.ui.descriptor.review.ReviewUpdatesViewModel
import org.ooni.probe.ui.log.LogViewModel
import org.ooni.probe.ui.measurement.MeasurementRawViewModel
import org.ooni.probe.ui.measurement.MeasurementViewModel
import org.ooni.probe.ui.onboarding.OnboardingViewModel
import org.ooni.probe.ui.result.ResultViewModel
Expand Down Expand Up @@ -314,6 +317,12 @@ class Dependencies(
)
}

private val getMeasurementsNotUploaded by lazy {
GetMeasurementsNotUploaded(
listMeasurementsNotUploaded = measurementRepository::listNotUploaded,
getMeasurementById = measurementRepository::getById,
)
}
private val getSettings by lazy {
GetSettings(
preferencesRepository = preferenceRepository,
Expand Down Expand Up @@ -351,7 +360,6 @@ class Dependencies(
getAutoRunSettings = getAutoRunSettings::invoke,
)
}

val observeAndConfigureAutoUpdate by lazy {
ObserveAndConfigureAutoUpdate(
backgroundContext = backgroundContext,
Expand All @@ -361,7 +369,6 @@ class Dependencies(
startDescriptorsUpdate = startDescriptorsUpdate,
)
}

private val rejectDescriptorUpdate by lazy {
RejectDescriptorUpdate(
updateDescriptorRejectedRevision = testDescriptorRepository::updateRejectedRevision,
Expand Down Expand Up @@ -413,7 +420,7 @@ class Dependencies(
}
private val uploadMissingMeasurements by lazy {
UploadMissingMeasurements(
getMeasurementsNotUploaded = measurementRepository::listNotUploaded,
getMeasurementsNotUploaded = getMeasurementsNotUploaded::invoke,
submitMeasurement = engine::submitMeasurements,
readFile = readFile,
deleteFiles = deleteFiles,
Expand Down Expand Up @@ -589,12 +596,14 @@ class Dependencies(
resultId: ResultModel.Id,
onBack: () -> Unit,
goToMeasurement: (MeasurementModel.ReportId, String?) -> Unit,
goToMeasurementRaw: (MeasurementModel.Id) -> Unit,
goToUpload: () -> Unit,
goToDashboard: () -> Unit,
) = ResultViewModel(
resultId = resultId,
onBack = onBack,
goToMeasurement = goToMeasurement,
goToMeasurementRaw = goToMeasurementRaw,
goToUpload = goToUpload,
goToDashboard = goToDashboard,
getResult = getResult::invoke,
Expand All @@ -616,6 +625,21 @@ class Dependencies(
isWebViewAvailable = isWebViewAvailable,
)

fun measurementRawViewModel(
measurementId: MeasurementModel.Id,
onBack: () -> Unit,
goToUpload: (MeasurementModel.Id) -> Unit,
goToMeasurement: (MeasurementModel.ReportId, String?) -> Unit,
) = MeasurementRawViewModel(
measurementId = measurementId,
onBack = onBack,
goToUpload = goToUpload,
goToMeasurement = goToMeasurement,
getMeasurement = measurementRepository::getById,
readFile = readFile,
shareFile = { launchAction(it) },
)

fun reviewUpdatesViewModel(
descriptorIds: List<InstalledTestDescriptorModel.Id>?,
onBack: () -> Unit,
Expand Down Expand Up @@ -648,10 +672,10 @@ class Dependencies(
)

fun uploadMeasurementsViewModel(
resultId: ResultModel.Id?,
filter: MeasurementsFilter,
onClose: () -> Unit,
) = UploadMeasurementsViewModel(
resultId = resultId,
filter = filter,
onClose = onClose,
uploadMissingMeasurements = uploadMissingMeasurements::invoke,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.ooni.probe.domain

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import org.ooni.probe.data.models.MeasurementModel
import org.ooni.probe.data.models.MeasurementWithUrl
import org.ooni.probe.data.models.MeasurementsFilter
import org.ooni.probe.data.models.ResultModel

class GetMeasurementsNotUploaded(
private val listMeasurementsNotUploaded: (ResultModel.Id?) -> Flow<List<MeasurementModel>>,
private val getMeasurementById: (MeasurementModel.Id) -> Flow<MeasurementWithUrl?>,
) {
fun invoke(filter: MeasurementsFilter): Flow<List<MeasurementModel>> =
when (filter) {
MeasurementsFilter.All -> listMeasurementsNotUploaded(null)

is MeasurementsFilter.Result -> listMeasurementsNotUploaded(filter.resultId)

is MeasurementsFilter.Measurement -> getMeasurementById(filter.measurementId).map {
if (it != null && it.measurement.isDoneAndMissingUpload) {
listOf(it.measurement)
} else {
emptyList()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,31 @@ import org.ooni.engine.models.Result
import org.ooni.probe.data.disk.DeleteFiles
import org.ooni.probe.data.disk.ReadFile
import org.ooni.probe.data.models.MeasurementModel
import org.ooni.probe.data.models.ResultModel
import org.ooni.probe.data.models.MeasurementsFilter
import org.ooni.probe.shared.monitoring.Instrumentation

class UploadMissingMeasurements(
private val getMeasurementsNotUploaded: (ResultModel.Id?) -> Flow<List<MeasurementModel>>,
private val getMeasurementsNotUploaded: (MeasurementsFilter) -> Flow<List<MeasurementModel>>,
private val submitMeasurement: suspend (String) -> Result<SubmitMeasurementResults, MkException>,
private val readFile: ReadFile,
private val deleteFiles: DeleteFiles,
private val updateMeasurement: suspend (MeasurementModel) -> Unit,
private val deleteMeasurementById: suspend (MeasurementModel.Id) -> Unit,
) {
operator fun invoke(resultId: ResultModel.Id? = null): Flow<State> =
operator fun invoke(filter: MeasurementsFilter): Flow<State> =
channelFlow {
Instrumentation.withTransaction(
operation = this@UploadMissingMeasurements::class.simpleName.orEmpty(),
data = mapOf(
"resultId" to resultId?.value.toString(),
"resultId" to
(filter as? MeasurementsFilter.Result)?.resultId?.value.toString(),
"measurementId" to
(filter as? MeasurementsFilter.Measurement)?.measurementId?.value.toString(),
),
) {
send(State.Starting)

val measurements = getMeasurementsNotUploaded(resultId).first()
val measurements = getMeasurementsNotUploaded(filter).first()
val total = measurements.size
var uploaded = 0
var failedToUpload = 0
Expand Down
Loading
Loading