diff --git a/README.md b/README.md index 63806833..cabbf0d7 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ Multiplatform (Android and iOS currently) version of the Probe app. +![Validate](https://github.com/ooni/probe-multiplatform/actions/workflows/validate.yml/badge.svg) +![Android Instrumented Tests](https://github.com/ooni/probe-multiplatform/actions/workflows/instrumented-tests.yml/badge.svg) + ## Project structure * `composeApp` is for code that will be shared across your Compose Multiplatform applications. diff --git a/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/DescriptorsTest.kt b/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/DescriptorsTest.kt index 8ebf9bf4..1122f81b 100644 --- a/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/DescriptorsTest.kt +++ b/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/DescriptorsTest.kt @@ -7,7 +7,6 @@ import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.hasText import androidx.compose.ui.test.isDisplayed import androidx.compose.ui.test.junit4.createEmptyComposeRule -import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onLast import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText diff --git a/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/RunningTestsTest.kt b/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/RunningTestsTest.kt index 358f934c..ffff25fd 100644 --- a/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/RunningTestsTest.kt +++ b/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/RunningTestsTest.kt @@ -2,6 +2,7 @@ package org.ooni.probe.uitesting import androidx.compose.ui.test.hasText import androidx.compose.ui.test.isDisplayed +import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onNodeWithTag @@ -14,6 +15,7 @@ import kotlinx.coroutines.test.runTest import ooniprobe.composeapp.generated.resources.Dashboard_RunTests_RunButton_Label import ooniprobe.composeapp.generated.resources.Dashboard_RunTests_SelectNone import ooniprobe.composeapp.generated.resources.Dashboard_RunV2_RunFinished +import ooniprobe.composeapp.generated.resources.Measurement_Title import ooniprobe.composeapp.generated.resources.OONIRun_Run import ooniprobe.composeapp.generated.resources.Res import ooniprobe.composeapp.generated.resources.Test_Circumvention_Fullname @@ -22,16 +24,15 @@ import ooniprobe.composeapp.generated.resources.Test_InstantMessaging_Fullname import ooniprobe.composeapp.generated.resources.Test_Performance_Fullname import ooniprobe.composeapp.generated.resources.Test_Psiphon_Fullname import ooniprobe.composeapp.generated.resources.Test_Signal_Fullname -import ooniprobe.composeapp.generated.resources.measurement -import org.jetbrains.compose.resources.getPluralString import org.junit.Before import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.ooni.probe.data.models.SettingsKey -import org.ooni.probe.uitesting.helpers.checkLinkInsideWebView +import org.ooni.probe.shared.getPluralStringResourceItem import org.ooni.probe.uitesting.helpers.checkSummaryInsideWebView +import org.ooni.probe.uitesting.helpers.checkTextAnywhereInsideWebView import org.ooni.probe.uitesting.helpers.clickOnText import org.ooni.probe.uitesting.helpers.isNewsMediaScan import org.ooni.probe.uitesting.helpers.isOoni @@ -64,13 +65,13 @@ class RunningTestsTest { clickOnText(Res.string.Dashboard_RunTests_SelectNone) clickOnText(Res.string.Test_Signal_Fullname) - clickOnText(getPluralString(Res.plurals.Dashboard_RunTests_RunButton_Label, 1, 1)) + clickOnRunButton(1) clickOnText(Res.string.Dashboard_RunV2_RunFinished, timeout = TEST_WAIT_TIMEOUT) clickOnText(Res.string.Test_InstantMessaging_Fullname) clickOnText(Res.string.Test_Signal_Fullname) - wait { onNodeWithText(Res.string.measurement).isDisplayed() } + wait { onNodeWithText(Res.string.Measurement_Title).isDisplayed() } checkSummaryInsideWebView("Signal") } } @@ -84,13 +85,13 @@ class RunningTestsTest { clickOnText(Res.string.Dashboard_RunTests_SelectNone) clickOnText(Res.string.Test_Psiphon_Fullname) - clickOnText(getPluralString(Res.plurals.Dashboard_RunTests_RunButton_Label, 1, 1)) + clickOnRunButton(1) clickOnText(Res.string.Dashboard_RunV2_RunFinished, timeout = TEST_WAIT_TIMEOUT) clickOnText(Res.string.Test_Circumvention_Fullname) clickOnText(Res.string.Test_Psiphon_Fullname) - wait { onNodeWithText(Res.string.measurement).isDisplayed() } + wait { onNodeWithText(Res.string.Measurement_Title).isDisplayed() } checkSummaryInsideWebView("Psiphon") } } @@ -107,13 +108,13 @@ class RunningTestsTest { .performScrollToNode(hasText("HTTP Header", substring = true)) .performTouchInput { swipeUp() } clickOnText("HTTP Header", substring = true) - clickOnText(getPluralString(Res.plurals.Dashboard_RunTests_RunButton_Label, 1, 1)) + clickOnRunButton(1) clickOnText(Res.string.Dashboard_RunV2_RunFinished, timeout = TEST_WAIT_TIMEOUT) clickOnText(Res.string.Test_Performance_Fullname) clickOnText("HTTP Header", substring = true) - wait { onNodeWithText(Res.string.measurement).isDisplayed() } + wait { onNodeWithText(Res.string.Measurement_Title).isDisplayed() } checkSummaryInsideWebView("middleboxes") } } @@ -130,17 +131,14 @@ class RunningTestsTest { .performScrollToNode(hasText("stunreachability")) .performTouchInput { swipeUp() } clickOnText("stunreachability", substring = true) - clickOnText(getPluralString(Res.plurals.Dashboard_RunTests_RunButton_Label, 1, 1)) + clickOnRunButton(1) clickOnText(Res.string.Dashboard_RunV2_RunFinished, timeout = TEST_WAIT_TIMEOUT) clickOnText(Res.string.Test_Experimental_Fullname) compose.onAllNodesWithText("stunreachability")[0].performClick() - wait { onNodeWithText(Res.string.measurement).isDisplayed() } - checkLinkInsideWebView( - "https://ooni.org/nettest/http-requests/", - "STUN Reachability", - ) + wait { onNodeWithText(Res.string.Measurement_Title).isDisplayed() } + checkTextAnywhereInsideWebView("stunreachability") } } @@ -154,17 +152,27 @@ class RunningTestsTest { clickOnText(Res.string.Dashboard_RunTests_SelectNone) clickOnText("Trusted International Media") - clickOnText(getPluralString(Res.plurals.Dashboard_RunTests_RunButton_Label, 1, 1)) + clickOnRunButton(1) clickOnText(Res.string.Dashboard_RunV2_RunFinished, timeout = TEST_WAIT_TIMEOUT) clickOnText("Trusted International Media") clickOnText("https://www.dw.com") - wait { onNodeWithText(Res.string.measurement).isDisplayed() } + wait { onNodeWithText(Res.string.Measurement_Title).isDisplayed() } checkSummaryInsideWebView("https://www.dw.com") } } + private suspend fun ComposeTestRule.clickOnRunButton(quantity: Int) { + clickOnText( + getPluralStringResourceItem( + Res.plurals.Dashboard_RunTests_RunButton_Label, + quantity, + quantity, + ), + ) + } + companion object { private val TEST_WAIT_TIMEOUT = 3.minutes } diff --git a/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/SettingsTest.kt b/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/SettingsTest.kt index 5746055b..8664f18b 100644 --- a/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/SettingsTest.kt +++ b/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/SettingsTest.kt @@ -7,6 +7,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import ooniprobe.composeapp.generated.resources.CategoryCode_ANON_Name +import ooniprobe.composeapp.generated.resources.Common_Back import ooniprobe.composeapp.generated.resources.Res import ooniprobe.composeapp.generated.resources.Settings_Advanced_DebugLogs import ooniprobe.composeapp.generated.resources.Settings_Advanced_Label @@ -25,7 +26,6 @@ import ooniprobe.composeapp.generated.resources.Settings_Title import ooniprobe.composeapp.generated.resources.Settings_WarmVPNInUse_Label import ooniprobe.composeapp.generated.resources.Settings_Websites_Categories_Description import ooniprobe.composeapp.generated.resources.Settings_Websites_Categories_Label -import ooniprobe.composeapp.generated.resources.back import org.jetbrains.compose.resources.getString import org.junit.Assert.assertTrue import org.junit.Before @@ -126,7 +126,7 @@ class SettingsTest { clickOnText(Res.string.Settings_Websites_Categories_Label) clickOnText(Res.string.CategoryCode_ANON_Name) - clickOnContentDescription(Res.string.back) + clickOnContentDescription(Res.string.Common_Back) onNodeWithText(getString(Res.string.Settings_Websites_Categories_Description, 1)) .assertIsDisplayed() diff --git a/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/UploadResultTest.kt b/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/UploadResultTest.kt index e319b44b..46592b4e 100644 --- a/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/UploadResultTest.kt +++ b/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/UploadResultTest.kt @@ -9,13 +9,13 @@ import kotlinx.coroutines.test.runTest import ooniprobe.composeapp.generated.resources.Dashboard_RunTests_RunButton_Label import ooniprobe.composeapp.generated.resources.Dashboard_RunTests_SelectNone import ooniprobe.composeapp.generated.resources.Dashboard_RunV2_RunFinished +import ooniprobe.composeapp.generated.resources.Measurement_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.Snackbar_ResultsSomeNotUploaded_UploadAll import ooniprobe.composeapp.generated.resources.Test_InstantMessaging_Fullname import ooniprobe.composeapp.generated.resources.Test_Signal_Fullname -import ooniprobe.composeapp.generated.resources.measurement import org.jetbrains.compose.resources.getPluralString import org.jetbrains.compose.resources.getString import org.junit.Before @@ -75,7 +75,7 @@ class UploadResultTest { clickOnText(Res.string.Test_Signal_Fullname) - wait { onNodeWithText(Res.string.measurement).isDisplayed() } + wait { onNodeWithText(Res.string.Measurement_Title).isDisplayed() } checkSummaryInsideWebView("Signal") } } @@ -105,7 +105,7 @@ class UploadResultTest { clickOnText("https://www.dw.com") - wait { onNodeWithText(Res.string.measurement).isDisplayed() } + wait { onNodeWithText(Res.string.Measurement_Title).isDisplayed() } checkSummaryInsideWebView("https://www.dw.com") } } diff --git a/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/helpers/ComposeTestHelpers.kt b/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/helpers/ComposeTestHelpers.kt index af34dff2..7e979284 100644 --- a/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/helpers/ComposeTestHelpers.kt +++ b/composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/helpers/ComposeTestHelpers.kt @@ -91,13 +91,10 @@ fun ComposeTestRule.checkSummaryInsideWebView(text: String) { } } -fun ComposeTestRule.checkLinkInsideWebView( - link: String, - text: String, -) { +fun ComposeTestRule.checkTextAnywhereInsideWebView(text: String) { waitAssertion(WEBSITE_WAIT_TIMEOUT) { onWebView() - .withElement(findElement(Locator.CSS_SELECTOR, "a[href=\"$link\"")) + .withElement(findElement(Locator.CSS_SELECTOR, "*")) .check(webMatches(getText(), containsString(text))) } } diff --git a/composeApp/src/androidMain/kotlin/org/ooni/engine/AndroidOonimkallBridge.kt b/composeApp/src/androidMain/kotlin/org/ooni/engine/AndroidOonimkallBridge.kt index f568dfab..3bc19da3 100644 --- a/composeApp/src/androidMain/kotlin/org/ooni/engine/AndroidOonimkallBridge.kt +++ b/composeApp/src/androidMain/kotlin/org/ooni/engine/AndroidOonimkallBridge.kt @@ -49,6 +49,10 @@ class AndroidOonimkallBridge : OonimkallBridge { val response = session.httpDo(context, request.toMk()) return OonimkallBridge.HTTPResponse(body = response.body) } + + override fun close() { + session.close() + } } } diff --git a/composeApp/src/androidMain/kotlin/org/ooni/probe/background/AppWorkerManager.kt b/composeApp/src/androidMain/kotlin/org/ooni/probe/background/AppWorkerManager.kt index be6a9963..6e2f98ca 100644 --- a/composeApp/src/androidMain/kotlin/org/ooni/probe/background/AppWorkerManager.kt +++ b/composeApp/src/androidMain/kotlin/org/ooni/probe/background/AppWorkerManager.kt @@ -62,6 +62,7 @@ class AppWorkerManager( suspend fun configureDescriptorAutoUpdate(): Boolean { return withContext(backgroundDispatcher) { val request = PeriodicWorkRequestBuilder(1, TimeUnit.DAYS) + .setInitialDelay(1, TimeUnit.DAYS) // avoid immediate start .build() workManager.enqueueUniquePeriodicWork( DescriptorUpdateWorker.AutoUpdateWorkerName, diff --git a/composeApp/src/androidMain/kotlin/org/ooni/probe/background/DescriptorUpdateWorker.kt b/composeApp/src/androidMain/kotlin/org/ooni/probe/background/DescriptorUpdateWorker.kt index afac9b82..23b4825c 100644 --- a/composeApp/src/androidMain/kotlin/org/ooni/probe/background/DescriptorUpdateWorker.kt +++ b/composeApp/src/androidMain/kotlin/org/ooni/probe/background/DescriptorUpdateWorker.kt @@ -12,12 +12,13 @@ import androidx.work.ForegroundInfo import androidx.work.WorkerParameters import androidx.work.workDataOf import co.touchlab.kermit.Logger -import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.first +import kotlinx.serialization.SerializationException import kotlinx.serialization.encodeToString import ooniprobe.composeapp.generated.resources.Dashboard_Running_Running +import ooniprobe.composeapp.generated.resources.Notification_ChannelName import ooniprobe.composeapp.generated.resources.Res -import ooniprobe.composeapp.generated.resources.notification_channel_name import org.jetbrains.compose.resources.getString import org.ooni.probe.AndroidApplication import org.ooni.probe.R @@ -41,11 +42,17 @@ class DescriptorUpdateWorker( } override suspend fun doWork(): Result { - return coroutineScope { - val descriptors = getDescriptors() ?: return@coroutineScope Result.failure() - if (descriptors.isEmpty()) return@coroutineScope Result.success(buildWorkData(descriptors.map { it.id })) + try { + val descriptors = getDescriptors() ?: return Result.failure() + if (descriptors.isEmpty()) { + Logger.i("Skipping DescriptorUpdateWorker: no descriptors to update") + return Result.success(buildWorkData(descriptors.map { it.id })) + } dependencies.getDescriptorUpdate.invoke(descriptors) - return@coroutineScope Result.success(buildWorkData(descriptors.map { it.id })) + return Result.success(buildWorkData(descriptors.map { it.id })) + } catch (e: CancellationException) { + Logger.w("DescriptorUpdateWorker: cancelled", e) + return Result.failure() } } @@ -53,9 +60,13 @@ class DescriptorUpdateWorker( val descriptorsJson = inputData.getString(DATA_KEY_DESCRIPTORS) if (descriptorsJson != null) { try { - val ids = json.decodeFromString>(descriptorsJson) + val ids = + json.decodeFromString>(descriptorsJson) return testDescriptorRepository.selectByRunIds(ids).first() - } catch (e: Exception) { + } catch (e: SerializationException) { + Logger.w("Could not start update worker: invalid configuration", e) + return null + } catch (e: IllegalArgumentException) { Logger.w("Could not start update worker: invalid configuration", e) return null } @@ -68,7 +79,7 @@ class DescriptorUpdateWorker( notificationManager.createNotificationChannel( NotificationChannel( NOTIFICATION_CHANNEL_ID, - getString(Res.string.notification_channel_name), + getString(Res.string.Notification_ChannelName), NotificationManager.IMPORTANCE_DEFAULT, ), ) 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..2223fed2 100644 --- a/composeApp/src/androidMain/kotlin/org/ooni/probe/background/RunWorker.kt +++ b/composeApp/src/androidMain/kotlin/org/ooni/probe/background/RunWorker.kt @@ -22,10 +22,10 @@ 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_ChannelName import ooniprobe.composeapp.generated.resources.Notification_StopTest import ooniprobe.composeapp.generated.resources.Res -import ooniprobe.composeapp.generated.resources.notification_channel_name +import ooniprobe.composeapp.generated.resources.Results_UploadingMissing import org.jetbrains.compose.resources.getString import org.ooni.probe.AndroidApplication import org.ooni.probe.MainActivity @@ -130,7 +130,7 @@ class RunWorker( notificationManager.createNotificationChannel( NotificationChannel( NOTIFICATION_CHANNEL_ID, - getString(Res.string.notification_channel_name), + getString(Res.string.Notification_ChannelName), NotificationManager.IMPORTANCE_LOW, ), ) @@ -143,7 +143,7 @@ class RunWorker( val progress = state.uploaded + state.failedToUpload + 1 setContentText( getString( - Res.string.Modal_ResultsNotUploaded_Uploading, + Res.string.Results_UploadingMissing, "$progress/${state.total}", ), ) diff --git a/composeApp/src/commonMain/composeResources/values/strings-common.xml b/composeApp/src/commonMain/composeResources/values/strings-common.xml index 39dc3d3e..da685069 100644 --- a/composeApp/src/commonMain/composeResources/values/strings-common.xml +++ b/composeApp/src/commonMain/composeResources/values/strings-common.xml @@ -232,63 +232,71 @@ OONI Probe cannot run automatically without battery optimization. Do you want to try again? - Last updated %1$s - Back - refresh - Measurement - - %1$d measurement - %1$d measurements - - - January - February - March - April - May - June - July - August - September - October - November - December - - %1$s ago - - %1$d minute - %1$d minutes - - - %1$d hour - %1$d hours - - %1$dh - %1$dm - %1$ds - Testing - Manual Run - Auto Run - Collapse - Expand - - Run %1$d test - Run %1$d tests - - All Types - All Sources - VPN - Go to Settings > General > VPN and disconnect from your VPN. - Failed - OK - Anomaly - Correct answer - Incorrect answer - Logs - Share Logs - Error sharing logs - Filter Logs - Only the last %1$d results are shown - Skip after this amount of results failed to upload + + Back + refresh + Collapse + Expand + %1$s ago + %1$d minute + %1$d minutes + %1$d hour + %1$d hours + %1$dh + %1$dm + %1$ds + + January + February + March + April + May + June + July + August + September + October + November + December + + Correct answer + Incorrect answer + + Last updated %1$s + + Run %1$d test + Run %1$d tests + Unsupported URL + + Measurement + + %1$d measurement + %1$d measurements + + Failed + OK + Anomaly + + All Types + All Sources + Only the last %1$d results are shown + Uploading missing results %1$s + + Logs + Share Logs + Error sharing logs + Filter Logs + Go to Settings > General > VPN and disconnect from your VPN. + Skip after this amount of results failed to upload + Results are automatically uploaded to OONI explorer + Limit Websites test duration + Maximum Websites test duration + Tests will run every hour in the background + Only for manual runs + + Testing + Manual Run + Auto Run + VPN diff --git a/composeApp/src/commonMain/composeResources/values/untraslatable.xml b/composeApp/src/commonMain/composeResources/values/untraslatable.xml index 9bd0951e..fea55471 100644 --- a/composeApp/src/commonMain/composeResources/values/untraslatable.xml +++ b/composeApp/src/commonMain/composeResources/values/untraslatable.xml @@ -1,5 +1,39 @@ - mailto:bugs@openobservatory.org + bugs@openobservatory.org [bug-report] OONI Probe %1$s %1$s: %2$s + + + @string/Common_Minutes_One + @string/Common_Minutes_Other + + + @string/Common_Hour_One + @string/Common_Hour_Other + + + + @string/Dashboard_RunTests_RunButton_Label_One + @string/Dashboard_RunTests_RunButton_Label_Other + + + + @string/Measurements_Count_One + @string/Measurements_Count_Other + + + + @string/Common_Months_January + @string/Common_Months_February + @string/Common_Months_March + @string/Common_Months_April + @string/Common_Months_May + @string/Common_Months_June + @string/Common_Months_July + @string/Common_Months_August + @string/Common_Months_September + @string/Common_Months_October + @string/Common_Months_November + @string/Common_Months_December + diff --git a/composeApp/src/commonMain/debug/kotlin/org/ooni/probe/config/BuildTypeDefaults.kt b/composeApp/src/commonMain/debug/kotlin/org/ooni/probe/config/BuildTypeDefaults.kt new file mode 100644 index 00000000..4aca2826 --- /dev/null +++ b/composeApp/src/commonMain/debug/kotlin/org/ooni/probe/config/BuildTypeDefaults.kt @@ -0,0 +1,8 @@ +package org.ooni.probe.config + +object BuildTypeDefaults : BuildTypeDefaultsInterface { + override val ooniApiBaseUrl = "https://api.dev.ooni.io" + override val ooniRunDomain = "run.test.ooni.org" + override val ooniRunDashboardUrl = "https://run.test.ooni.org" + override val explorerUrl = "https://explorer.ooni.org" +} diff --git a/composeApp/src/commonMain/debug/kotlin/org/ooni/probe/config/OrganizationConfigInterface.kt b/composeApp/src/commonMain/debug/kotlin/org/ooni/probe/config/OrganizationConfigInterface.kt deleted file mode 100644 index 1d38bf95..00000000 --- a/composeApp/src/commonMain/debug/kotlin/org/ooni/probe/config/OrganizationConfigInterface.kt +++ /dev/null @@ -1,26 +0,0 @@ -package org.ooni.probe.config - - -interface OrganizationConfigInterface { - val baseSoftwareName: String - - val ooniApiBaseUrl: String - get() = "https://api.ooni.org" - - val ooniRunDomain: String - get() = "run.ooni.org" - - val ooniRunDashboardUrl: String - get() = "https://run.ooni.org" - - val explorerUrl: String - get() = "https://explorer.test.ooni.org" - - val testDisplayMode: TestDisplayMode - - val autorunTaskId: String - - val onboardingImages: OnboardingImages - - val updateDescriptorTaskId: String -} diff --git a/composeApp/src/commonMain/kotlin/org/ooni/engine/Engine.kt b/composeApp/src/commonMain/kotlin/org/ooni/engine/Engine.kt index f06299af..fd86a5af 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/engine/Engine.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/engine/Engine.kt @@ -52,15 +52,19 @@ class Engine( val settingsSerialized = json.encodeToString(taskSettings) var task: OonimkallBridge.Task? = null + var isCancelled = false try { task = bridge.startTask(settingsSerialized) - addRunCancelListener { task.interrupt() } + addRunCancelListener { + isCancelled = true + task.interrupt() + } while (!task.isDone() && isActive) { val eventJson = task.waitForNextEvent() val taskEventResult = json.decodeFromString(eventJson) - taskEventMapper(taskEventResult)?.let { send(it) } + taskEventMapper(taskEventResult, isCancelled)?.let { send(it) } } } catch (e: Exception) { Logger.d("Error while running task", e) @@ -81,22 +85,32 @@ class Engine( session(sessionConfig).submitMeasurement(measurement) }.mapError { MkException(it) } - suspend fun checkIn(taskOrigin: TaskOrigin): Result = - resultOf(backgroundContext) { + suspend fun checkIn(taskOrigin: TaskOrigin): Result { + return resultOf(backgroundContext) { val preferences = getEnginePreferences() val sessionConfig = buildSessionConfig(taskOrigin, preferences) - session(sessionConfig).checkIn( - OonimkallBridge.CheckInConfig( - charging = isBatteryCharging(), - onWiFi = networkTypeFinder() == NetworkType.Wifi, - platform = platformInfo.platform.value, - runType = taskOrigin.value, - softwareName = sessionConfig.softwareName, - softwareVersion = sessionConfig.softwareVersion, - webConnectivityCategories = preferences.enabledWebCategories, - ), - ) + val session = session(sessionConfig) + try { + session.checkIn( + OonimkallBridge.CheckInConfig( + charging = isBatteryCharging(), + onWiFi = networkTypeFinder() == NetworkType.Wifi, + platform = platformInfo.platform.value, + runType = taskOrigin.value, + softwareName = sessionConfig.softwareName, + softwareVersion = sessionConfig.softwareVersion, + webConnectivityCategories = preferences.enabledWebCategories, + ), + ) + } finally { + try { + session.close() + } catch (e: Exception) { + Logger.w("Error closing session", e) + } + } }.mapError { MkException(it) } + } suspend fun httpDo( method: String, diff --git a/composeApp/src/commonMain/kotlin/org/ooni/engine/OonimkallBridge.kt b/composeApp/src/commonMain/kotlin/org/ooni/engine/OonimkallBridge.kt index 99624984..7a8e2d23 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/engine/OonimkallBridge.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/engine/OonimkallBridge.kt @@ -45,6 +45,10 @@ interface OonimkallBridge { @Throws(Exception::class) fun httpDo(request: HTTPRequest): HTTPResponse + + // Needs to be called after `checkIn` + @Throws(Exception::class) + fun close() } data class SubmitMeasurementResults( diff --git a/composeApp/src/commonMain/kotlin/org/ooni/engine/TaskEventMapper.kt b/composeApp/src/commonMain/kotlin/org/ooni/engine/TaskEventMapper.kt index 09a4ba99..71d2fd02 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/engine/TaskEventMapper.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/engine/TaskEventMapper.kt @@ -9,7 +9,10 @@ class TaskEventMapper( private val networkTypeFinder: NetworkTypeFinder, private val json: Json, ) { - operator fun invoke(result: TaskEventResult): TaskEvent? { + operator fun invoke( + result: TaskEventResult, + isCancelled: Boolean = false, + ): TaskEvent? { val key = result.key val value = result.value @@ -33,6 +36,7 @@ class TaskEventMapper( TaskEvent.ResolverLookupFailure( message = value.failure, value = value, + isCancelled = isCancelled, ) } ?: run { Logger.d("Task Event $key missing 'value'") @@ -44,6 +48,7 @@ class TaskEventMapper( TaskEvent.StartupFailure( message = value.failure, value = value, + isCancelled = isCancelled, ) } ?: run { Logger.d("Task Event $key missing 'value'") diff --git a/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TaskEvent.kt b/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TaskEvent.kt index 3b35dfa0..12d62280 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TaskEvent.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TaskEvent.kt @@ -59,6 +59,7 @@ sealed interface TaskEvent { data class ResolverLookupFailure( val message: String?, val value: TaskEventResult.Value, + val isCancelled: Boolean, ) : TaskEvent data object Started : TaskEvent @@ -66,9 +67,17 @@ sealed interface TaskEvent { data class StartupFailure( val message: String?, val value: TaskEventResult.Value, + val isCancelled: Boolean, ) : TaskEvent data class TaskTerminated( val index: Int, ) : TaskEvent + + fun isCancelled() = + when (this) { + is StartupFailure -> isCancelled + is ResolverLookupFailure -> isCancelled + else -> null + } } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/App.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/App.kt index 884d4273..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 @@ -97,6 +98,9 @@ fun App( LaunchedEffect(Unit) { dependencies.bootstrapTestDescriptors() dependencies.bootstrapPreferences() + dependencies.configureDescriptorAutoUpdate() + dependencies.fetchDescriptorUpdate(null) + dependencies.startSingleRunInner(RunSpecification.OnlyUploadMissingResults) } LaunchedEffect(Unit) { dependencies.finishInProgressData() @@ -104,10 +108,6 @@ fun App( LaunchedEffect(Unit) { dependencies.observeAndConfigureAutoRun() } - LaunchedEffect(Unit) { - dependencies.configureDescriptorAutoUpdate() - dependencies.fetchDescriptorUpdate(null) - } LaunchedEffect(deepLink) { when (deepLink) { 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..734422a4 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,13 +105,13 @@ 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 } - if (getNetworkType() == NetworkType.VPN) { + if (getNetworkType() == NetworkType.VPN && spec == null) { Logger.i("Skipping auto-run tests: VPN enabled") return } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/config/OrganizationConfig.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/config/OrganizationConfig.kt index f722bf11..4393c783 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/config/OrganizationConfig.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/config/OrganizationConfig.kt @@ -2,6 +2,27 @@ package org.ooni.probe.config import org.jetbrains.compose.resources.DrawableResource +interface OrganizationConfigInterface { + val baseSoftwareName: String + val testDisplayMode: TestDisplayMode + val autorunTaskId: String + val onboardingImages: OnboardingImages + val updateDescriptorTaskId: String + val hasWebsitesDescriptor: Boolean + + val ooniApiBaseUrl get() = BuildTypeDefaults.ooniApiBaseUrl + val ooniRunDomain get() = BuildTypeDefaults.ooniRunDomain + val ooniRunDashboardUrl get() = BuildTypeDefaults.ooniRunDashboardUrl + val explorerUrl get() = BuildTypeDefaults.explorerUrl +} + +interface BuildTypeDefaultsInterface { + val ooniApiBaseUrl: String + val ooniRunDomain: String + val ooniRunDashboardUrl: String + val explorerUrl: String +} + data class OnboardingImages( val image1: DrawableResource, val image2: DrawableResource, diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/InstalledTestDescriptorModel.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/InstalledTestDescriptorModel.kt index 8ff87e75..50053c9f 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/InstalledTestDescriptorModel.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/InstalledTestDescriptorModel.kt @@ -2,19 +2,18 @@ package org.ooni.probe.data.models import co.touchlab.kermit.Logger import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.LocalDateTime.Companion.Format +import kotlinx.datetime.format +import kotlinx.datetime.format.MonthNames +import kotlinx.datetime.format.char import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import ooniprobe.composeapp.generated.resources.Common_Months import ooniprobe.composeapp.generated.resources.Dashboard_Runv2_Overview_Description +import ooniprobe.composeapp.generated.resources.Dashboard_Runv2_Overview_LastUpdated import ooniprobe.composeapp.generated.resources.Res -import org.jetbrains.compose.resources.stringResource -import kotlinx.datetime.LocalDateTime.Companion.Format -import kotlinx.datetime.format -import kotlinx.datetime.format.MonthNames -import kotlinx.datetime.format.char -import ooniprobe.composeapp.generated.resources.Dashboard_Runv2_Overview_LastUpdatd import ooniprobe.composeapp.generated.resources.TestResults_NotAvailable -import ooniprobe.composeapp.generated.resources.months import ooniprobe.composeapp.generated.resources.performance_datausage import ooniprobe.composeapp.generated.resources.small_datausage import ooniprobe.composeapp.generated.resources.test_circumvention @@ -25,6 +24,7 @@ import ooniprobe.composeapp.generated.resources.test_websites import ooniprobe.composeapp.generated.resources.websites_datausage import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.stringArrayResource +import org.jetbrains.compose.resources.stringResource import org.ooni.probe.data.TestDescriptor import org.ooni.probe.shared.InstalledDescriptorIcons import org.ooni.probe.shared.hexToColor @@ -85,7 +85,7 @@ fun InstalledTestDescriptorModel.toDescriptor(updateStatus: UpdateStatus = Updat shortDescription = { shortDescriptionIntl?.getCurrent() ?: shortDescription }, description = { descriptionIntl?.getCurrent() ?: description }, metadata = { - val monthNames = stringArrayResource(Res.array.months) + val monthNames = stringArrayResource(Res.array.Common_Months) val formattedDate = { date: LocalDateTime? -> date?.format(dateTimeFormat(monthNames)) } formattedDate(dateCreated)?.let { formattedDateCreated -> stringResource( @@ -93,7 +93,7 @@ fun InstalledTestDescriptorModel.toDescriptor(updateStatus: UpdateStatus = Updat author.orEmpty(), formattedDateCreated, ) + ". " + formattedDate(dateUpdated)?.let { - stringResource(Res.string.Dashboard_Runv2_Overview_LastUpdatd, it) + stringResource(Res.string.Dashboard_Runv2_Overview_LastUpdated, it) } } }, diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/PreferenceItem.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/PreferenceItem.kt new file mode 100644 index 00000000..a0b93168 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/PreferenceItem.kt @@ -0,0 +1,76 @@ +package org.ooni.probe.data.models + +import androidx.compose.runtime.Composable +import org.jetbrains.compose.resources.DrawableResource +import org.jetbrains.compose.resources.StringResource + +open class PreferenceItem( + open val title: StringResource, + open val icon: DrawableResource? = null, + open val type: PreferenceItemType, + open val key: SettingsKey, + open val supportingContent: @Composable (() -> Unit)? = null, + open val trailingContent: @Composable (() -> Unit)? = null, + open val enabled: Boolean = true, + open val indentation: Int = 0, +) + +data class SettingsItem( + override val icon: DrawableResource? = null, + override val title: StringResource, + override val type: PreferenceItemType, + override val key: SettingsKey, + override val supportingContent: @Composable (() -> Unit)? = null, + override val trailingContent: @Composable (() -> Unit)? = null, + override val enabled: Boolean = true, + override val indentation: Int = 0, +) : PreferenceItem( + title = title, + icon = icon, + supportingContent = supportingContent, + type = type, + key = key, + enabled = enabled, + ) + +data class SettingsCategoryItem( + override val icon: DrawableResource? = null, + override val title: StringResource, + val route: PreferenceCategoryKey, + val settings: List? = emptyList(), + override val supportingContent: @Composable (() -> Unit)? = null, + val footerContent: @Composable (() -> Unit)? = null, + override val indentation: Int = 0, +) : PreferenceItem( + title = title, + icon = icon, + supportingContent = supportingContent, + type = PreferenceItemType.ROUTE, + key = SettingsKey.ROUTE, + ) + +enum class PreferenceItemType { + SWITCH, + INT, + BUTTON, + SELECT, + ROUTE, +} + +enum class PreferenceCategoryKey(val value: String) { + NOTIFICATIONS("notifications"), + TEST_OPTIONS("test_options"), + PRIVACY("privacy"), + PROXY("proxy"), + ADVANCED("advanced"), + SEND_EMAIL("send_email"), + ABOUT_OONI("about_ooni"), + + WEBSITES_CATEGORIES("websites_categories"), + SEE_RECENT_LOGS("see_recent_logs"), + ; + + companion object { + fun fromValue(value: String?) = value?.let { entries.firstOrNull { it.value == value } } + } +} 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/data/models/PreferenceModel.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/SettingsKey.kt similarity index 50% rename from composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/PreferenceModel.kt rename to composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/SettingsKey.kt index 7a4e3d95..b87c3884 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/PreferenceModel.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/SettingsKey.kt @@ -1,77 +1,5 @@ package org.ooni.probe.data.models -import androidx.compose.runtime.Composable -import org.jetbrains.compose.resources.DrawableResource -import org.jetbrains.compose.resources.StringResource - -open class PreferenceItem( - open val title: StringResource, - open val icon: DrawableResource? = null, - open val type: PreferenceItemType, - open val key: SettingsKey, - open val supportingContent: @Composable (() -> Unit)? = null, - open val trailingContent: @Composable (() -> Unit)? = null, - open val enabled: Boolean = true, -) - -data class SettingsItem( - override val icon: DrawableResource? = null, - override val title: StringResource, - override val type: PreferenceItemType, - override val key: SettingsKey, - override val supportingContent: @Composable (() -> Unit)? = null, - override val trailingContent: @Composable (() -> Unit)? = null, - override val enabled: Boolean = true, -) : PreferenceItem( - title = title, - icon = icon, - supportingContent = supportingContent, - type = type, - key = key, - enabled = enabled, - ) - -data class SettingsCategoryItem( - override val icon: DrawableResource? = null, - override val title: StringResource, - val route: PreferenceCategoryKey, - val settings: List? = emptyList(), - override val supportingContent: @Composable (() -> Unit)? = null, - val footerContent: @Composable (() -> Unit)? = null, -) : PreferenceItem( - title = title, - icon = icon, - supportingContent = supportingContent, - type = PreferenceItemType.ROUTE, - key = SettingsKey.ROUTE, - ) - -enum class PreferenceItemType { - SWITCH, - INT, - BUTTON, - SELECT, - ROUTE, -} - -enum class PreferenceCategoryKey(val value: String) { - NOTIFICATIONS("notifications"), - TEST_OPTIONS("test_options"), - PRIVACY("privacy"), - PROXY("proxy"), - ADVANCED("advanced"), - SEND_EMAIL("send_email"), - ABOUT_OONI("about_ooni"), - - WEBSITES_CATEGORIES("websites_categories"), - SEE_RECENT_LOGS("see_recent_logs"), - ; - - companion object { - fun fromValue(value: String) = entries.first { it.value == value } - } -} - enum class SettingsKey(val value: String) { // Notifications NOTIFICATIONS_ENABLED("notifications_enabled"), 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 f617cec9..b247ae7b 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt @@ -105,7 +105,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/BootstrapPreferences.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/BootstrapPreferences.kt index 91dfbdf7..d54523d9 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/BootstrapPreferences.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/BootstrapPreferences.kt @@ -33,7 +33,7 @@ class BootstrapPreferences( SettingsKey.AUTOMATED_TESTING_CHARGING to true, SettingsKey.AUTOMATED_TESTING_NOT_UPLOADED_LIMIT to NOT_UPLOADED_LIMIT_DEFAULT, ) + - preferenceDefaults(), + organizationPreferenceDefaults(), ) } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/FetchDescriptor.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/FetchDescriptor.kt index 48830c06..41cc162e 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/FetchDescriptor.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/FetchDescriptor.kt @@ -1,6 +1,7 @@ package org.ooni.probe.domain import co.touchlab.kermit.Logger +import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json import org.ooni.engine.Engine.MkException import org.ooni.engine.models.OONIRunDescriptor @@ -26,8 +27,11 @@ class FetchDescriptor( json.decodeFromString(it).toModel().copy( revisions = fetchRevisions(descriptorId).get()?.revisions, ) - } catch (e: Throwable) { - Logger.e(e) { "Failed to decode descriptor" } + } catch (e: SerializationException) { + Logger.e(e) { "Failed to decode descriptor $descriptorId" } + null + } catch (e: IllegalArgumentException) { + Logger.e(e) { "Failed to decode descriptor $descriptorId" } null } } ?: throw MkException(Throwable("Failed to fetch descriptor")) @@ -43,8 +47,11 @@ class FetchDescriptor( result?.let { try { json.decodeFromString(it) - } catch (e: Throwable) { - Logger.e(e) { "Failed to decode revisions" } + } catch (e: SerializationException) { + Logger.e(e) { "Failed to decode descriptor revisions $descriptorId" } + null + } catch (e: IllegalArgumentException) { + Logger.e(e) { "Failed to decode descriptor revisions $descriptorId" } null } } ?: throw MkException(Throwable("Failed to fetch revision")) 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/GetSettings.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetSettings.kt index 3ef7f165..b03b1f9b 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetSettings.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetSettings.kt @@ -26,8 +26,10 @@ import ooniprobe.composeapp.generated.resources.Settings_About_Label import ooniprobe.composeapp.generated.resources.Settings_Advanced_DebugLogs import ooniprobe.composeapp.generated.resources.Settings_Advanced_Label import ooniprobe.composeapp.generated.resources.Settings_Advanced_RecentLogs +import ooniprobe.composeapp.generated.resources.Settings_AutoTest_NotUploadedLimit import ooniprobe.composeapp.generated.resources.Settings_AutomatedTesting_RunAutomatically import ooniprobe.composeapp.generated.resources.Settings_AutomatedTesting_RunAutomatically_ChargingOnly +import ooniprobe.composeapp.generated.resources.Settings_AutomatedTesting_RunAutomatically_Description import ooniprobe.composeapp.generated.resources.Settings_AutomatedTesting_RunAutomatically_Footer import ooniprobe.composeapp.generated.resources.Settings_AutomatedTesting_RunAutomatically_WiFiOnly import ooniprobe.composeapp.generated.resources.Settings_Notifications_Enabled @@ -37,12 +39,17 @@ import ooniprobe.composeapp.generated.resources.Settings_Privacy_SendCrashReport import ooniprobe.composeapp.generated.resources.Settings_Proxy_Label import ooniprobe.composeapp.generated.resources.Settings_SendEmail_Label import ooniprobe.composeapp.generated.resources.Settings_Sharing_UploadResults +import ooniprobe.composeapp.generated.resources.Settings_Sharing_UploadResults_Description import ooniprobe.composeapp.generated.resources.Settings_Storage_Clear import ooniprobe.composeapp.generated.resources.Settings_Storage_Label import ooniprobe.composeapp.generated.resources.Settings_TestOptions_Label import ooniprobe.composeapp.generated.resources.Settings_WarmVPNInUse_Label +import ooniprobe.composeapp.generated.resources.Settings_Websites_Categories_Description +import ooniprobe.composeapp.generated.resources.Settings_Websites_Categories_Label +import ooniprobe.composeapp.generated.resources.Settings_Websites_MaxRuntimeEnabled_Description +import ooniprobe.composeapp.generated.resources.Settings_Websites_MaxRuntimeEnabled_New +import ooniprobe.composeapp.generated.resources.Settings_Websites_MaxRuntime_New import ooniprobe.composeapp.generated.resources.advanced -import ooniprobe.composeapp.generated.resources.auto_test_not_uploaded_limit import ooniprobe.composeapp.generated.resources.ic_settings import ooniprobe.composeapp.generated.resources.notifications import ooniprobe.composeapp.generated.resources.outline_info @@ -51,6 +58,7 @@ import ooniprobe.composeapp.generated.resources.proxy import ooniprobe.composeapp.generated.resources.send_email import org.jetbrains.compose.resources.stringResource import org.ooni.engine.models.WebConnectivityCategory +import org.ooni.probe.config.OrganizationConfig import org.ooni.probe.data.models.PreferenceCategoryKey import org.ooni.probe.data.models.PreferenceItemType import org.ooni.probe.data.models.SettingsCategoryItem @@ -59,6 +67,8 @@ import org.ooni.probe.data.models.SettingsKey import org.ooni.probe.data.repositories.PreferenceRepository import org.ooni.probe.ui.settings.category.SettingsDescription import org.ooni.probe.ui.shared.formatDataUsage +import org.ooni.probe.ui.shared.shortFormat +import kotlin.time.Duration.Companion.seconds class GetSettings( private val preferencesRepository: PreferenceRepository, @@ -82,6 +92,7 @@ class GetSettings( val enabledCategoriesCount = WebConnectivityCategory.entries.count { preferences[it.settingsKey] == true } buildSettings( + hasWebsitesDescriptor = OrganizationConfig.hasWebsitesDescriptor, uploadResultsEnabled = preferences[SettingsKey.UPLOAD_RESULTS] == true, autoRunEnabled = preferences[SettingsKey.AUTOMATED_TESTING_ENABLED] == true, autoRunNotUploadedLimit = preferences[SettingsKey.AUTOMATED_TESTING_NOT_UPLOADED_LIMIT] as? Int, @@ -95,6 +106,7 @@ class GetSettings( } private fun buildSettings( + hasWebsitesDescriptor: Boolean, uploadResultsEnabled: Boolean, autoRunEnabled: Boolean, autoRunNotUploadedLimit: Int?, @@ -131,43 +143,117 @@ class GetSettings( title = Res.string.Settings_Sharing_UploadResults, key = SettingsKey.UPLOAD_RESULTS, type = PreferenceItemType.SWITCH, + supportingContent = { + Text( + stringResource(Res.string.Settings_Sharing_UploadResults_Description), + style = MaterialTheme.typography.labelLarge, + ) + }, ), SettingsItem( title = Res.string.Settings_AutomatedTesting_RunAutomatically, key = SettingsKey.AUTOMATED_TESTING_ENABLED, type = PreferenceItemType.SWITCH, enabled = uploadResultsEnabled, - ), - SettingsItem( - title = Res.string.Settings_AutomatedTesting_RunAutomatically_WiFiOnly, - key = SettingsKey.AUTOMATED_TESTING_WIFIONLY, - type = PreferenceItemType.SWITCH, - enabled = autoRunEnabled && uploadResultsEnabled, - ), - SettingsItem( - title = Res.string.Settings_AutomatedTesting_RunAutomatically_ChargingOnly, - key = SettingsKey.AUTOMATED_TESTING_CHARGING, - type = PreferenceItemType.SWITCH, - enabled = autoRunEnabled && uploadResultsEnabled, - ), - SettingsItem( - title = Res.string.auto_test_not_uploaded_limit, - key = SettingsKey.AUTOMATED_TESTING_NOT_UPLOADED_LIMIT, - type = PreferenceItemType.INT, - enabled = autoRunEnabled && uploadResultsEnabled, supportingContent = { - val value = ( - autoRunNotUploadedLimit - ?: BootstrapPreferences.NOT_UPLOADED_LIMIT_DEFAULT - ).coerceAtLeast(1) - Text(value.toString()) + Text( + stringResource(Res.string.Settings_AutomatedTesting_RunAutomatically_Description), + style = MaterialTheme.typography.labelLarge, + ) }, ), - ) + webConnectivityPreferences( - enabledCategoriesCount, - maxRuntimeEnabled, - maxRuntime, - ), + ) + if (autoRunEnabled) { + listOf( + SettingsItem( + title = Res.string.Settings_AutomatedTesting_RunAutomatically_WiFiOnly, + key = SettingsKey.AUTOMATED_TESTING_WIFIONLY, + type = PreferenceItemType.SWITCH, + enabled = autoRunEnabled && uploadResultsEnabled, + indentation = 1, + ), + SettingsItem( + title = Res.string.Settings_AutomatedTesting_RunAutomatically_ChargingOnly, + key = SettingsKey.AUTOMATED_TESTING_CHARGING, + type = PreferenceItemType.SWITCH, + enabled = autoRunEnabled && uploadResultsEnabled, + indentation = 1, + ), + SettingsItem( + title = Res.string.Settings_AutoTest_NotUploadedLimit, + key = SettingsKey.AUTOMATED_TESTING_NOT_UPLOADED_LIMIT, + type = PreferenceItemType.INT, + enabled = autoRunEnabled && uploadResultsEnabled, + supportingContent = { + val value = ( + autoRunNotUploadedLimit + ?: BootstrapPreferences.NOT_UPLOADED_LIMIT_DEFAULT + ).coerceAtLeast(1) + Text(value.toString()) + }, + indentation = 1, + ), + ) + } else { + emptyList() + } + if (hasWebsitesDescriptor) { + listOfNotNull( + SettingsItem( + title = Res.string.Settings_Websites_MaxRuntimeEnabled_New, + key = SettingsKey.MAX_RUNTIME_ENABLED, + type = PreferenceItemType.SWITCH, + supportingContent = { + Text( + stringResource(Res.string.Settings_Websites_MaxRuntimeEnabled_Description), + style = MaterialTheme.typography.labelLarge, + ) + }, + indentation = 0, + ), + if (maxRuntimeEnabled) { + SettingsItem( + title = Res.string.Settings_Websites_MaxRuntime_New, + key = SettingsKey.MAX_RUNTIME, + type = PreferenceItemType.INT, + supportingContent = { + maxRuntime?.let { + Text(it.coerceAtLeast(0).seconds.shortFormat()) + } + }, + indentation = 1, + ) + } else { + null + }, + ) + } else { + emptyList() + } + if (hasWebsitesDescriptor) { + listOf( + SettingsCategoryItem( + title = Res.string.Settings_Websites_Categories_Label, + route = PreferenceCategoryKey.WEBSITES_CATEGORIES, + supportingContent = { + Text( + stringResource( + Res.string.Settings_Websites_Categories_Description, + enabledCategoriesCount, + ), + ) + }, + settings = WebConnectivityCategory.entries.mapNotNull { cat -> + SettingsItem( + icon = cat.icon, + title = cat.title, + supportingContent = { Text(stringResource(cat.description)) }, + key = cat.settingsKey ?: return@mapNotNull null, + type = PreferenceItemType.SWITCH, + ) + }, + ), + ) + } else { + emptyList() + }, footerContent = { SettingsDescription( Res.string.Settings_AutomatedTesting_RunAutomatically_Footer, 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/domain/RunNetTest.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/RunNetTest.kt index f6f27216..562b9594 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/RunNetTest.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/RunNetTest.kt @@ -241,17 +241,24 @@ class RunNetTest( } } - val value = when (event) { - is TaskEvent.StartupFailure -> event.value - is TaskEvent.ResolverLookupFailure -> event.value - else -> null - } ?: return + if (event.isCancelled() == true) return + + when (event) { + is TaskEvent.StartupFailure -> + Logger.w("StartupFailure", StartupFailure(message, event.value)) - Logger.w(message ?: "Failure", Failure(value)) + is TaskEvent.ResolverLookupFailure -> + Logger.i( + "ResolverLookupFailure", + ResolverLookupFailure(message, event.value), + ) + + else -> Unit + } } is TaskEvent.BugJsonDump -> { - Logger.w("BugJsonDump", Failure(event.value)) + Logger.w("BugJsonDump", BugJsonDump(event.value)) } is TaskEvent.TaskTerminated -> Unit @@ -291,5 +298,21 @@ class RunNetTest( ) } - inner class Failure(value: TaskEventResult.Value) : Exception(json.encodeToString(value)) + open inner class Failure(message: String?, value: TaskEventResult.Value?) : Exception( + if (message != null && value != null) { + message + "\n" + json.encodeToString(value) + } else if (value != null) { + json.encodeToString(value) + } else { + message ?: "" + }, + ) + + inner class StartupFailure(message: String?, value: TaskEventResult.Value?) : + Failure(message, value) + + inner class ResolverLookupFailure(message: String?, value: TaskEventResult.Value?) : + Failure(message, value) + + inner class BugJsonDump(value: TaskEventResult.Value?) : Failure(null, value) } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/SendSupportEmail.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/SendSupportEmail.kt index 4289f140..29b4cd34 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/SendSupportEmail.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/SendSupportEmail.kt @@ -14,7 +14,6 @@ class SendSupportEmail( private val launchAction: (PlatformAction) -> Boolean, ) { 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) diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/ShareLogFile.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/ShareLogFile.kt index 6835155b..b07833cb 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/ShareLogFile.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/ShareLogFile.kt @@ -2,7 +2,7 @@ package org.ooni.probe.domain import okio.Path import ooniprobe.composeapp.generated.resources.Res -import ooniprobe.composeapp.generated.resources.logs +import ooniprobe.composeapp.generated.resources.Settings_Logs import org.jetbrains.compose.resources.getString import org.ooni.probe.data.models.PlatformAction @@ -13,7 +13,7 @@ class ShareLogFile( suspend operator fun invoke(): Boolean = shareFile( PlatformAction.FileSharing( - title = getString(Res.string.logs), + title = getString(Res.string.Settings_Logs), filePath = getAppLoggerFile(), ), ) diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/shared/ResourceExt.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/shared/ResourceExt.kt new file mode 100644 index 00000000..6579c836 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/shared/ResourceExt.kt @@ -0,0 +1,79 @@ +package org.ooni.probe.shared + +import androidx.compose.runtime.Composable +import ooniprobe.composeapp.generated.resources.Common_Hour_One +import ooniprobe.composeapp.generated.resources.Common_Hour_Other +import ooniprobe.composeapp.generated.resources.Common_Minutes_One +import ooniprobe.composeapp.generated.resources.Common_Minutes_Other +import ooniprobe.composeapp.generated.resources.Common_Months_April +import ooniprobe.composeapp.generated.resources.Common_Months_August +import ooniprobe.composeapp.generated.resources.Common_Months_December +import ooniprobe.composeapp.generated.resources.Common_Months_February +import ooniprobe.composeapp.generated.resources.Common_Months_January +import ooniprobe.composeapp.generated.resources.Common_Months_July +import ooniprobe.composeapp.generated.resources.Common_Months_June +import ooniprobe.composeapp.generated.resources.Common_Months_March +import ooniprobe.composeapp.generated.resources.Common_Months_May +import ooniprobe.composeapp.generated.resources.Common_Months_November +import ooniprobe.composeapp.generated.resources.Common_Months_October +import ooniprobe.composeapp.generated.resources.Common_Months_September +import ooniprobe.composeapp.generated.resources.Dashboard_RunTests_RunButton_Label_One +import ooniprobe.composeapp.generated.resources.Dashboard_RunTests_RunButton_Label_Other +import ooniprobe.composeapp.generated.resources.Measurements_Count_One +import ooniprobe.composeapp.generated.resources.Measurements_Count_Other +import ooniprobe.composeapp.generated.resources.Res +import org.jetbrains.compose.resources.PluralStringResource +import org.jetbrains.compose.resources.getPluralString +import org.jetbrains.compose.resources.getString +import org.jetbrains.compose.resources.pluralStringResource +import org.jetbrains.compose.resources.stringResource + +val stringMap = mapOf( + "@string/Common_Minutes_One" to Res.string.Common_Minutes_One, + "@string/Common_Minutes_Other" to Res.string.Common_Minutes_Other, + "@string/Common_Hour_One" to Res.string.Common_Hour_One, + "@string/Common_Hour_Other" to Res.string.Common_Hour_Other, + "@string/Dashboard_RunTests_RunButton_Label_One" to Res.string.Dashboard_RunTests_RunButton_Label_One, + "@string/Dashboard_RunTests_RunButton_Label_Other" to Res.string.Dashboard_RunTests_RunButton_Label_Other, + "@string/Measurements_Count_One" to Res.string.Measurements_Count_One, + "@string/Measurements_Count_Other" to Res.string.Measurements_Count_Other, +) + +@Composable +fun stringMonthArrayResource(): List { + return listOf( + stringResource(Res.string.Common_Months_January), + stringResource(Res.string.Common_Months_February), + stringResource(Res.string.Common_Months_March), + stringResource(Res.string.Common_Months_April), + stringResource(Res.string.Common_Months_May), + stringResource(Res.string.Common_Months_June), + stringResource(Res.string.Common_Months_July), + stringResource(Res.string.Common_Months_August), + stringResource(Res.string.Common_Months_September), + stringResource(Res.string.Common_Months_October), + stringResource(Res.string.Common_Months_November), + stringResource(Res.string.Common_Months_December), + ) +} + +@Composable +fun pluralStringResourceItem( + resource: PluralStringResource, + quantity: Int, + vararg formatArgs: Any, +): String { + return stringMap[pluralStringResource(resource, quantity, formatArgs)]?.let { + return stringResource(it, *formatArgs) + } ?: "" +} + +suspend fun getPluralStringResourceItem( + resource: PluralStringResource, + quantity: Int, + vararg formatArgs: Any, +): String { + return stringMap[getPluralString(resource, quantity, formatArgs)]?.let { + return getString(it, *formatArgs) + } ?: "" +} diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/choosewebsites/ChooseWebsitesScreen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/choosewebsites/ChooseWebsitesScreen.kt index cad7e5a5..515aa9ea 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/choosewebsites/ChooseWebsitesScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/choosewebsites/ChooseWebsitesScreen.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp +import ooniprobe.composeapp.generated.resources.Common_Back import ooniprobe.composeapp.generated.resources.CustomWebsites_Fab_Text import ooniprobe.composeapp.generated.resources.Modal_Cancel import ooniprobe.composeapp.generated.resources.Modal_CustomURL_NotSaved @@ -39,7 +40,6 @@ import ooniprobe.composeapp.generated.resources.Res import ooniprobe.composeapp.generated.resources.Settings_Websites_CustomURL_Add import ooniprobe.composeapp.generated.resources.Settings_Websites_CustomURL_Title import ooniprobe.composeapp.generated.resources.Settings_Websites_CustomURL_URL -import ooniprobe.composeapp.generated.resources.back import ooniprobe.composeapp.generated.resources.ic_add import ooniprobe.composeapp.generated.resources.ic_cancel import ooniprobe.composeapp.generated.resources.ic_timer @@ -63,7 +63,7 @@ fun ChooseWebsitesScreen( IconButton(onClick = { onEvent(ChooseWebsitesViewModel.Event.BackClicked) }) { Icon( Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(Res.string.back), + contentDescription = stringResource(Res.string.Common_Back), ) } }, 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..80329586 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.Results_UploadingMissing 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.Results_UploadingMissing, "$progress/${uploadState.total}", ), style = MaterialTheme.typography.bodyLarge, diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/descriptor/DescriptorScreen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/descriptor/DescriptorScreen.kt index 0b4790ee..4308c19e 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/descriptor/DescriptorScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/descriptor/DescriptorScreen.kt @@ -39,13 +39,13 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import ooniprobe.composeapp.generated.resources.AddDescriptor_AutoRun import ooniprobe.composeapp.generated.resources.AddDescriptor_Settings +import ooniprobe.composeapp.generated.resources.Common_Back import ooniprobe.composeapp.generated.resources.Dashboard_Overview_ChooseWebsites import ooniprobe.composeapp.generated.resources.Dashboard_Overview_Estimated import ooniprobe.composeapp.generated.resources.Dashboard_Overview_LastRun_Never import ooniprobe.composeapp.generated.resources.Dashboard_Overview_LatestTest import ooniprobe.composeapp.generated.resources.Dashboard_Runv2_Overview_ReviewUpdates import ooniprobe.composeapp.generated.resources.Res -import ooniprobe.composeapp.generated.resources.back import ooniprobe.composeapp.generated.resources.ooni_empty_state import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource @@ -91,7 +91,7 @@ fun DescriptorScreen( IconButton(onClick = { onEvent(DescriptorViewModel.Event.BackClicked) }) { Icon( Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(Res.string.back), + contentDescription = stringResource(Res.string.Common_Back), ) } }, diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/log/LogScreen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/log/LogScreen.kt index 8599de96..ee6588eb 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/log/LogScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/log/LogScreen.kt @@ -34,14 +34,14 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import co.touchlab.kermit.Severity +import ooniprobe.composeapp.generated.resources.Common_Back import ooniprobe.composeapp.generated.resources.Res +import ooniprobe.composeapp.generated.resources.Settings_FilterLogs +import ooniprobe.composeapp.generated.resources.Settings_Logs +import ooniprobe.composeapp.generated.resources.Settings_ShareLogs +import ooniprobe.composeapp.generated.resources.Settings_ShareLogs_Error import ooniprobe.composeapp.generated.resources.Settings_Storage_Delete -import ooniprobe.composeapp.generated.resources.back -import ooniprobe.composeapp.generated.resources.filter_logs import ooniprobe.composeapp.generated.resources.ic_delete_all -import ooniprobe.composeapp.generated.resources.logs -import ooniprobe.composeapp.generated.resources.share_logs -import ooniprobe.composeapp.generated.resources.share_logs_error import org.jetbrains.compose.resources.getString import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource @@ -57,12 +57,12 @@ fun LogScreen( ) { Column(Modifier.background(MaterialTheme.colorScheme.background)) { TopBar( - title = { Text(stringResource(Res.string.logs)) }, + title = { Text(stringResource(Res.string.Settings_Logs)) }, navigationIcon = { IconButton(onClick = { onEvent(LogViewModel.Event.BackClicked) }) { Icon( Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(Res.string.back), + contentDescription = stringResource(Res.string.Common_Back), ) } }, @@ -76,7 +76,7 @@ fun LogScreen( IconButton(onClick = { onEvent(LogViewModel.Event.ShareClicked) }) { Icon( Icons.Default.Share, - contentDescription = stringResource(Res.string.share_logs), + contentDescription = stringResource(Res.string.Settings_ShareLogs), ) } }, @@ -90,7 +90,7 @@ fun LogScreen( .padding(bottom = 8.dp), ) { Text( - stringResource(Res.string.filter_logs), + stringResource(Res.string.Settings_FilterLogs), modifier = Modifier.weight(2f), ) SeverityFilter( @@ -129,7 +129,7 @@ fun LogScreen( snackbarHostState.showSnackbar( getString( when (error) { - LogViewModel.Error.Share -> Res.string.share_logs_error + LogViewModel.Error.Share -> Res.string.Settings_ShareLogs_Error }, ), ) diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/measurement/MeasurementScreen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/measurement/MeasurementScreen.kt index ac4e6bed..2e5e8310 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/measurement/MeasurementScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/measurement/MeasurementScreen.kt @@ -28,10 +28,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import ooniprobe.composeapp.generated.resources.Common_Back +import ooniprobe.composeapp.generated.resources.Common_Refresh +import ooniprobe.composeapp.generated.resources.Measurement_Title import ooniprobe.composeapp.generated.resources.Res -import ooniprobe.composeapp.generated.resources.back -import ooniprobe.composeapp.generated.resources.measurement -import ooniprobe.composeapp.generated.resources.refresh import org.intellij.markdown.html.urlEncode import org.jetbrains.compose.resources.stringResource import org.ooni.probe.config.OrganizationConfig @@ -56,13 +56,13 @@ fun MeasurementScreen( Box { TopBar( title = { - Text(stringResource(Res.string.measurement)) + Text(stringResource(Res.string.Measurement_Title)) }, navigationIcon = { IconButton(onClick = { onBack() }) { Icon( Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(Res.string.back), + contentDescription = stringResource(Res.string.Common_Back), ) } }, @@ -93,7 +93,7 @@ fun MeasurementScreen( ) { Icon( Icons.Default.Refresh, - contentDescription = stringResource(Res.string.refresh), + contentDescription = stringResource(Res.string.Common_Refresh), ) } } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Navigation.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Navigation.kt index a3064879..f39b19c6 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Navigation.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Navigation.kt @@ -161,9 +161,11 @@ fun Navigation( route = Screen.SettingsCategory.NAV_ROUTE, arguments = Screen.SettingsCategory.ARGUMENTS, ) { entry -> - val category = entry.arguments?.getString("category") ?: return@composable + val category = PreferenceCategoryKey.fromValue( + entry.arguments?.getString("category"), + ) ?: return@composable when (category) { - PreferenceCategoryKey.ABOUT_OONI.value -> { + PreferenceCategoryKey.ABOUT_OONI -> { val viewModel = viewModel { dependencies.aboutViewModel(onBack = { navController.goBack() }) } @@ -174,7 +176,7 @@ fun Navigation( ) } - PreferenceCategoryKey.PROXY.value -> { + PreferenceCategoryKey.PROXY -> { val viewModel = viewModel { dependencies.proxyViewModel(onBack = { navController.goBack() }) } @@ -182,7 +184,7 @@ fun Navigation( ProxyScreen(state, viewModel::onEvent) } - PreferenceCategoryKey.SEE_RECENT_LOGS.value -> { + PreferenceCategoryKey.SEE_RECENT_LOGS -> { val viewModel = viewModel { dependencies.logViewModel(onBack = { navController.goBack() }) } @@ -193,7 +195,7 @@ fun Navigation( else -> { val viewModel = viewModel { dependencies.settingsCategoryViewModel( - categoryKey = category, + categoryKey = category.value, goToSettingsForCategory = { navController.safeNavigate(Screen.SettingsCategory(it)) }, diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/onboarding/OnboardingQuiz.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/onboarding/OnboardingQuiz.kt index 76247808..e822a1c7 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/onboarding/OnboardingQuiz.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/onboarding/OnboardingQuiz.kt @@ -39,9 +39,9 @@ import ooniprobe.composeapp.generated.resources.Onboarding_PopQuiz_Title import ooniprobe.composeapp.generated.resources.Onboarding_PopQuiz_True import ooniprobe.composeapp.generated.resources.Onboarding_PopQuiz_Wrong_Button_Back import ooniprobe.composeapp.generated.resources.Onboarding_PopQuiz_Wrong_Button_Continue +import ooniprobe.composeapp.generated.resources.Onboarding_QuizAnswer_Correct +import ooniprobe.composeapp.generated.resources.Onboarding_QuizAnswer_Incorrect import ooniprobe.composeapp.generated.resources.Res -import ooniprobe.composeapp.generated.resources.quiz_answer_correct -import ooniprobe.composeapp.generated.resources.quiz_answer_incorrect import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.stringResource import org.ooni.probe.data.models.Animation @@ -200,9 +200,9 @@ private fun ResultAnimation( animation = if (isCorrect) Animation.QuizCorrect else Animation.QuizIncorrect, contentDescription = stringResource( if (isCorrect) { - Res.string.quiz_answer_correct + Res.string.Onboarding_QuizAnswer_Correct } else { - Res.string.quiz_answer_incorrect + Res.string.Onboarding_QuizAnswer_Incorrect }, ), restartOnPlay = false, diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/onboarding/OnboardingScreen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/onboarding/OnboardingScreen.kt index 94d5e8ba..139d882b 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/onboarding/OnboardingScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/onboarding/OnboardingScreen.kt @@ -372,7 +372,7 @@ fun ColumnScope.RequestPermissionStep(onEvent: (OnboardingViewModel.Event) -> Un Logger.i("Permission request cancelled") // Nothing to do here } catch (e: Exception) { - Logger.e("Error requesting permission") + Logger.e("Error requesting permission", e) } } }, diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultMeasurementCell.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultMeasurementCell.kt index 70a09210..829d52ab 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultMeasurementCell.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultMeasurementCell.kt @@ -17,6 +17,9 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import ooniprobe.composeapp.generated.resources.Measurements_Anomaly +import ooniprobe.composeapp.generated.resources.Measurements_Failed +import ooniprobe.composeapp.generated.resources.Measurements_Ok import ooniprobe.composeapp.generated.resources.Modal_UploadFailed_Title import ooniprobe.composeapp.generated.resources.Res import ooniprobe.composeapp.generated.resources.Snackbar_ResultsNotUploaded_Text @@ -24,9 +27,6 @@ import ooniprobe.composeapp.generated.resources.ic_cloud_off import ooniprobe.composeapp.generated.resources.ic_measurement_anomaly import ooniprobe.composeapp.generated.resources.ic_measurement_failed import ooniprobe.composeapp.generated.resources.ic_measurement_ok -import ooniprobe.composeapp.generated.resources.measurement_anomaly -import ooniprobe.composeapp.generated.resources.measurement_failed -import ooniprobe.composeapp.generated.resources.measurement_ok import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import org.ooni.engine.models.TestType @@ -111,9 +111,9 @@ fun ResultMeasurementCell( ), contentDescription = stringResource( when { - isFailed -> Res.string.measurement_failed - measurement.isAnomaly -> Res.string.measurement_anomaly - else -> Res.string.measurement_ok + isFailed -> Res.string.Measurements_Failed + measurement.isAnomaly -> Res.string.Measurements_Anomaly + else -> Res.string.Measurements_Ok }, ), tint = Color.Unspecified, diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultScreen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultScreen.kt index 3127dc6b..3d2429a4 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultScreen.kt @@ -41,10 +41,12 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import ooniprobe.composeapp.generated.resources.Common_Back import ooniprobe.composeapp.generated.resources.Modal_Cancel import ooniprobe.composeapp.generated.resources.Modal_ReRun_Title import ooniprobe.composeapp.generated.resources.Modal_ReRun_Websites_Run import ooniprobe.composeapp.generated.resources.Modal_ReRun_Websites_Title +import ooniprobe.composeapp.generated.resources.NetworkType_Vpn import ooniprobe.composeapp.generated.resources.Res import ooniprobe.composeapp.generated.resources.TestResults_NotAvailable import ooniprobe.composeapp.generated.resources.TestResults_Overview_Hero_DataUsage @@ -58,12 +60,10 @@ import ooniprobe.composeapp.generated.resources.TestResults_Summary_Hero_Runtime import ooniprobe.composeapp.generated.resources.TestResults_Summary_Hero_WiFi import ooniprobe.composeapp.generated.resources.TestResults_Summary_Performance_Hero_Download import ooniprobe.composeapp.generated.resources.TestResults_Summary_Performance_Hero_Upload -import ooniprobe.composeapp.generated.resources.back import ooniprobe.composeapp.generated.resources.ic_download import ooniprobe.composeapp.generated.resources.ic_replay import ooniprobe.composeapp.generated.resources.ic_upload import ooniprobe.composeapp.generated.resources.ooni_bw -import ooniprobe.composeapp.generated.resources.vpn import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import org.ooni.engine.models.NetworkType @@ -94,7 +94,7 @@ fun ResultScreen( IconButton(onClick = { onEvent(ResultViewModel.Event.BackClicked) }) { Icon( Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(Res.string.back), + contentDescription = stringResource(Res.string.Common_Back), ) } }, @@ -316,7 +316,7 @@ private fun NetworkType.label(): String = when (this) { NetworkType.Mobile -> Res.string.TestResults_Summary_Hero_Mobile NetworkType.NoInternet -> Res.string.TestResults_Summary_Hero_NoInternet - NetworkType.VPN -> Res.string.vpn + NetworkType.VPN -> Res.string.NetworkType_Vpn NetworkType.Wifi -> Res.string.TestResults_Summary_Hero_WiFi is NetworkType.Unknown -> Res.string.TestResults_NotAvailable }, 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/results/ResultCell.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/results/ResultCell.kt index 3ae7627a..0ed90f10 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/results/ResultCell.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/results/ResultCell.kt @@ -18,19 +18,19 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import ooniprobe.composeapp.generated.resources.Measurements_Count import ooniprobe.composeapp.generated.resources.Modal_UploadFailed_Title import ooniprobe.composeapp.generated.resources.Res import ooniprobe.composeapp.generated.resources.Snackbar_ResultsNotUploaded_Text +import ooniprobe.composeapp.generated.resources.TaskOrigin_AutoRun +import ooniprobe.composeapp.generated.resources.TaskOrigin_Manual import ooniprobe.composeapp.generated.resources.TestResults_UnknownASN import ooniprobe.composeapp.generated.resources.ic_cloud_off -import ooniprobe.composeapp.generated.resources.measurements_count -import ooniprobe.composeapp.generated.resources.task_origin_auto_run -import ooniprobe.composeapp.generated.resources.task_origin_manual import org.jetbrains.compose.resources.painterResource -import org.jetbrains.compose.resources.pluralStringResource import org.jetbrains.compose.resources.stringResource import org.ooni.engine.models.TaskOrigin import org.ooni.probe.data.models.ResultListItem +import org.ooni.probe.shared.pluralStringResourceItem import org.ooni.probe.ui.dashboard.TestDescriptorLabel import org.ooni.probe.ui.shared.relativeDateTime @@ -106,8 +106,8 @@ fun ResultCell( Text( stringResource( when (item.result.taskOrigin) { - TaskOrigin.AutoRun -> Res.string.task_origin_auto_run - TaskOrigin.OoniRun -> Res.string.task_origin_manual + TaskOrigin.AutoRun -> Res.string.TaskOrigin_AutoRun + TaskOrigin.OoniRun -> Res.string.TaskOrigin_Manual }, ), style = MaterialTheme.typography.labelLarge, @@ -115,8 +115,8 @@ fun ResultCell( ) if (!hasError) { Text( - pluralStringResource( - Res.plurals.measurements_count, + pluralStringResourceItem( + Res.plurals.Measurements_Count, item.measurementsCount.toInt(), item.measurementsCount, ), diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/results/ResultFilterViews.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/results/ResultFilterViews.kt index 6c9b0b88..611c2a25 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/results/ResultFilterViews.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/results/ResultFilterViews.kt @@ -13,10 +13,10 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import ooniprobe.composeapp.generated.resources.Res -import ooniprobe.composeapp.generated.resources.task_origin_all -import ooniprobe.composeapp.generated.resources.task_origin_auto_run -import ooniprobe.composeapp.generated.resources.task_origin_manual -import ooniprobe.composeapp.generated.resources.test_type_all +import ooniprobe.composeapp.generated.resources.Results_TaskOrigin_All +import ooniprobe.composeapp.generated.resources.Results_TestType_All +import ooniprobe.composeapp.generated.resources.TaskOrigin_AutoRun +import ooniprobe.composeapp.generated.resources.TaskOrigin_Manual import org.jetbrains.compose.resources.stringResource import org.ooni.engine.models.TaskOrigin import org.ooni.probe.data.models.Descriptor @@ -108,16 +108,16 @@ fun OriginFilter( @Composable private fun ResultFilter.Type.label() = when (this) { - ResultFilter.Type.All -> stringResource(Res.string.test_type_all) + ResultFilter.Type.All -> stringResource(Res.string.Results_TestType_All) is ResultFilter.Type.One -> value.title() } @Composable private fun ResultFilter.Type.name() = when (this) { - ResultFilter.Type.All -> stringResource(Res.string.task_origin_all) + ResultFilter.Type.All -> stringResource(Res.string.Results_TaskOrigin_All) is ResultFilter.Type.One -> when (value) { - TaskOrigin.AutoRun -> stringResource(Res.string.task_origin_auto_run) - TaskOrigin.OoniRun -> stringResource(Res.string.task_origin_manual) + TaskOrigin.AutoRun -> stringResource(Res.string.TaskOrigin_AutoRun) + TaskOrigin.OoniRun -> stringResource(Res.string.TaskOrigin_Manual) } } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/results/ResultsScreen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/results/ResultsScreen.kt index ebe9d25a..3053b1ea 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/results/ResultsScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/results/ResultsScreen.kt @@ -46,6 +46,7 @@ import ooniprobe.composeapp.generated.resources.Modal_Cancel import ooniprobe.composeapp.generated.resources.Modal_Delete import ooniprobe.composeapp.generated.resources.Modal_DoYouWantToDeleteAllTests import ooniprobe.composeapp.generated.resources.Res +import ooniprobe.composeapp.generated.resources.Results_LimitedNotice import ooniprobe.composeapp.generated.resources.Snackbar_ResultsSomeNotUploaded_Text import ooniprobe.composeapp.generated.resources.Snackbar_ResultsSomeNotUploaded_UploadAll import ooniprobe.composeapp.generated.resources.TestResults_Overview_FilterTests @@ -59,12 +60,10 @@ import ooniprobe.composeapp.generated.resources.TestResults_Summary_Performance_ import ooniprobe.composeapp.generated.resources.ic_delete_all import ooniprobe.composeapp.generated.resources.ic_download import ooniprobe.composeapp.generated.resources.ic_upload -import ooniprobe.composeapp.generated.resources.months import ooniprobe.composeapp.generated.resources.ooni_empty_state -import ooniprobe.composeapp.generated.resources.results_limited_notice import org.jetbrains.compose.resources.painterResource -import org.jetbrains.compose.resources.stringArrayResource import org.jetbrains.compose.resources.stringResource +import org.ooni.probe.shared.stringMonthArrayResource import org.ooni.probe.ui.shared.TopBar import org.ooni.probe.ui.shared.formatDataUsage import org.ooni.probe.ui.shared.isHeightCompact @@ -151,7 +150,7 @@ fun ResultsScreen( item("limited") { Text( text = stringResource( - Res.string.results_limited_notice, + Res.string.Results_LimitedNotice, state.filter.limit, ), style = MaterialTheme.typography.labelLarge, @@ -316,7 +315,7 @@ private fun Summary(summary: ResultsViewModel.Summary?) { @Composable private fun ResultDateHeader(date: LocalDate) { - val monthNames = stringArrayResource(Res.array.months) + val monthNames = stringMonthArrayResource() Text( date.format( Format { diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/run/RunScreen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/run/RunScreen.kt index 336d359c..054ba640 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/run/RunScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/run/RunScreen.kt @@ -43,6 +43,9 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.Role import androidx.compose.ui.state.ToggleableState import androidx.compose.ui.unit.dp +import ooniprobe.composeapp.generated.resources.Common_Back +import ooniprobe.composeapp.generated.resources.Common_Collapse +import ooniprobe.composeapp.generated.resources.Common_Expand import ooniprobe.composeapp.generated.resources.Dashboard_RunTests_Description import ooniprobe.composeapp.generated.resources.Dashboard_RunTests_RunButton_Label import ooniprobe.composeapp.generated.resources.Dashboard_RunTests_SelectAll @@ -55,21 +58,18 @@ import ooniprobe.composeapp.generated.resources.Modal_DisableVPN_Title import ooniprobe.composeapp.generated.resources.Modal_OK import ooniprobe.composeapp.generated.resources.Modal_RunAnyway import ooniprobe.composeapp.generated.resources.Res -import ooniprobe.composeapp.generated.resources.back -import ooniprobe.composeapp.generated.resources.collapse -import ooniprobe.composeapp.generated.resources.disable_vpn_instructions -import ooniprobe.composeapp.generated.resources.expand +import ooniprobe.composeapp.generated.resources.Settings_DisableVpnInstructions import ooniprobe.composeapp.generated.resources.ic_keyboard_arrow_down import ooniprobe.composeapp.generated.resources.ic_keyboard_arrow_up import ooniprobe.composeapp.generated.resources.ic_timer import org.jetbrains.compose.resources.painterResource -import org.jetbrains.compose.resources.pluralStringResource import org.jetbrains.compose.resources.stringResource import org.ooni.engine.models.TestType import org.ooni.probe.config.OrganizationConfig import org.ooni.probe.config.TestDisplayMode import org.ooni.probe.data.models.Descriptor import org.ooni.probe.data.models.NetTest +import org.ooni.probe.shared.pluralStringResourceItem import org.ooni.probe.ui.dashboard.TestDescriptorLabel import org.ooni.probe.ui.dashboard.TestDescriptorSection import org.ooni.probe.ui.shared.ParentSelectableItem @@ -91,7 +91,7 @@ fun RunScreen( IconButton(onClick = { onEvent(RunViewModel.Event.BackClicked) }) { Icon( Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(Res.string.back), + contentDescription = stringResource(Res.string.Common_Back), ) } }, @@ -189,7 +189,7 @@ fun RunScreen( .padding(WindowInsets.navigationBars.asPaddingValues()), ) { Text( - text = pluralStringResource( + text = pluralStringResourceItem( Res.plurals.Dashboard_RunTests_RunButton_Label, selectedTestsCount, selectedTestsCount, @@ -291,9 +291,9 @@ private fun DescriptorItem( ), contentDescription = stringResource( if (descriptorItem.isExpanded) { - Res.string.collapse + Res.string.Common_Collapse } else { - Res.string.expand + Res.string.Common_Expand }, ), ) @@ -380,7 +380,7 @@ private fun DisableVpnInstructionsDialog(onDismiss: () -> Unit) { AlertDialog( onDismissRequest = { onDismiss() }, title = { Text(stringResource(Res.string.Modal_DisableVPN)) }, - text = { Text(stringResource(Res.string.disable_vpn_instructions)) }, + text = { Text(stringResource(Res.string.Settings_DisableVpnInstructions)) }, confirmButton = { TextButton(onClick = { onDismiss() }) { Text(stringResource(Res.string.Modal_OK)) 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 8950e440..16a4af3e 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/commonMain/kotlin/org/ooni/probe/ui/running/RunningScreen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/running/RunningScreen.kt index be11f3ca..4f6298cc 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/running/RunningScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/running/RunningScreen.kt @@ -34,6 +34,7 @@ import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import ooniprobe.composeapp.generated.resources.Common_Back import ooniprobe.composeapp.generated.resources.Dashboard_Running_EstimatedTimeLeft import ooniprobe.composeapp.generated.resources.Dashboard_Running_ProxyInUse import ooniprobe.composeapp.generated.resources.Dashboard_Running_Running @@ -41,7 +42,6 @@ import ooniprobe.composeapp.generated.resources.Dashboard_Running_Stopping_Notic import ooniprobe.composeapp.generated.resources.Dashboard_Running_Stopping_Title import ooniprobe.composeapp.generated.resources.Notification_StopTest import ooniprobe.composeapp.generated.resources.Res -import ooniprobe.composeapp.generated.resources.back import ooniprobe.composeapp.generated.resources.ooni_empty_state import ooniprobe.composeapp.generated.resources.test_circumvention import org.jetbrains.compose.resources.painterResource @@ -73,7 +73,7 @@ fun RunningScreen( IconButton(onClick = { onEvent(RunningViewModel.Event.BackClicked) }) { Icon( Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(Res.string.back), + contentDescription = stringResource(Res.string.Common_Back), ) } }, diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/about/AboutScreen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/about/AboutScreen.kt index c26cd225..0d367611 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/about/AboutScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/about/AboutScreen.kt @@ -26,9 +26,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import ooniprobe.composeapp.generated.resources.Common_Back import ooniprobe.composeapp.generated.resources.Res import ooniprobe.composeapp.generated.resources.Settings_About_Content_Paragraph -import ooniprobe.composeapp.generated.resources.back import ooniprobe.composeapp.generated.resources.version import org.jetbrains.compose.resources.stringResource import org.ooni.probe.ui.shared.MarkdownViewer @@ -53,7 +53,7 @@ fun AboutScreen( IconButton(onClick = { onEvent(AboutViewModel.Event.BackClicked) }) { Icon( Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(Res.string.back), + contentDescription = stringResource(Res.string.Common_Back), ) } }, diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryScreen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryScreen.kt index e0c07bcb..6d04f733 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryScreen.kt @@ -38,10 +38,10 @@ import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import ooniprobe.composeapp.generated.resources.Common_Back import ooniprobe.composeapp.generated.resources.Modal_Cancel import ooniprobe.composeapp.generated.resources.Modal_OK import ooniprobe.composeapp.generated.resources.Res -import ooniprobe.composeapp.generated.resources.back import org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.painterResource @@ -65,7 +65,7 @@ fun SettingsCategoryScreen( IconButton(onClick = { onEvent(SettingsCategoryViewModel.Event.BackClicked) }) { Icon( Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(Res.string.back), + contentDescription = stringResource(Res.string.Common_Back), ) } }, @@ -78,69 +78,73 @@ fun SettingsCategoryScreen( val category = state.category ?: return Column { category.settings?.forEach { preferenceItem -> - when (preferenceItem.type) { - PreferenceItemType.SWITCH -> - SwitchSettingsView( - icon = preferenceItem.icon, - title = preferenceItem.title, - key = preferenceItem.key, - checked = state.preferences[preferenceItem.key] == true, - enabled = preferenceItem.enabled, - supportingContent = preferenceItem.supportingContent, - onCheckedChange = { key, value -> - onEvent( - SettingsCategoryViewModel.Event.CheckedChangeClick( - key, - value, - ), - ) - }, - ) + Box( + modifier = Modifier.padding(start = 32.dp * preferenceItem.indentation), + ) { + when (preferenceItem.type) { + PreferenceItemType.SWITCH -> + SwitchSettingsView( + icon = preferenceItem.icon, + title = preferenceItem.title, + key = preferenceItem.key, + checked = state.preferences[preferenceItem.key] == true, + enabled = preferenceItem.enabled, + supportingContent = preferenceItem.supportingContent, + onCheckedChange = { key, value -> + onEvent( + SettingsCategoryViewModel.Event.CheckedChangeClick( + key, + value, + ), + ) + }, + ) - PreferenceItemType.INT -> - NumberPickerItem( - title = preferenceItem.title, - supportingContent = preferenceItem.supportingContent, - enabled = preferenceItem.enabled, - value = state.preferences[preferenceItem.key] as? Int, - onChanged = { - onEvent( - SettingsCategoryViewModel.Event.IntChanged( - preferenceItem.key, - it, - ), - ) - }, - ) + PreferenceItemType.INT -> + NumberPickerItem( + title = preferenceItem.title, + supportingContent = preferenceItem.supportingContent, + enabled = preferenceItem.enabled, + value = state.preferences[preferenceItem.key] as? Int, + onChanged = { + onEvent( + SettingsCategoryViewModel.Event.IntChanged( + preferenceItem.key, + it, + ), + ) + }, + ) - PreferenceItemType.BUTTON -> - RouteSettingsView( - title = preferenceItem.title, - supportingContent = preferenceItem.supportingContent, - trailingContent = preferenceItem.trailingContent, - ) + PreferenceItemType.BUTTON -> + RouteSettingsView( + title = preferenceItem.title, + supportingContent = preferenceItem.supportingContent, + trailingContent = preferenceItem.trailingContent, + ) - PreferenceItemType.ROUTE -> - RouteSettingsView( - title = preferenceItem.title, - supportingContent = preferenceItem.supportingContent, - modifier = - Modifier.clickable { - if (preferenceItem is SettingsCategoryItem) { - onEvent( - SettingsCategoryViewModel.Event.SettingsCategoryClick( - preferenceItem.route, - ), - ) - } - }, - ) + PreferenceItemType.ROUTE -> + RouteSettingsView( + title = preferenceItem.title, + supportingContent = preferenceItem.supportingContent, + modifier = + Modifier.clickable { + if (preferenceItem is SettingsCategoryItem) { + onEvent( + SettingsCategoryViewModel.Event.SettingsCategoryClick( + preferenceItem.route, + ), + ) + } + }, + ) - PreferenceItemType.SELECT -> - RouteSettingsView( - title = preferenceItem.title, - supportingContent = preferenceItem.supportingContent, - ) + PreferenceItemType.SELECT -> + RouteSettingsView( + title = preferenceItem.title, + supportingContent = preferenceItem.supportingContent, + ) + } } } category.footerContent?.let { diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/proxy/ProxyScreen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/proxy/ProxyScreen.kt index ea271a8a..1e496573 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/proxy/ProxyScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/proxy/ProxyScreen.kt @@ -35,12 +35,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp +import ooniprobe.composeapp.generated.resources.Common_Back import ooniprobe.composeapp.generated.resources.Res import ooniprobe.composeapp.generated.resources.Settings_Proxy_Custom_Hostname import ooniprobe.composeapp.generated.resources.Settings_Proxy_Custom_Port import ooniprobe.composeapp.generated.resources.Settings_Proxy_Custom_Protocol import ooniprobe.composeapp.generated.resources.Settings_Proxy_Enabled -import ooniprobe.composeapp.generated.resources.back import org.jetbrains.compose.resources.stringResource import org.ooni.probe.data.models.ProxyProtocol import org.ooni.probe.data.models.ProxyType @@ -62,7 +62,7 @@ fun ProxyScreen( }) { Icon( Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(Res.string.back), + contentDescription = stringResource(Res.string.Common_Back), ) } }, diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/shared/DateFormats.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/shared/DateFormats.kt index 740e1db0..e37f1172 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/shared/DateFormats.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/shared/DateFormats.kt @@ -8,15 +8,15 @@ import kotlinx.datetime.TimeZone import kotlinx.datetime.format import kotlinx.datetime.format.char import kotlinx.datetime.toInstant +import ooniprobe.composeapp.generated.resources.Common_Ago +import ooniprobe.composeapp.generated.resources.Common_Hours +import ooniprobe.composeapp.generated.resources.Common_Hours_Abbreviated +import ooniprobe.composeapp.generated.resources.Common_Minutes +import ooniprobe.composeapp.generated.resources.Common_Minutes_Abbreviated +import ooniprobe.composeapp.generated.resources.Common_Seconds_Abbreviated import ooniprobe.composeapp.generated.resources.Res -import ooniprobe.composeapp.generated.resources.ago -import ooniprobe.composeapp.generated.resources.hours -import ooniprobe.composeapp.generated.resources.hours_abbreviated -import ooniprobe.composeapp.generated.resources.minutes -import ooniprobe.composeapp.generated.resources.minutes_abbreviated -import ooniprobe.composeapp.generated.resources.seconds_abbreviated -import org.jetbrains.compose.resources.pluralStringResource import org.jetbrains.compose.resources.stringResource +import org.ooni.probe.shared.pluralStringResourceItem import org.ooni.probe.shared.today import kotlin.time.Duration @@ -44,12 +44,12 @@ fun LocalDateTime.relativeDateTime(): String = val diff = (Clock.System.now() - toInstant(TimeZone.currentSystemDefault())) val diffString = diff.toComponents { hours, minutes, _, _ -> if (hours > 0) { - pluralStringResource(Res.plurals.hours, hours.toInt(), hours.toInt()) + pluralStringResourceItem(Res.plurals.Common_Hours, hours.toInt(), hours.toInt()) } else { - pluralStringResource(Res.plurals.minutes, minutes, minutes) + pluralStringResourceItem(Res.plurals.Common_Minutes, minutes, minutes) } } - stringResource(Res.string.ago, diffString) + stringResource(Res.string.Common_Ago, diffString) } else { longFormat() } @@ -61,9 +61,23 @@ fun LocalDateTime.logFormat(): String = format(logDateTimeFormat) @Composable fun Duration.shortFormat(): String = toComponents { hours, minutes, seconds, _ -> - (if (hours > 0) stringResource(Res.string.hours_abbreviated, hours.toInt()) else "") + - " " + - (if (minutes > 0) stringResource(Res.string.minutes_abbreviated, minutes) else "") + - " " + - (if (seconds > 0) stringResource(Res.string.seconds_abbreviated, seconds) else "") + ( + if (hours > 0) { + stringResource(Res.string.Common_Hours_Abbreviated, hours.toInt()) + } else { + "" + } + ) + " " + ( + if (minutes > 0) { + stringResource(Res.string.Common_Minutes_Abbreviated, minutes) + } else { + "" + } + ) + " " + ( + if (seconds > 0) { + stringResource(Res.string.Common_Seconds_Abbreviated, seconds) + } else { + "" + } + ) }.trimStart() diff --git a/composeApp/src/commonMain/release/kotlin/org/ooni/probe/config/BuildTypeDefaults.kt b/composeApp/src/commonMain/release/kotlin/org/ooni/probe/config/BuildTypeDefaults.kt new file mode 100644 index 00000000..ce360acd --- /dev/null +++ b/composeApp/src/commonMain/release/kotlin/org/ooni/probe/config/BuildTypeDefaults.kt @@ -0,0 +1,8 @@ +package org.ooni.probe.config + +object BuildTypeDefaults : BuildTypeDefaultsInterface { + override val ooniApiBaseUrl = "https://api.ooni.org" + override val ooniRunDomain = "run.ooni.org" + override val ooniRunDashboardUrl = "https://run.ooni.org" + override val explorerUrl = "https://explorer.ooni.org" +} diff --git a/composeApp/src/commonMain/release/kotlin/org/ooni/probe/config/OrganizationConfigInterface.kt b/composeApp/src/commonMain/release/kotlin/org/ooni/probe/config/OrganizationConfigInterface.kt deleted file mode 100644 index 79889cd9..00000000 --- a/composeApp/src/commonMain/release/kotlin/org/ooni/probe/config/OrganizationConfigInterface.kt +++ /dev/null @@ -1,25 +0,0 @@ -package org.ooni.probe.config - -interface OrganizationConfigInterface { - val baseSoftwareName: String - - val ooniApiBaseUrl: String - get() = "https://api.ooni.org" - - val ooniRunDomain: String - get() = "run.ooni.org" - - val ooniRunDashboardUrl: String - get() = "https://run.ooni.org" - - val explorerUrl: String - get() = "https://explorer.test.ooni.org" - - val testDisplayMode: TestDisplayMode - - val autorunTaskId: String - - val onboardingImages: OnboardingImages - - val updateDescriptorTaskId: String -} diff --git a/composeApp/src/commonTest/kotlin/org/ooni/engine/TestOonimkallBridge.kt b/composeApp/src/commonTest/kotlin/org/ooni/engine/TestOonimkallBridge.kt index 250dd1b1..dff97e4d 100644 --- a/composeApp/src/commonTest/kotlin/org/ooni/engine/TestOonimkallBridge.kt +++ b/composeApp/src/commonTest/kotlin/org/ooni/engine/TestOonimkallBridge.kt @@ -46,5 +46,7 @@ class TestOonimkallBridge : OonimkallBridge { override fun checkIn(config: OonimkallBridge.CheckInConfig): OonimkallBridge.CheckInResults = checkInMock!!(config) override fun httpDo(request: OonimkallBridge.HTTPRequest): OonimkallBridge.HTTPResponse = httpDoMock!!(request) + + override fun close() {} } } diff --git a/composeApp/src/commonTest/kotlin/org/ooni/probe/background/RunBackgroundTaskTest.kt b/composeApp/src/commonTest/kotlin/org/ooni/probe/background/RunBackgroundTaskTest.kt new file mode 100644 index 00000000..d77295fa --- /dev/null +++ b/composeApp/src/commonTest/kotlin/org/ooni/probe/background/RunBackgroundTaskTest.kt @@ -0,0 +1,101 @@ +package org.ooni.probe.background + +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.ooni.engine.models.NetworkType +import org.ooni.engine.models.TaskOrigin +import org.ooni.probe.data.models.ResultModel +import org.ooni.probe.data.models.RunBackgroundState +import org.ooni.probe.data.models.RunSpecification +import org.ooni.probe.data.models.SettingsKey +import org.ooni.probe.domain.UploadMissingMeasurements +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class RunBackgroundTaskTest { + @Test + fun skipAutoRunIfVpnIsEnabled() = + runTest { + var wasRunDescriptorsCalled = false + val state = MutableStateFlow(RunBackgroundState.Idle()) + val subject = buildSubject( + getNetworkType = { NetworkType.VPN }, + getRunBackgroundState = { state }, + runDescriptors = { + wasRunDescriptorsCalled = true + state.value = RunBackgroundState.RunningTests() + delay(100) + state.value = RunBackgroundState.Idle() + }, + ) + + subject(null).collect() + + assertFalse(wasRunDescriptorsCalled) + } + + @Test + fun runManualRunIfVpnIsEnabled() = + runTest { + var wasRunDescriptorsCalled = false + val state = MutableStateFlow(RunBackgroundState.Idle()) + val subject = buildSubject( + getNetworkType = { NetworkType.VPN }, + getRunBackgroundState = { state }, + runDescriptors = { + wasRunDescriptorsCalled = true + state.value = RunBackgroundState.RunningTests() + delay(100) + state.value = RunBackgroundState.Idle() + }, + ) + + subject( + RunSpecification.Full( + tests = emptyList(), + taskOrigin = TaskOrigin.OoniRun, + isRerun = false, + ), + ).collect() + + assertTrue(wasRunDescriptorsCalled) + } + + private fun buildSubject( + getPreferenceValueByKey: (SettingsKey) -> Flow = { flowOf(true) }, + uploadMissingMeasurements: (ResultModel.Id?) -> Flow = { emptyFlow() }, + checkSkipAutoRunNotUploadedLimit: () -> Flow = { flowOf(false) }, + getNetworkType: () -> NetworkType = { NetworkType.Wifi }, + getAutoRunSpecification: suspend () -> RunSpecification.Full = { + RunSpecification.Full( + tests = emptyList(), + taskOrigin = TaskOrigin.AutoRun, + isRerun = false, + ) + }, + runDescriptors: suspend (RunSpecification) -> Unit = {}, + setRunBackgroundState: ((RunBackgroundState) -> RunBackgroundState) -> Unit = {}, + getRunBackgroundState: () -> Flow = { flowOf(RunBackgroundState.Idle()) }, + addRunCancelListener: (() -> Unit) -> Unit = {}, + clearRunCancelListeners: () -> Unit = {}, + getLatestResult: () -> Flow = { flowOf(null) }, + ) = RunBackgroundTask( + getPreferenceValueByKey = getPreferenceValueByKey, + uploadMissingMeasurements = uploadMissingMeasurements, + checkSkipAutoRunNotUploadedLimit = checkSkipAutoRunNotUploadedLimit, + getNetworkType = getNetworkType, + getAutoRunSpecification = getAutoRunSpecification, + runDescriptors = runDescriptors, + setRunBackgroundState = setRunBackgroundState, + getRunBackgroundState = getRunBackgroundState, + addRunCancelListener = addRunCancelListener, + clearRunCancelListeners = clearRunCancelListeners, + getLatestResult = getLatestResult, + ) +} 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) diff --git a/composeApp/src/dwMain/kotlin/org/ooni/probe/config/OrganizationConfig.kt b/composeApp/src/dwMain/kotlin/org/ooni/probe/config/OrganizationConfig.kt index 6237f0cf..fa7326fb 100644 --- a/composeApp/src/dwMain/kotlin/org/ooni/probe/config/OrganizationConfig.kt +++ b/composeApp/src/dwMain/kotlin/org/ooni/probe/config/OrganizationConfig.kt @@ -13,4 +13,5 @@ object OrganizationConfig : OrganizationConfigInterface { image2 = Res.drawable.onboarding, image3 = Res.drawable.onboarding, ) + override val hasWebsitesDescriptor = false } diff --git a/composeApp/src/dwMain/kotlin/org/ooni/probe/config/OrganizationSettings.kt b/composeApp/src/dwMain/kotlin/org/ooni/probe/config/OrganizationSettings.kt index 05538846..e69de29b 100644 --- a/composeApp/src/dwMain/kotlin/org/ooni/probe/config/OrganizationSettings.kt +++ b/composeApp/src/dwMain/kotlin/org/ooni/probe/config/OrganizationSettings.kt @@ -1,16 +0,0 @@ -package org.ooni.probe.domain - -import org.ooni.probe.data.models.PreferenceItem -import org.ooni.probe.data.models.SettingsKey - -fun webConnectivityPreferences( - enabledCategoriesCount: Int, - maxRuntimeEnabled: Boolean, - maxRuntime: Int?, -): List { - return emptyList() -} - -fun preferenceDefaults(): List> { - return emptyList() -} diff --git a/composeApp/src/dwMain/kotlin/org/ooni/probe/domain/OrganizationPreferenceDefaults.kt b/composeApp/src/dwMain/kotlin/org/ooni/probe/domain/OrganizationPreferenceDefaults.kt new file mode 100644 index 00000000..284516ed --- /dev/null +++ b/composeApp/src/dwMain/kotlin/org/ooni/probe/domain/OrganizationPreferenceDefaults.kt @@ -0,0 +1,8 @@ +package org.ooni.probe.domain + +import org.ooni.probe.data.models.PreferenceItem +import org.ooni.probe.data.models.SettingsKey + +fun organizationPreferenceDefaults(): List> { + return emptyList() +} diff --git a/composeApp/src/iosMain/kotlin/org/ooni/probe/SetupDependencies.kt b/composeApp/src/iosMain/kotlin/org/ooni/probe/SetupDependencies.kt index 308d98bc..0ef4c7ff 100644 --- a/composeApp/src/iosMain/kotlin/org/ooni/probe/SetupDependencies.kt +++ b/composeApp/src/iosMain/kotlin/org/ooni/probe/SetupDependencies.kt @@ -162,7 +162,6 @@ class SetupDependencies( private fun sendMail(action: PlatformAction.Mail): Boolean { MFMailComposeViewController.canSendMail().let { canSendMail -> - val email = action.to.removePrefix("mailto:") if (canSendMail) { MFMailComposeViewController().apply { mailComposeDelegate = object : @@ -176,7 +175,7 @@ class SetupDependencies( controller.dismissViewControllerAnimated(true, null) } } - setToRecipients(listOf(email)) + setToRecipients(listOf(action.to)) setSubject(action.subject) setMessageBody(action.body, isHTML = false) }.let { @@ -188,7 +187,7 @@ class SetupDependencies( } return true } else { - UIPasteboard.generalPasteboard.string = email + UIPasteboard.generalPasteboard.string = action.to return false } } diff --git a/composeApp/src/ooniMain/kotlin/org/ooni/probe/config/OrganizationConfig.kt b/composeApp/src/ooniMain/kotlin/org/ooni/probe/config/OrganizationConfig.kt index e8ec35c7..87dc2ff9 100644 --- a/composeApp/src/ooniMain/kotlin/org/ooni/probe/config/OrganizationConfig.kt +++ b/composeApp/src/ooniMain/kotlin/org/ooni/probe/config/OrganizationConfig.kt @@ -15,4 +15,5 @@ object OrganizationConfig : OrganizationConfigInterface { image2 = Res.drawable.onboarding2, image3 = Res.drawable.onboarding3, ) + override val hasWebsitesDescriptor = true } diff --git a/composeApp/src/ooniMain/kotlin/org/ooni/probe/domain/OrganizationPreferenceDefaults.kt b/composeApp/src/ooniMain/kotlin/org/ooni/probe/domain/OrganizationPreferenceDefaults.kt new file mode 100644 index 00000000..31de858b --- /dev/null +++ b/composeApp/src/ooniMain/kotlin/org/ooni/probe/domain/OrganizationPreferenceDefaults.kt @@ -0,0 +1,13 @@ +package org.ooni.probe.domain + +import org.ooni.engine.models.WebConnectivityCategory +import org.ooni.probe.data.models.SettingsKey + +fun organizationPreferenceDefaults(): List> { + return listOf( + SettingsKey.MAX_RUNTIME_ENABLED to true, + SettingsKey.MAX_RUNTIME to 90, + ) + WebConnectivityCategory.entries + .mapNotNull { it.settingsKey } + .map { it to true } +} diff --git a/composeApp/src/ooniMain/kotlin/org/ooni/probe/domain/OrganizationSettings.kt b/composeApp/src/ooniMain/kotlin/org/ooni/probe/domain/OrganizationSettings.kt deleted file mode 100644 index d6112600..00000000 --- a/composeApp/src/ooniMain/kotlin/org/ooni/probe/domain/OrganizationSettings.kt +++ /dev/null @@ -1,73 +0,0 @@ -package org.ooni.probe.domain - -import androidx.compose.material3.Text -import ooniprobe.composeapp.generated.resources.Res -import ooniprobe.composeapp.generated.resources.Settings_Websites_Categories_Description -import ooniprobe.composeapp.generated.resources.Settings_Websites_Categories_Label -import ooniprobe.composeapp.generated.resources.Settings_Websites_MaxRuntime -import ooniprobe.composeapp.generated.resources.Settings_Websites_MaxRuntimeEnabled -import org.jetbrains.compose.resources.stringResource -import org.ooni.engine.models.WebConnectivityCategory -import org.ooni.probe.data.models.PreferenceCategoryKey -import org.ooni.probe.data.models.PreferenceItem -import org.ooni.probe.data.models.PreferenceItemType -import org.ooni.probe.data.models.SettingsCategoryItem -import org.ooni.probe.data.models.SettingsItem -import org.ooni.probe.data.models.SettingsKey -import org.ooni.probe.ui.shared.shortFormat -import kotlin.time.Duration.Companion.seconds - -fun webConnectivityPreferences( - enabledCategoriesCount: Int, - maxRuntimeEnabled: Boolean, - maxRuntime: Int?, -): List { - return listOf( - SettingsCategoryItem( - title = Res.string.Settings_Websites_Categories_Label, - route = PreferenceCategoryKey.WEBSITES_CATEGORIES, - supportingContent = { - Text( - stringResource( - Res.string.Settings_Websites_Categories_Description, - enabledCategoriesCount, - ), - ) - }, - settings = WebConnectivityCategory.entries.mapNotNull { cat -> - SettingsItem( - icon = cat.icon, - title = cat.title, - supportingContent = { Text(stringResource(cat.description)) }, - key = cat.settingsKey ?: return@mapNotNull null, - type = PreferenceItemType.SWITCH, - ) - }, - ), - SettingsItem( - title = Res.string.Settings_Websites_MaxRuntimeEnabled, - key = SettingsKey.MAX_RUNTIME_ENABLED, - type = PreferenceItemType.SWITCH, - ), - SettingsItem( - title = Res.string.Settings_Websites_MaxRuntime, - key = SettingsKey.MAX_RUNTIME, - type = PreferenceItemType.INT, - enabled = maxRuntimeEnabled, - supportingContent = { - maxRuntime?.let { - Text(it.coerceAtLeast(0).seconds.shortFormat()) - } - }, - ), - ) -} - -fun preferenceDefaults(): List> { - return listOf( - SettingsKey.MAX_RUNTIME_ENABLED to true, - SettingsKey.MAX_RUNTIME to 90, - ) + WebConnectivityCategory.entries - .mapNotNull { it.settingsKey } - .map { it to true } -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3249c1f6..e2f4d604 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -43,7 +43,7 @@ androidx-datastore-core-okio = { group = "androidx.datastore", name = "datastore androidx-datastore-preferences-core = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "dataStoreVersion" } # Engine -android-oonimkall = { module = "org.ooni:oonimkall", version = "2024.08.08-091959" } +android-oonimkall = { module = "org.ooni:oonimkall", version = "2024.11.26-053000" } # Logging kermit = { module = "co.touchlab:kermit", version = "2.0.4" } diff --git a/iosApp/Podfile b/iosApp/Podfile index 2e032086..8e9e141c 100644 --- a/iosApp/Podfile +++ b/iosApp/Podfile @@ -2,7 +2,7 @@ platform :ios, '14.0' use_frameworks! def shared_pods - ooni_version = "v3.22.0" + ooni_version = "v3.24.0" ooni_pods_location = "https://github.com/ooni/probe-cli/releases/download/#{ooni_version}" pod 'composeApp', :path => '../composeApp' diff --git a/iosApp/Podfile.lock b/iosApp/Podfile.lock index 74e7870f..e6910a28 100644 --- a/iosApp/Podfile.lock +++ b/iosApp/Podfile.lock @@ -1,12 +1,12 @@ PODS: - composeApp (1.0): - Sentry (~> 8.38.0) - - libcrypto (2024.05.22-093305) - - libevent (2024.05.22-093305) - - libssl (2024.05.22-093305) - - libtor (2024.05.22-093305) - - libz (2024.05.22-093305) - - oonimkall (2024.05.22-093305) + - libcrypto (2024.11.26-053946) + - libevent (2024.11.26-053946) + - libssl (2024.11.26-053946) + - libtor (2024.11.26-053946) + - libz (2024.11.26-053946) + - oonimkall (2024.11.26-053946) - Sentry (8.38.0): - Sentry/Core (= 8.38.0) - Sentry/Core (8.38.0) @@ -16,12 +16,12 @@ PODS: DEPENDENCIES: - composeApp (from `../composeApp`) - - libcrypto (from `https://github.com/ooni/probe-cli/releases/download/v3.22.0/libcrypto.podspec`) - - libevent (from `https://github.com/ooni/probe-cli/releases/download/v3.22.0/libevent.podspec`) - - libssl (from `https://github.com/ooni/probe-cli/releases/download/v3.22.0/libssl.podspec`) - - libtor (from `https://github.com/ooni/probe-cli/releases/download/v3.22.0/libtor.podspec`) - - libz (from `https://github.com/ooni/probe-cli/releases/download/v3.22.0/libz.podspec`) - - oonimkall (from `https://github.com/ooni/probe-cli/releases/download/v3.22.0/oonimkall.podspec`) + - libcrypto (from `https://github.com/ooni/probe-cli/releases/download/v3.24.0/libcrypto.podspec`) + - libevent (from `https://github.com/ooni/probe-cli/releases/download/v3.24.0/libevent.podspec`) + - libssl (from `https://github.com/ooni/probe-cli/releases/download/v3.24.0/libssl.podspec`) + - libtor (from `https://github.com/ooni/probe-cli/releases/download/v3.24.0/libtor.podspec`) + - libz (from `https://github.com/ooni/probe-cli/releases/download/v3.24.0/libz.podspec`) + - oonimkall (from `https://github.com/ooni/probe-cli/releases/download/v3.24.0/oonimkall.podspec`) - sqlite3 (~> 3.42.0) SPEC REPOS: @@ -33,29 +33,29 @@ EXTERNAL SOURCES: composeApp: :path: "../composeApp" libcrypto: - :podspec: https://github.com/ooni/probe-cli/releases/download/v3.22.0/libcrypto.podspec + :podspec: https://github.com/ooni/probe-cli/releases/download/v3.24.0/libcrypto.podspec libevent: - :podspec: https://github.com/ooni/probe-cli/releases/download/v3.22.0/libevent.podspec + :podspec: https://github.com/ooni/probe-cli/releases/download/v3.24.0/libevent.podspec libssl: - :podspec: https://github.com/ooni/probe-cli/releases/download/v3.22.0/libssl.podspec + :podspec: https://github.com/ooni/probe-cli/releases/download/v3.24.0/libssl.podspec libtor: - :podspec: https://github.com/ooni/probe-cli/releases/download/v3.22.0/libtor.podspec + :podspec: https://github.com/ooni/probe-cli/releases/download/v3.24.0/libtor.podspec libz: - :podspec: https://github.com/ooni/probe-cli/releases/download/v3.22.0/libz.podspec + :podspec: https://github.com/ooni/probe-cli/releases/download/v3.24.0/libz.podspec oonimkall: - :podspec: https://github.com/ooni/probe-cli/releases/download/v3.22.0/oonimkall.podspec + :podspec: https://github.com/ooni/probe-cli/releases/download/v3.24.0/oonimkall.podspec SPEC CHECKSUMS: composeApp: 675ce506dcd05e76ecb4fdd0f73a10d4af635af1 - libcrypto: 1bb58600c586e28688f5578f4675f5ffa46c8eaf - libevent: 5c8502ca5cc38be31bb510ddade0f238bcc5f0dc - libssl: 170bebcaf567a0285e91a8850b9686137d07c3e1 - libtor: c72b23da6a5d2e16173149784f11cf66156c35be - libz: 83658eb2a0db785623ffdf9ce13407e6b8b5c8f9 - oonimkall: 9768ce9dad18265d45d2ea972c84fb0bd5237cc3 + libcrypto: 7fb50209305641e986f6b1a1e4680ec4d71602e3 + libevent: b9f2a7f3b682f7a072f4d21edf360052d3a591dc + libssl: 115acb9ff8f7d20f437ff8e112f1d21f210dad94 + libtor: efd4a639943c12a239094c2927891578cb4c7fcc + libz: 8e83c9433082438bce962f836f845d835f26b850 + oonimkall: 935a4263650a75bef0d8b487f7d349094b4c2b73 Sentry: 205813e7e758b53df157cedb8c55b31a14300645 sqlite3: f163dbbb7aa3339ad8fc622782c2d9d7b72f7e9c -PODFILE CHECKSUM: 7ccceb3a8043e0a52e0aaf77c23a4e93dbe689e7 +PODFILE CHECKSUM: 59b6c2ae5c19ca87fa0a3f359deffa5e78551529 COCOAPODS: 1.16.2 diff --git a/iosApp/iosApp/engine/IosOonimkallBridge.swift b/iosApp/iosApp/engine/IosOonimkallBridge.swift index 3c101686..0b5668d8 100644 --- a/iosApp/iosApp/engine/IosOonimkallBridge.swift +++ b/iosApp/iosApp/engine/IosOonimkallBridge.swift @@ -33,51 +33,52 @@ class IosOonimkallBridge: OonimkallBridge { } func doNewSession(sessionConfig: OonimkallBridgeSessionConfig) throws -> OonimkallBridgeSession { - class IosSession: OonimkallBridgeSession { - private let sessionConfig: OonimkallSessionConfig + func error(_ message: String, code: Int = 0, domain: String = "IosOonimkallBridge", function: String = #function, file: String = #file, line: Int = #line) -> NSError { - init(sessionConfig: OonimkallSessionConfig) { - self.sessionConfig = sessionConfig - } + let functionKey = "\(domain).function" + let fileKey = "\(domain).file" + let lineKey = "\(domain).line" - func error(_ message: String, code: Int = 0, domain: String = "IosOonimkallBridge", function: String = #function, file: String = #file, line: Int = #line) -> NSError { + let error = NSError(domain: domain, code: code, userInfo: [ + message: message, + functionKey: function, + fileKey: file, + lineKey: line + ]) - let functionKey = "\(domain).function" - let fileKey = "\(domain).file" - let lineKey = "\(domain).line" + return error + } - let error = NSError(domain: domain, code: code, userInfo: [ - message: message, - functionKey: function, - fileKey: file, - lineKey: line - ]) + class IosSession: OonimkallBridgeSession { + private let session: OonimkallSession - return error + init(sessionConfig: OonimkallSessionConfig) throws { + var sessionError: NSError? + guard let session = OonimkallNewSession(sessionConfig, &sessionError) else { + throw error("Unable to create session") + } + // throw error if any + if sessionError != nil { + throw sessionError! + } + self.session = session } func checkIn(config: OonimkallBridgeCheckInConfig) throws -> OonimkallBridgeCheckInResults { - var error: NSError? - let ses = OonimkallNewSession(sessionConfig, &error) - // throw error if any - if error != nil { - throw error! - } - guard let context = ses?.newContext(withTimeout: CONTEXT_TIMEOUT) else { - throw self.error("Unable to create context") + guard let context = session.newContext(withTimeout: CONTEXT_TIMEOUT) else { + throw error("Unable to create context") } do { - let info = try ses?.check(in: context, config: config.toMk()) - - + let info = try session.check(in: context, config: config.toMk()) + var responseUrls = [OonimkallBridgeUrlInfo]() - let size = info?.webConnectivity?.size() ?? 0 + let size = info.webConnectivity?.size() ?? 0 for i in 0.. OonimkallBridgeHTTPResponse { - var error: NSError? - let ses = OonimkallNewSession(sessionConfig, &error) - // throw error if any - if error != nil { - throw error! - } - guard let context = ses?.newContext(withTimeout: CONTEXT_TIMEOUT) else { - throw self.error("Unable to create context") + guard let context = session.newContext(withTimeout: CONTEXT_TIMEOUT) else { + throw error("Unable to create context") } do { - let response = try ses?.httpDo(context, jreq: request.toMk()) - return OonimkallBridgeHTTPResponse(body: response?.body) + let response = try session.httpDo(context, jreq: request.toMk()) + return OonimkallBridgeHTTPResponse(body: response.body) } catch { throw error } } func submitMeasurement(measurement: String) throws -> OonimkallBridgeSubmitMeasurementResults { - var error: NSError? - let ses = OonimkallNewSession(sessionConfig, &error) - // throw error if any - if error != nil { - throw error! - } - guard let context = ses?.newContext(withTimeout: CONTEXT_TIMEOUT) else { - throw self.error("Unable to create context") + guard let context = session.newContext(withTimeout: CONTEXT_TIMEOUT) else { + throw error("Unable to create context") } do { - let result: OonimkallSubmitMeasurementResults? = try ses?.submit(context, measurement: measurement) + let result: OonimkallSubmitMeasurementResults? = try session.submit(context, measurement: measurement) return OonimkallBridgeSubmitMeasurementResults( updatedMeasurement: result?.updatedMeasurement, updatedReportId: result?.updatedReportID ?? "" @@ -136,9 +125,13 @@ class IosOonimkallBridge: OonimkallBridge { throw error } } + + func close() throws { + try session.close() + } } - return IosSession(sessionConfig: sessionConfig.toMk()) + return try IosSession(sessionConfig: sessionConfig.toMk()) } }