Skip to content

Commit 470a05a

Browse files
authored
Merge pull request #685 from ooni/measurement-raw-screen
Measurement raw data screen
2 parents a8eba22 + cb9bc82 commit 470a05a

File tree

20 files changed

+438
-53
lines changed

20 files changed

+438
-53
lines changed

composeApp/src/commonMain/composeResources/values/strings-common.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,9 @@
122122
<string name="Measurements_Count_Other">%1$d measured</string>
123123
<string name="Measurements_Failed_One">%1$d failed</string>
124124
<string name="Measurements_Failed_Other">%1$d failed</string>
125+
<string name="Measurement_Raw_NotUploadedReasoning">This measurements was not uploaded. Upload it to view the analysis.</string>
126+
<string name="Measurement_Raw_Share">Share measurement data</string>
127+
<string name="Measurement_Raw_Upload">Upload</string>
125128

126129
<!-- Settings -->
127130

composeApp/src/commonMain/kotlin/org/ooni/probe/background/RunBackgroundTask.kt

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.first
1515
import kotlinx.coroutines.flow.onEach
1616
import kotlinx.coroutines.flow.takeWhile
1717
import kotlinx.coroutines.launch
18+
import org.ooni.probe.data.models.MeasurementsFilter
1819
import org.ooni.probe.data.models.ResultModel
1920
import org.ooni.probe.data.models.RunBackgroundState
2021
import org.ooni.probe.data.models.RunSpecification
@@ -25,7 +26,7 @@ import org.ooni.probe.shared.monitoring.Instrumentation
2526

2627
class RunBackgroundTask(
2728
private val getPreferenceValueByKey: (SettingsKey) -> Flow<Any?>,
28-
private val uploadMissingMeasurements: (ResultModel.Id?) -> Flow<UploadMissingMeasurements.State>,
29+
private val uploadMissingMeasurements: (MeasurementsFilter) -> Flow<UploadMissingMeasurements.State>,
2930
private val checkAutoRunConstraints: suspend () -> Boolean,
3031
private val getAutoRunSpecification: suspend () -> RunSpecification.Full,
3132
private val runDescriptors: suspend (RunSpecification.Full) -> Unit,
@@ -54,7 +55,7 @@ class RunBackgroundTask(
5455
if (isAutoRun && !checkAutoRunConstraints()) return@withTransaction
5556

5657
if (isAutoRun || spec is RunSpecification.OnlyUploadMissingResults) {
57-
val uploadCancelled = uploadMissingResults()
58+
val uploadCancelled = uploadMissingResults(MeasurementsFilter.All)
5859
if (uploadCancelled) return@withTransaction
5960
}
6061

@@ -67,15 +68,15 @@ class RunBackgroundTask(
6768

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

70-
getLatestResult().first()?.id.let { latestResultId ->
71+
getLatestResult().first()?.id?.let { latestResultId ->
7172
val idleState = getRunBackgroundState().first()
72-
uploadMissingResults(resultId = latestResultId)
73+
uploadMissingResults(MeasurementsFilter.Result(latestResultId))
7374
updateState(idleState)
7475
}
7576
}
7677
}
7778

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

@@ -84,7 +85,7 @@ class RunBackgroundTask(
8485

8586
coroutineScope {
8687
val uploadJob = async {
87-
uploadMissingMeasurements(resultId)
88+
uploadMissingMeasurements(filter)
8889
.collectLatest { uploadState ->
8990
updateState(RunBackgroundState.UploadingMissingResults(uploadState))
9091
}
@@ -101,7 +102,7 @@ class RunBackgroundTask(
101102
try {
102103
uploadJob.await()
103104
} catch (e: CancellationException) {
104-
Logger.i("Upload Missing Results (result=$resultId): cancelled")
105+
Logger.i("Upload Missing Results (filter=$filter): cancelled")
105106
}
106107
}
107108

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.ooni.probe.data.models
2+
3+
sealed interface MeasurementsFilter {
4+
data object All : MeasurementsFilter
5+
6+
data class Result(val resultId: ResultModel.Id) : MeasurementsFilter
7+
8+
data class Measurement(val measurementId: MeasurementModel.Id) : MeasurementsFilter
9+
}

composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/MeasurementRepository.kt

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package org.ooni.probe.data.repositories
22

33
import app.cash.sqldelight.coroutines.asFlow
44
import app.cash.sqldelight.coroutines.mapToList
5+
import app.cash.sqldelight.coroutines.mapToOne
56
import co.touchlab.kermit.Logger
67
import kotlinx.coroutines.flow.Flow
78
import kotlinx.coroutines.flow.map
@@ -10,6 +11,7 @@ import kotlinx.serialization.json.Json
1011
import org.ooni.engine.models.TestKeys
1112
import org.ooni.engine.models.TestType
1213
import org.ooni.probe.Database
14+
import org.ooni.probe.data.GetById
1315
import org.ooni.probe.data.Measurement
1416
import org.ooni.probe.data.SelectByResultIdWithUrl
1517
import org.ooni.probe.data.SelectTestKeysByDescriptorKey
@@ -81,6 +83,13 @@ class MeasurementRepository(
8183
.map { list -> list.mapNotNull { it.toModel() } }
8284
}
8385

86+
fun getById(measurementId: MeasurementModel.Id) =
87+
database.measurementQueries
88+
.getById(measurementId.value)
89+
.asFlow()
90+
.mapToOne(backgroundContext)
91+
.map { it.toModel() }
92+
8493
suspend fun createOrUpdate(model: MeasurementModel): MeasurementModel.Id =
8594
withContext(backgroundContext) {
8695
database.transactionWithResult {
@@ -176,6 +185,38 @@ class MeasurementRepository(
176185
)
177186
}
178187

188+
private fun GetById.toModel(): MeasurementWithUrl? {
189+
return MeasurementWithUrl(
190+
measurement = Measurement(
191+
id = id,
192+
test_name = test_name,
193+
start_time = start_time,
194+
runtime = runtime,
195+
is_done = is_done,
196+
is_uploaded = is_uploaded,
197+
is_failed = is_failed,
198+
failure_msg = failure_msg,
199+
is_upload_failed = is_upload_failed,
200+
upload_failure_msg = upload_failure_msg,
201+
is_rerun = is_rerun,
202+
is_anomaly = is_anomaly,
203+
report_id = report_id,
204+
test_keys = test_keys,
205+
rerun_network = rerun_network,
206+
url_id = url_id,
207+
result_id = result_id,
208+
).toModel() ?: return null,
209+
url = id_?.let { urlId ->
210+
Url(
211+
id = urlId,
212+
url = url,
213+
country_code = country_code,
214+
category_code = category_code,
215+
).toModel()
216+
},
217+
)
218+
}
219+
179220
private fun SelectTestKeysByDescriptorKey.toModel(): TestKeysWithResultId? {
180221
return TestKeysWithResultId(
181222
id = MeasurementModel.Id(id),

composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import org.ooni.probe.data.models.AutoRunParameters
3131
import org.ooni.probe.data.models.BatteryState
3232
import org.ooni.probe.data.models.InstalledTestDescriptorModel
3333
import org.ooni.probe.data.models.MeasurementModel
34+
import org.ooni.probe.data.models.MeasurementsFilter
3435
import org.ooni.probe.data.models.PlatformAction
3536
import org.ooni.probe.data.models.PreferenceCategoryKey
3637
import org.ooni.probe.data.models.ResultModel
@@ -54,6 +55,7 @@ import org.ooni.probe.domain.GetBootstrapTestDescriptors
5455
import org.ooni.probe.domain.GetDefaultTestDescriptors
5556
import org.ooni.probe.domain.GetEnginePreferences
5657
import org.ooni.probe.domain.GetFirstRun
58+
import org.ooni.probe.domain.GetMeasurementsNotUploaded
5759
import org.ooni.probe.domain.GetProxySettings
5860
import org.ooni.probe.domain.GetResult
5961
import org.ooni.probe.domain.GetResults
@@ -92,6 +94,7 @@ import org.ooni.probe.ui.descriptor.DescriptorViewModel
9294
import org.ooni.probe.ui.descriptor.add.AddDescriptorViewModel
9395
import org.ooni.probe.ui.descriptor.review.ReviewUpdatesViewModel
9496
import org.ooni.probe.ui.log.LogViewModel
97+
import org.ooni.probe.ui.measurement.MeasurementRawViewModel
9598
import org.ooni.probe.ui.measurement.MeasurementViewModel
9699
import org.ooni.probe.ui.onboarding.OnboardingViewModel
97100
import org.ooni.probe.ui.result.ResultViewModel
@@ -314,6 +317,12 @@ class Dependencies(
314317
)
315318
}
316319

320+
private val getMeasurementsNotUploaded by lazy {
321+
GetMeasurementsNotUploaded(
322+
listMeasurementsNotUploaded = measurementRepository::listNotUploaded,
323+
getMeasurementById = measurementRepository::getById,
324+
)
325+
}
317326
private val getSettings by lazy {
318327
GetSettings(
319328
preferencesRepository = preferenceRepository,
@@ -352,7 +361,6 @@ class Dependencies(
352361
getAutoRunSettings = getAutoRunSettings::invoke,
353362
)
354363
}
355-
356364
val observeAndConfigureAutoUpdate by lazy {
357365
ObserveAndConfigureAutoUpdate(
358366
backgroundContext = backgroundContext,
@@ -362,7 +370,6 @@ class Dependencies(
362370
startDescriptorsUpdate = startDescriptorsUpdate,
363371
)
364372
}
365-
366373
private val rejectDescriptorUpdate by lazy {
367374
RejectDescriptorUpdate(
368375
updateDescriptorRejectedRevision = testDescriptorRepository::updateRejectedRevision,
@@ -415,7 +422,7 @@ class Dependencies(
415422
}
416423
private val uploadMissingMeasurements by lazy {
417424
UploadMissingMeasurements(
418-
getMeasurementsNotUploaded = measurementRepository::listNotUploaded,
425+
getMeasurementsNotUploaded = getMeasurementsNotUploaded::invoke,
419426
submitMeasurement = engine::submitMeasurements,
420427
readFile = readFile,
421428
deleteFiles = deleteFiles,
@@ -591,12 +598,14 @@ class Dependencies(
591598
resultId: ResultModel.Id,
592599
onBack: () -> Unit,
593600
goToMeasurement: (MeasurementModel.ReportId, String?) -> Unit,
601+
goToMeasurementRaw: (MeasurementModel.Id) -> Unit,
594602
goToUpload: () -> Unit,
595603
goToDashboard: () -> Unit,
596604
) = ResultViewModel(
597605
resultId = resultId,
598606
onBack = onBack,
599607
goToMeasurement = goToMeasurement,
608+
goToMeasurementRaw = goToMeasurementRaw,
600609
goToUpload = goToUpload,
601610
goToDashboard = goToDashboard,
602611
getResult = getResult::invoke,
@@ -618,6 +627,21 @@ class Dependencies(
618627
isWebViewAvailable = isWebViewAvailable,
619628
)
620629

630+
fun measurementRawViewModel(
631+
measurementId: MeasurementModel.Id,
632+
onBack: () -> Unit,
633+
goToUpload: (MeasurementModel.Id) -> Unit,
634+
goToMeasurement: (MeasurementModel.ReportId, String?) -> Unit,
635+
) = MeasurementRawViewModel(
636+
measurementId = measurementId,
637+
onBack = onBack,
638+
goToUpload = goToUpload,
639+
goToMeasurement = goToMeasurement,
640+
getMeasurement = measurementRepository::getById,
641+
readFile = readFile,
642+
shareFile = { launchAction(it) },
643+
)
644+
621645
fun reviewUpdatesViewModel(
622646
descriptorIds: List<InstalledTestDescriptorModel.Id>?,
623647
onBack: () -> Unit,
@@ -651,10 +675,10 @@ class Dependencies(
651675
)
652676

653677
fun uploadMeasurementsViewModel(
654-
resultId: ResultModel.Id?,
678+
filter: MeasurementsFilter,
655679
onClose: () -> Unit,
656680
) = UploadMeasurementsViewModel(
657-
resultId = resultId,
681+
filter = filter,
658682
onClose = onClose,
659683
uploadMissingMeasurements = uploadMissingMeasurements::invoke,
660684
)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package org.ooni.probe.domain
2+
3+
import kotlinx.coroutines.flow.Flow
4+
import kotlinx.coroutines.flow.map
5+
import org.ooni.probe.data.models.MeasurementModel
6+
import org.ooni.probe.data.models.MeasurementWithUrl
7+
import org.ooni.probe.data.models.MeasurementsFilter
8+
import org.ooni.probe.data.models.ResultModel
9+
10+
class GetMeasurementsNotUploaded(
11+
private val listMeasurementsNotUploaded: (ResultModel.Id?) -> Flow<List<MeasurementModel>>,
12+
private val getMeasurementById: (MeasurementModel.Id) -> Flow<MeasurementWithUrl?>,
13+
) {
14+
fun invoke(filter: MeasurementsFilter): Flow<List<MeasurementModel>> =
15+
when (filter) {
16+
MeasurementsFilter.All -> listMeasurementsNotUploaded(null)
17+
18+
is MeasurementsFilter.Result -> listMeasurementsNotUploaded(filter.resultId)
19+
20+
is MeasurementsFilter.Measurement -> getMeasurementById(filter.measurementId).map {
21+
if (it != null && it.measurement.isDoneAndMissingUpload) {
22+
listOf(it.measurement)
23+
} else {
24+
emptyList()
25+
}
26+
}
27+
}
28+
}

composeApp/src/commonMain/kotlin/org/ooni/probe/domain/UploadMissingMeasurements.kt

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,28 +11,31 @@ import org.ooni.engine.models.Result
1111
import org.ooni.probe.data.disk.DeleteFiles
1212
import org.ooni.probe.data.disk.ReadFile
1313
import org.ooni.probe.data.models.MeasurementModel
14-
import org.ooni.probe.data.models.ResultModel
14+
import org.ooni.probe.data.models.MeasurementsFilter
1515
import org.ooni.probe.shared.monitoring.Instrumentation
1616

1717
class UploadMissingMeasurements(
18-
private val getMeasurementsNotUploaded: (ResultModel.Id?) -> Flow<List<MeasurementModel>>,
18+
private val getMeasurementsNotUploaded: (MeasurementsFilter) -> Flow<List<MeasurementModel>>,
1919
private val submitMeasurement: suspend (String) -> Result<SubmitMeasurementResults, MkException>,
2020
private val readFile: ReadFile,
2121
private val deleteFiles: DeleteFiles,
2222
private val updateMeasurement: suspend (MeasurementModel) -> Unit,
2323
private val deleteMeasurementById: suspend (MeasurementModel.Id) -> Unit,
2424
) {
25-
operator fun invoke(resultId: ResultModel.Id? = null): Flow<State> =
25+
operator fun invoke(filter: MeasurementsFilter): Flow<State> =
2626
channelFlow {
2727
Instrumentation.withTransaction(
2828
operation = this@UploadMissingMeasurements::class.simpleName.orEmpty(),
2929
data = mapOf(
30-
"resultId" to resultId?.value.toString(),
30+
"resultId" to
31+
(filter as? MeasurementsFilter.Result)?.resultId?.value.toString(),
32+
"measurementId" to
33+
(filter as? MeasurementsFilter.Measurement)?.measurementId?.value.toString(),
3134
),
3235
) {
3336
send(State.Starting)
3437

35-
val measurements = getMeasurementsNotUploaded(resultId).first()
38+
val measurements = getMeasurementsNotUploaded(filter).first()
3639
val total = measurements.size
3740
var uploaded = 0
3841
var failedToUpload = 0

0 commit comments

Comments
 (0)