diff --git a/composeApp/src/androidMain/kotlin/org/ooni/probe/background/RunWorker.kt b/composeApp/src/androidMain/kotlin/org/ooni/probe/background/RunWorker.kt index cb4e5a98..becd5f52 100644 --- a/composeApp/src/androidMain/kotlin/org/ooni/probe/background/RunWorker.kt +++ b/composeApp/src/androidMain/kotlin/org/ooni/probe/background/RunWorker.kt @@ -22,9 +22,9 @@ import kotlinx.serialization.encodeToString 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_ResultsNotUploaded_Uploading import ooniprobe.composeapp.generated.resources.Notification_StopTest import ooniprobe.composeapp.generated.resources.Res +import ooniprobe.composeapp.generated.resources.UploadingMissingResults import ooniprobe.composeapp.generated.resources.notification_channel_name import org.jetbrains.compose.resources.getString import org.ooni.probe.AndroidApplication @@ -143,7 +143,7 @@ class RunWorker( val progress = state.uploaded + state.failedToUpload + 1 setContentText( getString( - Res.string.Modal_ResultsNotUploaded_Uploading, + Res.string.UploadingMissingResults, "$progress/${state.total}", ), ) diff --git a/composeApp/src/commonMain/composeResources/values/strings-common.xml b/composeApp/src/commonMain/composeResources/values/strings-common.xml index 2199b650..54499155 100644 --- a/composeApp/src/commonMain/composeResources/values/strings-common.xml +++ b/composeApp/src/commonMain/composeResources/values/strings-common.xml @@ -296,4 +296,5 @@ Maximum Websites test duration Tests will run every hour in the background Only for manual runs + Uploading missing results %1$s diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/App.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/App.kt index dcfcc190..cafd010c 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/App.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/App.kt @@ -24,6 +24,7 @@ import ooniprobe.composeapp.generated.resources.Res import org.jetbrains.compose.resources.getString import org.jetbrains.compose.ui.tooling.preview.Preview import org.ooni.probe.data.models.DeepLink +import org.ooni.probe.data.models.RunSpecification import org.ooni.probe.di.Dependencies import org.ooni.probe.shared.PlatformInfo import org.ooni.probe.ui.navigation.BottomNavigationBar @@ -99,6 +100,7 @@ fun App( dependencies.bootstrapPreferences() dependencies.configureDescriptorAutoUpdate() dependencies.fetchDescriptorUpdate(null) + dependencies.startSingleRunInner(RunSpecification.OnlyUploadMissingResults) } LaunchedEffect(Unit) { dependencies.finishInProgressData() diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/background/RunBackgroundTask.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/background/RunBackgroundTask.kt index 62889795..8a30e959 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/background/RunBackgroundTask.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/background/RunBackgroundTask.kt @@ -28,8 +28,8 @@ class RunBackgroundTask( private val uploadMissingMeasurements: (ResultModel.Id?) -> Flow, private val checkSkipAutoRunNotUploadedLimit: () -> Flow, private val getNetworkType: () -> NetworkType, - private val getAutoRunSpecification: suspend () -> RunSpecification, - private val runDescriptors: suspend (RunSpecification) -> Unit, + private val getAutoRunSpecification: suspend () -> RunSpecification.Full, + private val runDescriptors: suspend (RunSpecification.Full) -> Unit, private val setRunBackgroundState: ((RunBackgroundState) -> RunBackgroundState) -> Unit, private val getRunBackgroundState: () -> Flow, private val addRunCancelListener: (() -> Unit) -> Unit, @@ -44,52 +44,57 @@ class RunBackgroundTask( return@channelFlow } - val uploadCancelled = uploadMissingResults(isAutoRun = spec == null) - if (uploadCancelled) return@channelFlow + val isAutoRun = spec == null + if (isAutoRun || spec is RunSpecification.OnlyUploadMissingResults) { + val uploadCancelled = uploadMissingResults() + if (uploadCancelled) return@channelFlow + } + + if (spec is RunSpecification.OnlyUploadMissingResults) { + setRunBackgroundState { RunBackgroundState.Idle() } + return@channelFlow + } - runTests(spec) + runTests(spec as? RunSpecification.Full) // When a test is cancelled, sometimes the last measurement isn't uploaded getLatestResult().first()?.id.let { latestResultId -> val idleState = getRunBackgroundState().first() - uploadMissingResults(isAutoRun = spec == null, resultId = latestResultId) + uploadMissingResults(resultId = latestResultId) updateState(idleState) } }.onCompletion { clearRunCancelListeners() } - private suspend fun ProducerScope.uploadMissingResults( - isAutoRun: Boolean, - resultId: ResultModel.Id? = null, - ): Boolean { + private suspend fun ProducerScope.uploadMissingResults(resultId: ResultModel.Id? = null): Boolean { val autoUpload = getPreferenceValueByKey(SettingsKey.UPLOAD_RESULTS).first() == true - var isCancelled = false + if (!autoUpload) return false - if ((isAutoRun || resultId != null) && autoUpload) { - coroutineScope { - val uploadJob = async { - uploadMissingMeasurements(resultId) - .collectLatest { uploadState -> - updateState(RunBackgroundState.UploadingMissingResults(uploadState)) - } - } + var isCancelled = false - addRunCancelListener { - isCancelled = true - if (uploadJob.isActive) uploadJob.cancel() - CoroutineScope(Dispatchers.Default).launch { - updateState(RunBackgroundState.Stopping) + coroutineScope { + val uploadJob = async { + uploadMissingMeasurements(resultId) + .collectLatest { uploadState -> + updateState(RunBackgroundState.UploadingMissingResults(uploadState)) } - } + } - try { - uploadJob.await() - } catch (e: CancellationException) { - Logger.i("Upload Missing Results (result=$resultId): cancelled") + addRunCancelListener { + isCancelled = true + if (uploadJob.isActive) uploadJob.cancel() + CoroutineScope(Dispatchers.Default).launch { + updateState(RunBackgroundState.Stopping) } } + + try { + uploadJob.await() + } catch (e: CancellationException) { + Logger.i("Upload Missing Results (result=$resultId): cancelled") + } } if (isCancelled) { @@ -100,7 +105,7 @@ class RunBackgroundTask( return false } - private suspend fun ProducerScope.runTests(spec: RunSpecification?) { + private suspend fun ProducerScope.runTests(spec: RunSpecification.Full?) { if (checkSkipAutoRunNotUploadedLimit().first()) { Logger.i("Skipping auto-run tests: too many not-uploaded results") return diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/RunSpecification.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/RunSpecification.kt index 6b705764..3c2ffd18 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/RunSpecification.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/RunSpecification.kt @@ -4,11 +4,17 @@ import kotlinx.serialization.Serializable import org.ooni.engine.models.TaskOrigin @Serializable -data class RunSpecification( - val tests: List, - val taskOrigin: TaskOrigin, - val isRerun: Boolean, -) { +sealed interface RunSpecification { + @Serializable + data object OnlyUploadMissingResults : RunSpecification + + @Serializable + data class Full( + val tests: List, + val taskOrigin: TaskOrigin, + val isRerun: Boolean, + ) : RunSpecification + @Serializable data class Test( val source: Source, diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt index 1867688d..1e5a9952 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt @@ -106,7 +106,7 @@ class Dependencies( @VisibleForTesting val buildDataStore: () -> DataStore, private val isBatteryCharging: () -> Boolean, - private val startSingleRunInner: (RunSpecification) -> Unit, + val startSingleRunInner: (RunSpecification) -> Unit, private val configureAutoRun: suspend (AutoRunParameters) -> Unit, val configureDescriptorAutoUpdate: suspend () -> Boolean, val fetchDescriptorUpdate: suspend (List?) -> Unit, diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetAutoRunSpecification.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetAutoRunSpecification.kt index 4f5eaa7f..47455162 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetAutoRunSpecification.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetAutoRunSpecification.kt @@ -11,10 +11,10 @@ class GetAutoRunSpecification( private val getDescriptors: GetTestDescriptors, private val preferenceRepository: PreferenceRepository, ) { - suspend operator fun invoke(): RunSpecification { + suspend operator fun invoke(): RunSpecification.Full { val descriptors = getDescriptors().first().filterForAutoRun() - return RunSpecification( + return RunSpecification.Full( tests = descriptors.map { descriptor -> RunSpecification.Test( source = RunSpecification.Test.Source.fromDescriptor(descriptor), diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetTestDescriptorsBySpec.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetTestDescriptorsBySpec.kt index 4ec91cd5..89017c85 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetTestDescriptorsBySpec.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetTestDescriptorsBySpec.kt @@ -9,7 +9,7 @@ import org.ooni.probe.data.models.RunSpecification class GetTestDescriptorsBySpec( private val getTestDescriptors: () -> Flow>, ) { - suspend operator fun invoke(spec: RunSpecification): List = + suspend operator fun invoke(spec: RunSpecification.Full): List = getTestDescriptors() .first() .filterNot { it.isExpired } @@ -30,7 +30,7 @@ class GetTestDescriptorsBySpec( } // Is this descriptor contained in the RunSpecification's list of tests - private fun RunSpecification.forDescriptor(descriptor: Descriptor) = + private fun RunSpecification.Full.forDescriptor(descriptor: Descriptor) = tests.firstOrNull { specTest -> when (descriptor.source) { is Descriptor.Source.Default -> { diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/RunDescriptors.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/RunDescriptors.kt index 529a47d1..58447056 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/RunDescriptors.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/RunDescriptors.kt @@ -20,7 +20,7 @@ import org.ooni.probe.shared.now import kotlin.time.Duration class RunDescriptors( - private val getTestDescriptorsBySpec: suspend (RunSpecification) -> List, + private val getTestDescriptorsBySpec: suspend (RunSpecification.Full) -> List, private val downloadUrls: suspend (TaskOrigin) -> Result, MkException>, private val storeResult: suspend (ResultModel) -> ResultModel.Id, private val markResultAsDone: suspend (ResultModel.Id) -> Unit, @@ -32,7 +32,7 @@ class RunDescriptors( private val getEnginePreferences: suspend () -> EnginePreferences, private val finishInProgressData: suspend () -> Unit, ) { - suspend operator fun invoke(spec: RunSpecification) { + suspend operator fun invoke(spec: RunSpecification.Full) { setRunBackgroundState { RunBackgroundState.RunningTests() } val descriptors = getTestDescriptorsBySpec(spec) @@ -55,7 +55,7 @@ class RunDescriptors( private suspend fun runDescriptorsCancellable( descriptors: List, - spec: RunSpecification, + spec: RunSpecification.Full, ) { addRunCancelListener { setRunBackgroundState { RunBackgroundState.Stopping } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/choosewebsites/ChooseWebsitesViewModel.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/choosewebsites/ChooseWebsitesViewModel.kt index 88cc5369..349f2ab1 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/choosewebsites/ChooseWebsitesViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/choosewebsites/ChooseWebsitesViewModel.kt @@ -101,7 +101,7 @@ class ChooseWebsitesViewModel( } startBackgroundRun( - RunSpecification( + RunSpecification.Full( tests = listOf( RunSpecification.Test( source = RunSpecification.Test.Source.Default("websites"), diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/RunBackgroundStateSection.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/RunBackgroundStateSection.kt index 600af177..b73e0075 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/RunBackgroundStateSection.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/RunBackgroundStateSection.kt @@ -27,9 +27,9 @@ 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_ResultsNotUploaded_Uploading import ooniprobe.composeapp.generated.resources.OONIRun_Run import ooniprobe.composeapp.generated.resources.Res +import ooniprobe.composeapp.generated.resources.UploadingMissingResults import ooniprobe.composeapp.generated.resources.ic_timer import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource @@ -104,7 +104,7 @@ fun RunBackgroundStateSection( val progress = uploadState.uploaded + uploadState.failedToUpload + 1 Text( text = stringResource( - Res.string.Modal_ResultsNotUploaded_Uploading, + Res.string.UploadingMissingResults, "$progress/${uploadState.total}", ), style = MaterialTheme.typography.bodyLarge, diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultViewModel.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultViewModel.kt index 7c877b9d..d481da49 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultViewModel.kt @@ -103,7 +103,7 @@ class ResultViewModel( private fun getRerunSpecification(): RunSpecification? { val item = _state.value.result ?: return null - return RunSpecification( + return RunSpecification.Full( tests = listOf( RunSpecification.Test( source = RunSpecification.Test.Source.fromDescriptor(item.descriptor), diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/run/RunViewModel.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/run/RunViewModel.kt index 86941bb1..d37e8c4d 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/run/RunViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/run/RunViewModel.kt @@ -224,7 +224,7 @@ class RunViewModel( .filter { it.isSelected } .map { it.item } } - return RunSpecification( + return RunSpecification.Full( tests = selectedTests.map { (descriptor, tests) -> RunSpecification.Test( diff --git a/composeApp/src/commonTest/kotlin/org/ooni/probe/ui/choosewebsites/ChooseWebsitesTest.kt b/composeApp/src/commonTest/kotlin/org/ooni/probe/ui/choosewebsites/ChooseWebsitesTest.kt index dcafeca0..7738b96a 100644 --- a/composeApp/src/commonTest/kotlin/org/ooni/probe/ui/choosewebsites/ChooseWebsitesTest.kt +++ b/composeApp/src/commonTest/kotlin/org/ooni/probe/ui/choosewebsites/ChooseWebsitesTest.kt @@ -45,7 +45,7 @@ class ChooseWebsitesTest { } onNodeWithText("Test ${websites.size} URLs").performClick() - val spec = runSpec + val spec = runSpec as? RunSpecification.Full assertNotNull(spec) assertEquals(false, spec.isRerun) assertEquals(TaskOrigin.OoniRun, spec.taskOrigin)