From 8e37f16104bb9b0ead8656a6657a7c660ca5f374 Mon Sep 17 00:00:00 2001 From: Norbel AMBANUMBEN Date: Tue, 6 Aug 2024 10:12:31 +0100 Subject: [PATCH 01/12] feat: start settings page --- composeApp/build.gradle.kts | 4 + .../org/ooni/probe/AndroidApplication.kt | 1 + .../org/ooni/probe/CreateDataStore.android.kt | 12 + .../composeResources/drawable/advanced.xml | 27 ++ .../drawable/notifications.xml | 13 + .../drawable/outline_info.xml | 10 + .../composeResources/drawable/privacy.xml | 13 + .../composeResources/drawable/proxy.xml | 13 + .../composeResources/drawable/send_email.xml | 15 + .../values/strings-common.xml | 28 ++ .../composeResources/values/untraslatable.xml | 69 +++++ .../kotlin/org/ooni/probe/CreateDataStore.kt | 34 +++ .../org/ooni/probe/data/SettingsRepository.kt | 23 ++ .../kotlin/org/ooni/probe/di/Dependencies.kt | 18 ++ .../ooni/probe/ui/navigation/Navigation.kt | 39 ++- .../org/ooni/probe/ui/navigation/Screen.kt | 7 + .../ooni/probe/ui/settings/SettingsScreen.kt | 138 ++++++++- .../probe/ui/settings/SettingsViewModel.kt | 27 ++ .../category/SettingsCategoryScreen.kt | 286 ++++++++++++++++++ .../category/SettingsCategoryViewModel.kt | 49 +++ .../resources/values/strings-organization.xml | 2 + .../kotlin/org/ooni/probe/CreateDataStore.kt | 24 ++ .../org/ooni/probe/SetupDependencies.kt | 1 + .../resources/values/strings-organization.xml | 1 + gradle/libs.versions.toml | 6 + 25 files changed, 851 insertions(+), 9 deletions(-) create mode 100644 composeApp/src/androidMain/kotlin/org/ooni/probe/CreateDataStore.android.kt create mode 100644 composeApp/src/commonMain/composeResources/drawable/advanced.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/notifications.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/outline_info.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/privacy.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/proxy.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/send_email.xml create mode 100644 composeApp/src/commonMain/composeResources/values/untraslatable.xml create mode 100644 composeApp/src/commonMain/kotlin/org/ooni/probe/CreateDataStore.kt create mode 100644 composeApp/src/commonMain/kotlin/org/ooni/probe/data/SettingsRepository.kt create mode 100644 composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsViewModel.kt create mode 100644 composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryScreen.kt create mode 100644 composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryViewModel.kt create mode 100644 composeApp/src/iosMain/kotlin/org/ooni/probe/CreateDataStore.kt diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index fa0dbfe9..ddbcedae 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -81,6 +81,10 @@ kotlin { implementation(libs.bundles.ui) implementation(libs.bundles.tooling) + implementation(libs.androidx.datastore.preferences.core) + implementation(libs.androidx.datastore.core.okio) + implementation(libs.kotlinx.atomicfu) + getByName("commonMain") { kotlin.srcDir(config.srcRoot) } diff --git a/composeApp/src/androidMain/kotlin/org/ooni/probe/AndroidApplication.kt b/composeApp/src/androidMain/kotlin/org/ooni/probe/AndroidApplication.kt index f1dd6f57..fbaf5725 100644 --- a/composeApp/src/androidMain/kotlin/org/ooni/probe/AndroidApplication.kt +++ b/composeApp/src/androidMain/kotlin/org/ooni/probe/AndroidApplication.kt @@ -18,6 +18,7 @@ class AndroidApplication : Application() { oonimkallBridge = AndroidOonimkallBridge(), baseFileDir = filesDir.absolutePath, cacheDir = cacheDir.absolutePath, + dataStore = getDataStore(this), ) } diff --git a/composeApp/src/androidMain/kotlin/org/ooni/probe/CreateDataStore.android.kt b/composeApp/src/androidMain/kotlin/org/ooni/probe/CreateDataStore.android.kt new file mode 100644 index 00000000..63a26e90 --- /dev/null +++ b/composeApp/src/androidMain/kotlin/org/ooni/probe/CreateDataStore.android.kt @@ -0,0 +1,12 @@ +package org.ooni.probe + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.SharedPreferencesMigration +import androidx.datastore.preferences.core.Preferences + +fun getDataStore(context: Context): DataStore = + getDataStore( + producePath = { context.filesDir.resolve(DATA_STORE_FILE_NAME).absolutePath }, + migrations = listOf(SharedPreferencesMigration(context, "notifications_enabled")), + ) diff --git a/composeApp/src/commonMain/composeResources/drawable/advanced.xml b/composeApp/src/commonMain/composeResources/drawable/advanced.xml new file mode 100644 index 00000000..1ab4df6a --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/advanced.xml @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/notifications.xml b/composeApp/src/commonMain/composeResources/drawable/notifications.xml new file mode 100644 index 00000000..296b925d --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/notifications.xml @@ -0,0 +1,13 @@ + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/outline_info.xml b/composeApp/src/commonMain/composeResources/drawable/outline_info.xml new file mode 100644 index 00000000..dd51cba7 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/outline_info.xml @@ -0,0 +1,10 @@ + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/privacy.xml b/composeApp/src/commonMain/composeResources/drawable/privacy.xml new file mode 100644 index 00000000..c756a2b8 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/privacy.xml @@ -0,0 +1,13 @@ + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/proxy.xml b/composeApp/src/commonMain/composeResources/drawable/proxy.xml new file mode 100644 index 00000000..01f9f771 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/proxy.xml @@ -0,0 +1,13 @@ + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/send_email.xml b/composeApp/src/commonMain/composeResources/drawable/send_email.xml new file mode 100644 index 00000000..cd2d858d --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/send_email.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/composeApp/src/commonMain/composeResources/values/strings-common.xml b/composeApp/src/commonMain/composeResources/values/strings-common.xml index 3c51645b..4e39f682 100644 --- a/composeApp/src/commonMain/composeResources/values/strings-common.xml +++ b/composeApp/src/commonMain/composeResources/values/strings-common.xml @@ -4,4 +4,32 @@ Dashboard Test Results Settings + + Notifications + Enabled + Interested in running OONI Probe tests during emergent censorship events? Enable notifications to receive a message when we hear of internet censorship near you. + + Test options + + Run tests automatically + Number of automated tests: %1$s. + Last automated test: %1$s. + Only on WiFi + Only while charging + By enabling automatic testing, OONI Probe tests will run automatically multiple times per day. Your test results will automatically get published on OONI Explorer: https://explorer.ooni.org/ \n\nImportant: If you have a VPN enabled, OONI Probe will not run tests automatically. Please turn off your VPN for automated OONI Probe testing. Learn more: https://ooni.org/support/faq/#can-i-run-ooni-probe-over-a-vpn + + Limit test duration + Test duration + Website categories to test + %1$s categories enabled + + Privacy + + Automatically Publish Results + Send crash reports + + OONI backend proxy + Advanced + Send email to support + diff --git a/composeApp/src/commonMain/composeResources/values/untraslatable.xml b/composeApp/src/commonMain/composeResources/values/untraslatable.xml new file mode 100644 index 00000000..f8bf623d --- /dev/null +++ b/composeApp/src/commonMain/composeResources/values/untraslatable.xml @@ -0,0 +1,69 @@ + + notifications + test_options + privacy + proxy + advanced + send_email + about_ooni + + notifications_enabled + notifications_completion + notifications_news + automated_testing_enabled + automated_testing_wifionly + automated_testing_charging + upload_results + debugLogs + storage_usage + send_crash + warn_vpn_in_use + run_http_invalid_request_line + run_http_header_field_manipulation + test_whatsapp + test_telegram + test_facebook_messenger + test_signal + test_psiphon + test_tor + test_riseupvpn + run_ndt + run_dash + experimental + long_running_tests_in_foreground + max_runtime_enabled + max_runtime + + ALDR + REL + PORN + PROV + POLR + HUMR + ENV + MILX + HATE + NEWS + XED + PUBH + GMB + ANON + DATE + GRP + LGBT + FILE + HACK + COMT + MMED + HOST + SRCH + GAME + CULTR + ECON + GOVT + COMM + CTRL + IGO + MISC + + diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/CreateDataStore.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/CreateDataStore.kt new file mode 100644 index 00000000..898edf7b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/CreateDataStore.kt @@ -0,0 +1,34 @@ +package org.ooni.probe + +import androidx.datastore.core.DataMigration +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import kotlinx.atomicfu.locks.SynchronizedObject +import kotlinx.atomicfu.locks.synchronized +import okio.Path.Companion.toPath + +private lateinit var dataStore: DataStore + +private val lock = SynchronizedObject() + +/** + * Gets the singleton DataStore instance, creating it if necessary. + */ +fun getDataStore( + producePath: () -> String, + migrations: List> = listOf(), +): DataStore = + synchronized(lock) { + if (::dataStore.isInitialized) { + dataStore + } else { + PreferenceDataStoreFactory.createWithPath( + produceFile = { producePath().toPath() }, + migrations = migrations, + ) + .also { dataStore = it } + } + } + +internal const val DATA_STORE_FILE_NAME = "probe.preferences_pb" diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/SettingsRepository.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/SettingsRepository.kt new file mode 100644 index 00000000..5a188d3a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/SettingsRepository.kt @@ -0,0 +1,23 @@ +package org.ooni.probe.data + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.map + +class SettingsRepository( + private val dataStore: DataStore, +) { + fun allSettings(keys: List>): Flow> = + dataStore.data.map { + keys.map { key -> key.name to it[key] }.toMap() + } + + suspend fun getValueByKey( + key: Preferences.Key, + defaultValue: T, + ): T { + return dataStore.data.map { it[key] }.firstOrNull() ?: defaultValue + } +} 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 ddabea7a..167a2fbd 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt @@ -1,23 +1,29 @@ package org.ooni.probe.di import androidx.annotation.VisibleForTesting +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences import kotlinx.serialization.json.Json import org.ooni.engine.Engine import org.ooni.engine.NetworkTypeFinder import org.ooni.engine.OonimkallBridge import org.ooni.engine.TaskEventMapper import org.ooni.engine.models.NetworkType +import org.ooni.probe.data.SettingsRepository import org.ooni.probe.data.models.TestResult import org.ooni.probe.shared.PlatformInfo import org.ooni.probe.ui.dashboard.DashboardViewModel import org.ooni.probe.ui.result.ResultViewModel import org.ooni.probe.ui.results.ResultsViewModel +import org.ooni.probe.ui.settings.SettingsViewModel +import org.ooni.probe.ui.settings.category.SettingsCategoryViewModel class Dependencies( val platformInfo: PlatformInfo, private val oonimkallBridge: OonimkallBridge, private val baseFileDir: String, private val cacheDir: String, + private val dataStore: DataStore, ) { // Data @@ -28,6 +34,7 @@ class Dependencies( private val networkTypeFinder by lazy { NetworkTypeFinder { NetworkType.Unknown("") } } // TODO private val taskEventMapper by lazy { TaskEventMapper(networkTypeFinder, json) } private val engine by lazy { Engine(oonimkallBridge, json, baseFileDir, cacheDir, taskEventMapper) } + private val preferenceManager by lazy { SettingsRepository(dataStore) } // ViewModels @@ -35,6 +42,17 @@ class Dependencies( fun resultsViewModel(goToResult: (TestResult.Id) -> Unit) = ResultsViewModel(goToResult) + fun settingsViewModel(goToSettingsForCategory: (String) -> Unit) = SettingsViewModel(goToSettingsForCategory) + + fun settingsCategoryViewModel( + goToSettingsForCategory: (String) -> Unit, + onBack: () -> Unit, + ) = SettingsCategoryViewModel( + preferenceManager = preferenceManager, + onBack = onBack, + goToSettingsForCategory = goToSettingsForCategory, + ) + fun resultViewModel( resultId: TestResult.Id, onBack: () -> Unit, 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 569b0cd8..053b615f 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 @@ -9,12 +9,16 @@ import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable +import ooniprobe.composeapp.generated.resources.Res +import ooniprobe.composeapp.generated.resources.send_email +import org.jetbrains.compose.resources.stringResource import org.ooni.probe.data.models.TestResult import org.ooni.probe.di.Dependencies import org.ooni.probe.ui.dashboard.DashboardScreen import org.ooni.probe.ui.result.ResultScreen import org.ooni.probe.ui.results.ResultsScreen import org.ooni.probe.ui.settings.SettingsScreen +import org.ooni.probe.ui.settings.category.SettingsCategoryScreen @Composable fun Navigation( @@ -44,7 +48,15 @@ fun Navigation( } composable(route = Screen.Settings.route) { - SettingsScreen() + val viewModel = + viewModel { + dependencies.settingsViewModel( + goToSettingsForCategory = { + navController.navigate(Screen.SettingsCategory(it).route) + }, + ) + } + SettingsScreen(viewModel::onEvent) } composable( @@ -62,5 +74,30 @@ fun Navigation( val state by viewModel.state.collectAsState() ResultScreen(state, viewModel::onEvent) } + + composable( + route = Screen.SettingsCategory.NAV_ROUTE, + arguments = Screen.SettingsCategory.ARGUMENTS, + ) { entry -> + val category = entry.arguments?.getString("category") ?: return@composable + when (category) { + stringResource(Res.string.send_email) -> { + // TODO: Implement based on platform + } + + else -> { + val viewModel = + viewModel { + dependencies.settingsCategoryViewModel( + goToSettingsForCategory = { + navController.navigate(Screen.SettingsCategory(it).route) + }, + onBack = { navController.navigateUp() }, + ) + } + SettingsCategoryScreen(category = category, onEvent = viewModel::onEvent) + } + } + } } } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Screen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Screen.kt index 41e94fe4..b1b0c81a 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Screen.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Screen.kt @@ -19,4 +19,11 @@ sealed class Screen( val ARGUMENTS = listOf(navArgument("resultId") { type = NavType.StringType }) } } + + data class SettingsCategory(val category: String) : Screen("settings/$category") { + companion object { + const val NAV_ROUTE = "settings/{category}" + val ARGUMENTS = listOf(navArgument("category") { type = NavType.StringType }) + } + } } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsScreen.kt index d21c93bc..1a6fbf8d 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsScreen.kt @@ -1,30 +1,152 @@ package org.ooni.probe.ui.settings +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.material3.ListItem import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import ooniprobe.composeapp.generated.resources.Res +import ooniprobe.composeapp.generated.resources.Settings_About_Label +import ooniprobe.composeapp.generated.resources.Settings_Advanced_Label +import ooniprobe.composeapp.generated.resources.Settings_Notifications_Label +import ooniprobe.composeapp.generated.resources.Settings_Privacy_Label +import ooniprobe.composeapp.generated.resources.Settings_Proxy_Label +import ooniprobe.composeapp.generated.resources.Settings_SendEmail_Label +import ooniprobe.composeapp.generated.resources.Settings_TestOptions_Label +import ooniprobe.composeapp.generated.resources.about_ooni +import ooniprobe.composeapp.generated.resources.advanced +import ooniprobe.composeapp.generated.resources.ic_settings +import ooniprobe.composeapp.generated.resources.notifications +import ooniprobe.composeapp.generated.resources.ooni_backend_proxy +import ooniprobe.composeapp.generated.resources.outline_info +import ooniprobe.composeapp.generated.resources.privacy +import ooniprobe.composeapp.generated.resources.proxy +import ooniprobe.composeapp.generated.resources.send_email import ooniprobe.composeapp.generated.resources.settings +import ooniprobe.composeapp.generated.resources.test_options +import org.jetbrains.compose.resources.DrawableResource +import org.jetbrains.compose.resources.StringResource +import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource -import org.jetbrains.compose.ui.tooling.preview.Preview -import org.ooni.probe.ui.theme.AppTheme @Composable -fun SettingsScreen() { +fun SettingsScreen(onNavigateToSettingsCategory: (SettingsViewModel.Event) -> Unit) { Column { TopAppBar( title = { Text(stringResource(Res.string.settings)) }, ) + SettingsItem( + icon = Res.drawable.notifications, + title = Res.string.Settings_Notifications_Label, + modifier = + stringResource(Res.string.notifications).let { category -> + Modifier.clickable { + onNavigateToSettingsCategory( + category.routeToSettingsCategory(), + ) + } + }, + ) + SettingsItem( + icon = Res.drawable.ic_settings, + title = Res.string.Settings_TestOptions_Label, + modifier = + stringResource(Res.string.test_options).let { category -> + Modifier.clickable { + onNavigateToSettingsCategory( + category.routeToSettingsCategory(), + ) + } + }, + ) + SettingsItem( + icon = Res.drawable.privacy, + title = Res.string.Settings_Privacy_Label, + modifier = + stringResource(Res.string.privacy).let { category -> + Modifier.clickable { + onNavigateToSettingsCategory( + category.routeToSettingsCategory(), + ) + } + }, + ) + SettingsItem( + icon = Res.drawable.proxy, + title = Res.string.Settings_Proxy_Label, + modifier = + stringResource(Res.string.ooni_backend_proxy).let { category -> + Modifier.clickable { + onNavigateToSettingsCategory( + category.routeToSettingsCategory(), + ) + } + }, + ) + SettingsItem( + icon = Res.drawable.advanced, + title = Res.string.Settings_Advanced_Label, + modifier = + stringResource(Res.string.advanced).let { category -> + Modifier.clickable { + onNavigateToSettingsCategory( + category.routeToSettingsCategory(), + ) + } + }, + ) + SettingsItem( + icon = Res.drawable.send_email, + title = Res.string.Settings_SendEmail_Label, + modifier = + stringResource(Res.string.send_email).let { category -> + Modifier.clickable { + onNavigateToSettingsCategory( + category.routeToSettingsCategory(), + ) + } + }, + ) + SettingsItem( + icon = Res.drawable.outline_info, + title = Res.string.Settings_About_Label, + modifier = + stringResource(Res.string.about_ooni).let { category -> + Modifier.clickable { + onNavigateToSettingsCategory( + category.routeToSettingsCategory(), + ) + } + }, + ) } } -@Preview +fun String.routeToSettingsCategory() = SettingsViewModel.Event.SettingsCategoryClick(this) + @Composable -fun SettingsScreenPreview() { - AppTheme { - SettingsScreen() - } +fun SettingsItem( + icon: DrawableResource, + title: StringResource, + modifier: Modifier, +) { + ListItem( + leadingContent = { + Image( + modifier = Modifier.height(24.dp).width(24.dp), + painter = painterResource(icon), + contentDescription = stringResource(title), + ) + }, + headlineContent = { Text(stringResource(title)) }, + modifier = modifier, + ) } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsViewModel.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsViewModel.kt new file mode 100644 index 00000000..106b4009 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsViewModel.kt @@ -0,0 +1,27 @@ +package org.ooni.probe.ui.settings + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +open class SettingsViewModel( + goToSettingsForCategory: (String) -> Unit, +) : ViewModel() { + private val events = MutableSharedFlow(extraBufferCapacity = 1) + + init { + events.filterIsInstance() + .onEach { goToSettingsForCategory(it.category) }.launchIn(viewModelScope) + } + + fun onEvent(event: Event) { + events.tryEmit(event) + } + + sealed interface Event { + data class SettingsCategoryClick(val category: String) : Event + } +} 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 new file mode 100644 index 00000000..2ea875ca --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryScreen.kt @@ -0,0 +1,286 @@ +package org.ooni.probe.ui.settings.category + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import ooniprobe.composeapp.generated.resources.Modal_EnableNotifications_Paragraph +import ooniprobe.composeapp.generated.resources.Res +import ooniprobe.composeapp.generated.resources.Settings_About_Label +import ooniprobe.composeapp.generated.resources.Settings_Advanced_Label +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_Footer +import ooniprobe.composeapp.generated.resources.Settings_AutomatedTesting_RunAutomatically_WiFiOnly +import ooniprobe.composeapp.generated.resources.Settings_Notifications_Enabled +import ooniprobe.composeapp.generated.resources.Settings_Notifications_Label +import ooniprobe.composeapp.generated.resources.Settings_Privacy_Label +import ooniprobe.composeapp.generated.resources.Settings_Privacy_SendCrashReports +import ooniprobe.composeapp.generated.resources.Settings_Proxy_Label +import ooniprobe.composeapp.generated.resources.Settings_Sharing_UploadResults +import ooniprobe.composeapp.generated.resources.Settings_TestOptions_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_MaxRuntime +import ooniprobe.composeapp.generated.resources.Settings_Websites_MaxRuntimeEnabled +import ooniprobe.composeapp.generated.resources.about_ooni +import ooniprobe.composeapp.generated.resources.advanced +import ooniprobe.composeapp.generated.resources.automated_testing_charging +import ooniprobe.composeapp.generated.resources.automated_testing_enabled +import ooniprobe.composeapp.generated.resources.automated_testing_wifionly +import ooniprobe.composeapp.generated.resources.back +import ooniprobe.composeapp.generated.resources.max_runtime +import ooniprobe.composeapp.generated.resources.max_runtime_enabled +import ooniprobe.composeapp.generated.resources.notifications +import ooniprobe.composeapp.generated.resources.notifications_enabled +import ooniprobe.composeapp.generated.resources.ooni_backend_proxy +import ooniprobe.composeapp.generated.resources.privacy +import ooniprobe.composeapp.generated.resources.send_crash +import ooniprobe.composeapp.generated.resources.test_options +import ooniprobe.composeapp.generated.resources.upload_results +import org.jetbrains.compose.resources.StringResource +import org.jetbrains.compose.resources.stringResource + +@Composable +fun SettingsCategoryScreen( + category: String, + onEvent: (SettingsCategoryViewModel.Event) -> Unit, +) { + val categories = + mapOf( + stringResource(Res.string.notifications) to + SettingsCategory( + title = Res.string.Settings_Notifications_Label, + content = { NotificationsSettingsScreen(onEvent) }, + ), + stringResource(Res.string.test_options) to + SettingsCategory( + title = Res.string.Settings_TestOptions_Label, + content = { TestOptionsSettingsScreen(onEvent) }, + ), + stringResource(Res.string.privacy) to + SettingsCategory( + title = Res.string.Settings_Privacy_Label, + content = { PrivacySettingsScreen(onEvent) }, + ), + stringResource(Res.string.advanced) to + SettingsCategory( + title = Res.string.Settings_Advanced_Label, + content = { return@SettingsCategory }, + ), + stringResource(Res.string.ooni_backend_proxy) to + SettingsCategory( + title = Res.string.Settings_Proxy_Label, + content = { return@SettingsCategory }, + ), + stringResource(Res.string.about_ooni) to + SettingsCategory( + title = Res.string.Settings_About_Label, + content = { return@SettingsCategory }, + ), + ) + + Column { + TopAppBar( + title = { + categories[category]?.let { Text(stringResource(it.title)) } + }, + navigationIcon = { + IconButton(onClick = { onEvent(SettingsCategoryViewModel.Event.BackClicked) }) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(Res.string.back), + ) + } + }, + ) + categories[category]?.content?.invoke() + } +} + +data class SettingsCategory( + val title: StringResource, + val content: @Composable () -> Unit, +) + +@Composable +fun NotificationsSettingsScreen(onEvent: (SettingsCategoryViewModel.Event) -> Unit) { + Column { + SwitchSettings( + title = Res.string.Settings_Notifications_Enabled, + key = stringResource(Res.string.notifications_enabled), + checked = false, + onCheckedChange = { key, value -> + onEvent( + SettingsCategoryViewModel.Event.CheckedChangeClick( + key, + value, + ), + ) + }, + ) + SettingsDescription( + Res.string.Modal_EnableNotifications_Paragraph, + ) + } +} + +@Composable +fun TestOptionsSettingsScreen(onEvent: (SettingsCategoryViewModel.Event) -> Unit) { + Column { + SwitchSettings( + title = Res.string.Settings_AutomatedTesting_RunAutomatically, + key = stringResource(Res.string.automated_testing_enabled), + checked = false, + onCheckedChange = { key, value -> + onEvent( + SettingsCategoryViewModel.Event.CheckedChangeClick( + key, + value, + ), + ) + }, + ) + // TODO: Add dependency on status of automated testing above + SwitchSettings( + title = Res.string.Settings_AutomatedTesting_RunAutomatically_WiFiOnly, + key = stringResource(Res.string.automated_testing_wifionly), + checked = false, + onCheckedChange = { key, value -> + onEvent( + SettingsCategoryViewModel.Event.CheckedChangeClick( + key, + value, + ), + ) + }, + ) + SwitchSettings( + title = Res.string.Settings_AutomatedTesting_RunAutomatically_ChargingOnly, + key = stringResource(Res.string.automated_testing_charging), + checked = false, + onCheckedChange = { key, value -> + onEvent( + SettingsCategoryViewModel.Event.CheckedChangeClick( + key, + value, + ), + ) + }, + ) + // TODO: add proper structure to navigate to the websites categories + SwitchSettings( + title = Res.string.Settings_Websites_Categories_Label, + supportingContent = { + Text(stringResource(Res.string.Settings_Websites_Categories_Description)) + }, + key = stringResource(Res.string.automated_testing_charging), + checked = false, + onCheckedChange = { key, value -> + onEvent( + SettingsCategoryViewModel.Event.CheckedChangeClick( + key, + value, + ), + ) + }, + ) + SwitchSettings( + title = Res.string.Settings_Websites_MaxRuntimeEnabled, + key = stringResource(Res.string.max_runtime_enabled), + checked = false, + onCheckedChange = { key, value -> + onEvent( + SettingsCategoryViewModel.Event.CheckedChangeClick( + key, + value, + ), + ) + }, + ) + // Add proper view to enter and validate value. + SwitchSettings( + title = Res.string.Settings_Websites_MaxRuntime, + key = stringResource(Res.string.max_runtime), + checked = false, + onCheckedChange = { key, value -> + onEvent( + SettingsCategoryViewModel.Event.CheckedChangeClick( + key, + value, + ), + ) + }, + ) + SettingsDescription( + Res.string.Settings_AutomatedTesting_RunAutomatically_Footer, + ) + } +} + +@Composable +fun PrivacySettingsScreen(onEvent: (SettingsCategoryViewModel.Event) -> Unit) { + Column { + SwitchSettings( + title = Res.string.Settings_Sharing_UploadResults, + key = stringResource(Res.string.upload_results), + checked = false, + onCheckedChange = { key, value -> + onEvent( + SettingsCategoryViewModel.Event.CheckedChangeClick( + key, + value, + ), + ) + }, + ) + SwitchSettings( + title = Res.string.Settings_Privacy_SendCrashReports, + key = stringResource(Res.string.send_crash), + checked = false, + onCheckedChange = { key, value -> + onEvent( + SettingsCategoryViewModel.Event.CheckedChangeClick( + key, + value, + ), + ) + }, + ) + } +} + +@Composable +fun SwitchSettings( + title: StringResource, + supportingContent: @Composable (() -> Unit)? = null, + key: String, + checked: Boolean, + onCheckedChange: (String, Boolean) -> Unit, +) { + ListItem( + headlineContent = { Text(stringResource(title)) }, + supportingContent = supportingContent, + trailingContent = { + Switch( + checked = checked, + onCheckedChange = { newValue -> onCheckedChange(key, newValue) }, + ) + }, + ) +} + +@Composable +fun SettingsDescription(description: StringResource) { + Text(stringResource(description), modifier = Modifier.padding(horizontal = 16.dp), fontSize = 12.sp) +} diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryViewModel.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryViewModel.kt new file mode 100644 index 00000000..77b36fda --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryViewModel.kt @@ -0,0 +1,49 @@ +package org.ooni.probe.ui.settings.category + +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn +import org.ooni.probe.data.SettingsRepository + +class SettingsCategoryViewModel( + preferenceManager: SettingsRepository, + goToSettingsForCategory: (String) -> Unit, + onBack: () -> Unit, +) : ViewModel() { + private val events = MutableSharedFlow(extraBufferCapacity = 1) + + init { + events.filterIsInstance() + .onEach { goToSettingsForCategory(it.category) }.launchIn(viewModelScope) + events.filterIsInstance() + .onEach { onBack() }.launchIn(viewModelScope) + } + + fun onEvent(event: Event) { + events.tryEmit(event) + } + + val settings: StateFlow?> = + preferenceManager + .allSettings(listOf(intPreferencesKey("notifications_enabled"))) + .stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5000L), + null, + ) + + sealed interface Event { + data class SettingsCategoryClick(val category: String) : Event + + data class CheckedChangeClick(val key: String, val value: Boolean) : Event + + data object BackClicked : Event + } +} diff --git a/composeApp/src/dwMain/resources/values/strings-organization.xml b/composeApp/src/dwMain/resources/values/strings-organization.xml index 3cbb405a..f29fd870 100644 --- a/composeApp/src/dwMain/resources/values/strings-organization.xml +++ b/composeApp/src/dwMain/resources/values/strings-organization.xml @@ -1,3 +1,5 @@ News Media Scan + + About News Media Scan diff --git a/composeApp/src/iosMain/kotlin/org/ooni/probe/CreateDataStore.kt b/composeApp/src/iosMain/kotlin/org/ooni/probe/CreateDataStore.kt new file mode 100644 index 00000000..c31bd927 --- /dev/null +++ b/composeApp/src/iosMain/kotlin/org/ooni/probe/CreateDataStore.kt @@ -0,0 +1,24 @@ +package org.ooni.probe + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import platform.Foundation.NSDocumentDirectory +import platform.Foundation.NSFileManager +import platform.Foundation.NSURL +import platform.Foundation.NSUserDomainMask + +@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) +fun createDataStore(): DataStore = + getDataStore( + producePath = { + val documentDirectory: NSURL? = + NSFileManager.defaultManager.URLForDirectory( + directory = NSDocumentDirectory, + inDomain = NSUserDomainMask, + appropriateForURL = null, + create = false, + error = null, + ) + requireNotNull(documentDirectory).path + "/$DATA_STORE_FILE_NAME" + }, + ) diff --git a/composeApp/src/iosMain/kotlin/org/ooni/probe/SetupDependencies.kt b/composeApp/src/iosMain/kotlin/org/ooni/probe/SetupDependencies.kt index 8bfafe23..48995a61 100644 --- a/composeApp/src/iosMain/kotlin/org/ooni/probe/SetupDependencies.kt +++ b/composeApp/src/iosMain/kotlin/org/ooni/probe/SetupDependencies.kt @@ -21,6 +21,7 @@ fun setupDependencies(bridge: OonimkallBridge) = oonimkallBridge = bridge, baseFileDir = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true).first().toString(), cacheDir = NSTemporaryDirectory(), + dataStore = createDataStore(), ) private val platformInfo get() = diff --git a/composeApp/src/ooniMain/resources/values/strings-organization.xml b/composeApp/src/ooniMain/resources/values/strings-organization.xml index 96cb7ce4..1ecb84f2 100644 --- a/composeApp/src/ooniMain/resources/values/strings-organization.xml +++ b/composeApp/src/ooniMain/resources/values/strings-organization.xml @@ -1,3 +1,4 @@ OONI Probe + About OONI diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c0479a30..102980ff 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,6 +8,7 @@ android-targetSdk = "34" compose-plugin = "1.6.11" kotlin = "2.0.0" +dataStoreVersion = "1.1.1" [plugins] @@ -25,12 +26,17 @@ ktlint = { id = "org.jlleitschuh.gradle.ktlint", version = "12.1.1" } # Kotlin kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.7.1" } kotlin-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version = "0.6.0" } +kotlinx-atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version = "0.23.2" } # UI android-activity = { module = "androidx.activity:activity-ktx", version = "1.9.1" } lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version = "2.8.0" } navigation = { module = "org.jetbrains.androidx.navigation:navigation-compose", version = "2.7.0-alpha07" } +# Preferences +androidx-datastore-core-okio = { group = "androidx.datastore", name = "datastore-core-okio", version.ref = "dataStoreVersion" } +androidx-datastore-preferences-core = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "dataStoreVersion" } + # Engine android-oonimkall = { module = "org.ooni:oonimkall", version = "2024.05.22-092559" } From f08531138c12ed7c095ba2f3227fd74a763f40f3 Mon Sep 17 00:00:00 2001 From: Norbel AMBANUMBEN Date: Wed, 14 Aug 2024 21:19:08 +0100 Subject: [PATCH 02/12] feat: add tests --- composeApp/build.gradle.kts | 4 - .../org/ooni/probe/AndroidApplication.kt | 13 +- .../org/ooni/probe/CreateDataStore.android.kt | 12 -- .../ooni/testing/CreatePreferenceDataStore.kt | 15 ++ .../composeResources/drawable/.gitignore | 7 +- .../org/ooni/probe/data/SettingsRepository.kt | 23 --- .../data/repositories/PreferenceRepository.kt | 140 ++++++++++++++++++ .../kotlin/org/ooni/probe/di/Dependencies.kt | 12 +- .../ooni/probe/ui/navigation/Navigation.kt | 2 +- .../category/SettingsCategoryViewModel.kt | 8 +- .../repositories/PreferenceRepositoryTest.kt | 108 ++++++++++++++ .../ooni/testing/CreatePreferenceDataStore.kt | 6 + .../org/ooni/probe/SetupDependencies.kt | 24 ++- .../testing/CreatePreferenceDataStore.kt} | 9 +- gradle/libs.versions.toml | 3 + 15 files changed, 328 insertions(+), 58 deletions(-) delete mode 100644 composeApp/src/androidMain/kotlin/org/ooni/probe/CreateDataStore.android.kt create mode 100644 composeApp/src/androidUnitTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt delete mode 100644 composeApp/src/commonMain/kotlin/org/ooni/probe/data/SettingsRepository.kt create mode 100644 composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/PreferenceRepository.kt create mode 100644 composeApp/src/commonTest/kotlin/org/ooni/probe/data/repositories/PreferenceRepositoryTest.kt create mode 100644 composeApp/src/commonTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt rename composeApp/src/{iosMain/kotlin/org/ooni/probe/CreateDataStore.kt => iosTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt} (79%) diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 2b29e669..4f4025bb 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -85,10 +85,6 @@ kotlin { implementation(libs.bundles.ui) implementation(libs.bundles.tooling) - implementation(libs.androidx.datastore.preferences.core) - implementation(libs.androidx.datastore.core.okio) - implementation(libs.kotlinx.atomicfu) - getByName("commonMain") { kotlin.srcDir(config.srcRoot) } diff --git a/composeApp/src/androidMain/kotlin/org/ooni/probe/AndroidApplication.kt b/composeApp/src/androidMain/kotlin/org/ooni/probe/AndroidApplication.kt index af7483b7..1e5f9e0c 100644 --- a/composeApp/src/androidMain/kotlin/org/ooni/probe/AndroidApplication.kt +++ b/composeApp/src/androidMain/kotlin/org/ooni/probe/AndroidApplication.kt @@ -3,6 +3,9 @@ package org.ooni.probe import android.app.Application import android.net.ConnectivityManager import android.os.Build +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.SharedPreferencesMigration +import androidx.datastore.preferences.core.Preferences import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.driver.android.AndroidSqliteDriver import org.ooni.engine.AndroidNetworkTypeFinder @@ -26,8 +29,8 @@ class AndroidApplication : Application() { databaseDriverFactory = ::buildDatabaseDriver, networkTypeFinder = AndroidNetworkTypeFinder(getSystemService(ConnectivityManager::class.java)), - dataStore = getDataStore(this), - ) + buildDataStore = ::buildDataStore, + ) } private val platformInfo by lazy { @@ -42,4 +45,10 @@ class AndroidApplication : Application() { private fun buildDatabaseDriver(): SqlDriver = AndroidSqliteDriver(Database.Schema, this, "v2") private fun readAssetFile(path: String) = assets.open(path).bufferedReader().use { it.readText() } + + private fun buildDataStore(): DataStore = + getDataStore( + producePath = { this.filesDir.resolve(DATA_STORE_FILE_NAME).absolutePath }, + migrations = listOf(SharedPreferencesMigration(this, "notifications_enabled")), + ) } diff --git a/composeApp/src/androidMain/kotlin/org/ooni/probe/CreateDataStore.android.kt b/composeApp/src/androidMain/kotlin/org/ooni/probe/CreateDataStore.android.kt deleted file mode 100644 index 63a26e90..00000000 --- a/composeApp/src/androidMain/kotlin/org/ooni/probe/CreateDataStore.android.kt +++ /dev/null @@ -1,12 +0,0 @@ -package org.ooni.probe - -import android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.SharedPreferencesMigration -import androidx.datastore.preferences.core.Preferences - -fun getDataStore(context: Context): DataStore = - getDataStore( - producePath = { context.filesDir.resolve(DATA_STORE_FILE_NAME).absolutePath }, - migrations = listOf(SharedPreferencesMigration(context, "notifications_enabled")), - ) diff --git a/composeApp/src/androidUnitTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt b/composeApp/src/androidUnitTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt new file mode 100644 index 00000000..900b756b --- /dev/null +++ b/composeApp/src/androidUnitTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt @@ -0,0 +1,15 @@ +package org.ooni.testing + +import android.app.Application +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.test.core.app.ApplicationProvider +import org.ooni.probe.DATA_STORE_FILE_NAME +import org.ooni.probe.getDataStore + +internal actual fun createPreferenceDataStore(): DataStore { + val app = ApplicationProvider.getApplicationContext() + return getDataStore( + producePath = { app.filesDir.resolve(DATA_STORE_FILE_NAME).absolutePath }, + ) +} diff --git a/composeApp/src/commonMain/composeResources/drawable/.gitignore b/composeApp/src/commonMain/composeResources/drawable/.gitignore index 4d576f73..f9d23428 100644 --- a/composeApp/src/commonMain/composeResources/drawable/.gitignore +++ b/composeApp/src/commonMain/composeResources/drawable/.gitignore @@ -1,2 +1,7 @@ -logo.xml \ No newline at end of file +logo.xml +test_experimental.xml +test_performance.xml +test_instant_messaging.xml +test_websites.xml +test_circumvention.xml \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/SettingsRepository.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/SettingsRepository.kt deleted file mode 100644 index 5a188d3a..00000000 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/SettingsRepository.kt +++ /dev/null @@ -1,23 +0,0 @@ -package org.ooni.probe.data - -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.map - -class SettingsRepository( - private val dataStore: DataStore, -) { - fun allSettings(keys: List>): Flow> = - dataStore.data.map { - keys.map { key -> key.name to it[key] }.toMap() - } - - suspend fun getValueByKey( - key: Preferences.Key, - defaultValue: T, - ): T { - return dataStore.data.map { it[key] }.firstOrNull() ?: defaultValue - } -} diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/PreferenceRepository.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/PreferenceRepository.kt new file mode 100644 index 00000000..95ed0407 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/PreferenceRepository.kt @@ -0,0 +1,140 @@ +package org.ooni.probe.data.repositories + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.map + +class PreferenceRepository( + private val dataStore: DataStore, +) { + /** + * This function is used to resolve the preference key for a given test. + * The preference key is the name of the test prefixed with the [prefix] of the descriptor + * and suffixed with "_autorun" if [autoRun] is true. + * + * Example: the preference key for the test "web_connectivity" in the + * descriptor "websites" is "websites_web_connectivity". + * If [autoRun] is true, the preference key is "websites_web_connectivity_autorun". + * + * @param name The name of the preference + * @param prefix The prefix of the preference + * @param autoRun If the preference is for auto run + * @return The preference key + */ + fun getPreferenceKey( + name: String, + prefix: String? = null, + autoRun: Boolean = false, + ): String { + return "${prefix?.let { "${it}_" } ?: ""}$name${if (autoRun) "_autorun" else ""}" + } + + fun allSettings(keys: List>): Flow> = + dataStore.data.map { + keys.map { key -> key.name to it[key] }.toMap() + } + + fun getValueByKey(key: Preferences.Key): Flow { + return dataStore.data.map { it[key] } + } + + suspend fun setValueByKey( + key: Preferences.Key, + value: T, + ) { + dataStore.edit { it[key] = value } + } + + suspend fun clear() { + dataStore.edit { it.clear() } + } + + suspend fun remove(key: Preferences.Key<*>) { + dataStore.edit { it.remove(key) } + } + + suspend fun contains(key: Preferences.Key<*>): Boolean { + return dataStore.data.map { it.contains(key) }.firstOrNull() ?: false + } +} + +enum class SettingsKey(val value: String) { + // Notifications + NOTIFICATIONS_ENABLED("notifications_enabled"), + + // Test Options + AUTOMATED_TESTING_ENABLED("automated_testing_enabled"), + AUTOMATED_TESTING_WIFIONLY("automated_testing_wifionly"), + AUTOMATED_TESTING_CHARGING("automated_testing_charging"), + MAX_RUNTIME_ENABLED("max_runtime_enabled"), + MAX_RUNTIME("max_runtime"), + + // Website categories + SRCH("SRCH"), + PORN("PORN"), + COMM("COMM"), + COMT("COMT"), + MMED("MMED"), + HATE("HATE"), + POLR("POLR"), + PUBH("PUBH"), + GAME("GAME"), + PROV("PROV"), + HACK("HACK"), + MILX("MILX"), + DATE("DATE"), + ANON("ANON"), + ALDR("ALDR"), + GMB("GMB"), + XED("XED"), + REL("REL"), + GRP("GRP"), + GOVT("GOVT"), + ECON("ECON"), + LGBT("LGBT"), + FILE("FILE"), + HOST("HOST"), + HUMR("HUMR"), + NEWS("NEWS"), + ENV("ENV"), + CULTR("CULTR"), + CTRL("CTRL"), + IGO("IGO"), + + // Privacy + UPLOAD_RESULTS("upload_results"), + SEND_CRASH("send_crash"), + + // Proxy + PROXY_HOSTNAME("proxy_hostname"), + PROXY_PORT("proxy_port"), + + // Advanced + THEME_ENABLED("theme_enabled"), + LANGUAGE_SETTING("language_setting"), + DEBUG_LOGS("debugLogs"), + WARN_VPN_IN_USE("warn_vpn_in_use"), + + // MISC + DELETE_UPLOADED_JSONS("deleteUploadedJsons"), + IS_NOTIFICATION_DIALOG("isNotificationDialog"), + FIRST_RUN("first_run"), + + // Run Tests + TEST_SIGNAL("test_signal"), + RUN_HTTP_INVALID_REQUEST_LINE("run_http_invalid_request_line"), + TEST_FACEBOOK_MESSENGER("test_facebook_messenger"), + RUN_DASH("run_dash"), + WEB_CONNECTIVITY("web_connectivity"), + RUN_NDT("run_ndt"), + TEST_PSIPHON("test_psiphon"), + TEST_TOR("test_tor"), + PROXY_PROTOCOL("proxy_protocol"), + TEST_TELEGRAM("test_telegram"), + RUN_HTTP_HEADER_FIELD_MANIPULATION("run_http_header_field_manipulation"), + EXPERIMENTAL("experimental"), + TEST_WHATSAPP("test_whatsapp"), +} 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 f1954ddc..b9141ddb 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt @@ -1,9 +1,9 @@ package org.ooni.probe.di import androidx.annotation.VisibleForTesting -import app.cash.sqldelight.db.SqlDriver import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences +import app.cash.sqldelight.db.SqlDriver import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import kotlinx.serialization.json.Json @@ -13,6 +13,7 @@ import org.ooni.engine.OonimkallBridge import org.ooni.engine.TaskEventMapper import org.ooni.probe.Database import org.ooni.probe.data.models.ResultModel +import org.ooni.probe.data.repositories.PreferenceRepository import org.ooni.probe.data.repositories.ResultRepository import org.ooni.probe.data.repositories.TestDescriptorRepository import org.ooni.probe.domain.BootstrapTestDescriptors @@ -21,8 +22,6 @@ import org.ooni.probe.domain.GetDefaultTestDescriptors import org.ooni.probe.domain.GetResult import org.ooni.probe.domain.GetResults import org.ooni.probe.domain.GetTestDescriptors -import org.ooni.engine.models.NetworkType -import org.ooni.probe.data.SettingsRepository import org.ooni.probe.shared.PlatformInfo import org.ooni.probe.ui.dashboard.DashboardViewModel import org.ooni.probe.ui.result.ResultViewModel @@ -38,8 +37,8 @@ class Dependencies( private val readAssetFile: (String) -> String, private val databaseDriverFactory: () -> SqlDriver, private val networkTypeFinder: NetworkTypeFinder, - private val dataStore: DataStore, - ) { + private val buildDataStore: () -> DataStore, +) { // Common private val backgroundDispatcher = Dispatchers.IO @@ -90,8 +89,7 @@ class Dependencies( listInstalledTestDescriptors = testDescriptorRepository::list, ) } - private val preferenceManager by lazy { SettingsRepository(dataStore) } - + private val preferenceManager by lazy { PreferenceRepository(buildDataStore()) } // ViewModels 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 29fd98f1..712476a7 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 @@ -9,10 +9,10 @@ import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable -import org.ooni.probe.data.models.ResultModel import ooniprobe.composeapp.generated.resources.Res import ooniprobe.composeapp.generated.resources.send_email import org.jetbrains.compose.resources.stringResource +import org.ooni.probe.data.models.ResultModel import org.ooni.probe.di.Dependencies import org.ooni.probe.ui.dashboard.DashboardScreen import org.ooni.probe.ui.result.ResultScreen diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryViewModel.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryViewModel.kt index 77b36fda..34951614 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryViewModel.kt @@ -1,5 +1,6 @@ package org.ooni.probe.ui.settings.category +import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.intPreferencesKey import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -10,10 +11,11 @@ import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn -import org.ooni.probe.data.SettingsRepository +import org.ooni.probe.data.repositories.PreferenceRepository +import org.ooni.probe.data.repositories.SettingsKey class SettingsCategoryViewModel( - preferenceManager: SettingsRepository, + preferenceManager: PreferenceRepository, goToSettingsForCategory: (String) -> Unit, onBack: () -> Unit, ) : ViewModel() { @@ -32,7 +34,7 @@ class SettingsCategoryViewModel( val settings: StateFlow?> = preferenceManager - .allSettings(listOf(intPreferencesKey("notifications_enabled"))) + .allSettings(listOf(booleanPreferencesKey(SettingsKey.NOTIFICATIONS_ENABLED.value))) .stateIn( viewModelScope, SharingStarted.WhileSubscribed(5000L), diff --git a/composeApp/src/commonTest/kotlin/org/ooni/probe/data/repositories/PreferenceRepositoryTest.kt b/composeApp/src/commonTest/kotlin/org/ooni/probe/data/repositories/PreferenceRepositoryTest.kt new file mode 100644 index 00000000..4d7321d0 --- /dev/null +++ b/composeApp/src/commonTest/kotlin/org/ooni/probe/data/repositories/PreferenceRepositoryTest.kt @@ -0,0 +1,108 @@ +package org.ooni.probe.data.repositories + +import androidx.datastore.preferences.core.stringPreferencesKey +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.ooni.testing.createPreferenceDataStore +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class PreferenceRepositoryTest { + private lateinit var preferenceRepository: PreferenceRepository + + @BeforeTest + fun before() { + preferenceRepository = PreferenceRepository(createPreferenceDataStore()) + } + + @AfterTest + fun after() = runBlocking { + runTest { + preferenceRepository.clear() + } + } + + @Test + fun testAllSettings() = runBlocking { + runTest { + val key = stringPreferencesKey(SettingsKey.LANGUAGE_SETTING.value) + val value = "value" + preferenceRepository.setValueByKey(key, value) + val setting: Map = preferenceRepository.allSettings(listOf(key)).first() + assertEquals(value, setting.values.first()) + } + } + + @Test + fun testGetPreferenceKey() { + assertEquals( + SettingsKey.LANGUAGE_SETTING.value, + preferenceRepository.getPreferenceKey(SettingsKey.LANGUAGE_SETTING.value) + ) + assertEquals( + "prefix_${SettingsKey.LANGUAGE_SETTING.value}", + preferenceRepository.getPreferenceKey(SettingsKey.LANGUAGE_SETTING.value, "prefix"), + ) + assertEquals( + "${SettingsKey.LANGUAGE_SETTING.value}_autorun", + preferenceRepository.getPreferenceKey( + SettingsKey.LANGUAGE_SETTING.value, autoRun = true + ), + ) + assertEquals( + "prefix_${SettingsKey.LANGUAGE_SETTING.value}_autorun", + preferenceRepository.getPreferenceKey( + SettingsKey.LANGUAGE_SETTING.value, "prefix", true + ), + ) + } + + @Test + fun testGetValueByKey() = runTest { + val key = stringPreferencesKey(SettingsKey.LANGUAGE_SETTING.value) + val value = "value" + preferenceRepository.setValueByKey(key, value) + assertEquals(value, preferenceRepository.getValueByKey(key = key).first()) + } + + + @Test + fun testSetValueByKey() = runTest { + val key = stringPreferencesKey(SettingsKey.LANGUAGE_SETTING.value) + val value = "value" + preferenceRepository.setValueByKey(key, value) + assertEquals(value, preferenceRepository.getValueByKey(key).first()) + } + + @Test + fun testClear() = runTest { + val key = stringPreferencesKey(SettingsKey.LANGUAGE_SETTING.value) + val value = "value" + preferenceRepository.setValueByKey(key, value) + preferenceRepository.clear() + assertNull(preferenceRepository.getValueByKey(key).first()) + } + + @Test + fun testRemove() = runTest { + val key = stringPreferencesKey(SettingsKey.LANGUAGE_SETTING.value) + val value = "value" + preferenceRepository.setValueByKey(key, value) + preferenceRepository.remove(key) + assertNull(preferenceRepository.getValueByKey(key).first()) + } + + @Test + fun testContains() = runBlocking { + runTest { + val key = stringPreferencesKey(SettingsKey.LANGUAGE_SETTING.value) + val value = "value" + preferenceRepository.setValueByKey(key, value) + assertEquals(true, preferenceRepository.contains(key)) + } + } +} diff --git a/composeApp/src/commonTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt b/composeApp/src/commonTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt new file mode 100644 index 00000000..6b6baa1c --- /dev/null +++ b/composeApp/src/commonTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt @@ -0,0 +1,6 @@ +package org.ooni.testing + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences + +internal expect fun createPreferenceDataStore(): DataStore diff --git a/composeApp/src/iosMain/kotlin/org/ooni/probe/SetupDependencies.kt b/composeApp/src/iosMain/kotlin/org/ooni/probe/SetupDependencies.kt index c6ded15a..b0dfb065 100644 --- a/composeApp/src/iosMain/kotlin/org/ooni/probe/SetupDependencies.kt +++ b/composeApp/src/iosMain/kotlin/org/ooni/probe/SetupDependencies.kt @@ -1,5 +1,7 @@ package org.ooni.probe +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences import app.cash.sqldelight.driver.native.NativeSqliteDriver import org.ooni.engine.NetworkTypeFinder import org.ooni.engine.OonimkallBridge @@ -8,9 +10,11 @@ import org.ooni.probe.shared.Platform import org.ooni.probe.shared.PlatformInfo import platform.Foundation.NSBundle import platform.Foundation.NSDocumentDirectory +import platform.Foundation.NSFileManager import platform.Foundation.NSSearchPathForDirectoriesInDomains import platform.Foundation.NSString import platform.Foundation.NSTemporaryDirectory +import platform.Foundation.NSURL import platform.Foundation.NSUserDomainMask import platform.Foundation.stringWithContentsOfFile import platform.UIKit.UIDevice @@ -37,8 +41,8 @@ fun setupDependencies( readAssetFile = ::readAssetFile, databaseDriverFactory = ::buildDatabaseDriver, networkTypeFinder = networkTypeFinder, - dataStore = createDataStore(), - ) + buildDataStore = ::buildDataStore, +) private val platformInfo get() = @@ -80,3 +84,19 @@ private fun readAssetFile(path: String): String { private class BundleMarker : NSObject() { companion object : NSObjectMeta() } + +@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) +fun buildDataStore(): DataStore = + getDataStore( + producePath = { + val documentDirectory: NSURL? = + NSFileManager.defaultManager.URLForDirectory( + directory = NSDocumentDirectory, + inDomain = NSUserDomainMask, + appropriateForURL = null, + create = false, + error = null, + ) + requireNotNull(documentDirectory).path + "/$DATA_STORE_FILE_NAME" + }, + ) diff --git a/composeApp/src/iosMain/kotlin/org/ooni/probe/CreateDataStore.kt b/composeApp/src/iosTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt similarity index 79% rename from composeApp/src/iosMain/kotlin/org/ooni/probe/CreateDataStore.kt rename to composeApp/src/iosTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt index c31bd927..5b17da7b 100644 --- a/composeApp/src/iosMain/kotlin/org/ooni/probe/CreateDataStore.kt +++ b/composeApp/src/iosTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt @@ -1,15 +1,17 @@ -package org.ooni.probe +package org.ooni.testing import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences +import org.ooni.probe.DATA_STORE_FILE_NAME +import org.ooni.probe.getDataStore import platform.Foundation.NSDocumentDirectory import platform.Foundation.NSFileManager import platform.Foundation.NSURL import platform.Foundation.NSUserDomainMask @OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) -fun createDataStore(): DataStore = - getDataStore( +internal actual fun createPreferenceDataStore(): DataStore { + return getDataStore( producePath = { val documentDirectory: NSURL? = NSFileManager.defaultManager.URLForDirectory( @@ -22,3 +24,4 @@ fun createDataStore(): DataStore = requireNotNull(documentDirectory).path + "/$DATA_STORE_FILE_NAME" }, ) +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b2331d0b..f6a25e27 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -66,6 +66,9 @@ ui = [ tooling = [ "kermit", "sqldelight-coroutines", + "kotlinx-atomicfu", + "androidx-datastore-core-okio", + "androidx-datastore-preferences-core", ] android-test = [ "android-test-core" From cef7b32794311fcd2899833620389ee17d81bc88 Mon Sep 17 00:00:00 2001 From: Norbel AMBANUMBEN Date: Fri, 16 Aug 2024 14:36:17 +0100 Subject: [PATCH 03/12] bootstrap settings --- .../composeResources/drawable/.gitignore | 2 +- .../drawable/category_aldr.xml | 25 + .../drawable/category_anon.xml | 35 + .../drawable/category_comm.xml | 15 + .../drawable/category_comt.xml | 20 + .../drawable/category_ctrl.xml | 55 ++ .../drawable/category_cultr.xml | 15 + .../drawable/category_date.xml | 15 + .../drawable/category_econ.xml | 30 + .../drawable/category_env.xml | 20 + .../drawable/category_file.xml | 20 + .../drawable/category_game.xml | 15 + .../drawable/category_gmb.xml | 28 + .../drawable/category_govt.xml | 25 + .../drawable/category_grp.xml | 25 + .../drawable/category_hack.xml | 15 + .../drawable/category_hate.xml | 20 + .../drawable/category_host.xml | 15 + .../drawable/category_humr.xml | 15 + .../drawable/category_igo.xml | 25 + .../drawable/category_lgbt.xml | 15 + .../drawable/category_milx.xml | 15 + .../drawable/category_misc.xml | 15 + .../drawable/category_mmed.xml | 25 + .../drawable/category_news.xml | 35 + .../drawable/category_polr.xml | 15 + .../drawable/category_porn.xml | 35 + .../drawable/category_prov.xml | 15 + .../drawable/category_pubh.xml | 20 + .../drawable/category_rel.xml | 20 + .../drawable/category_srch.xml | 20 + .../drawable/category_xed.xml | 20 + .../values/strings-common.xml | 69 ++ .../composeResources/values/untraslatable.xml | 69 -- .../data/repositories/PreferenceRepository.kt | 14 + .../kotlin/org/ooni/probe/di/Dependencies.kt | 8 +- .../ooni/probe/ui/navigation/Navigation.kt | 18 +- .../org/ooni/probe/ui/navigation/Screen.kt | 3 +- .../ooni/probe/ui/settings/SettingsScreen.kt | 637 +++++++++++++++--- .../probe/ui/settings/SettingsViewModel.kt | 5 +- .../category/SettingsCategoryScreen.kt | 333 ++++----- .../category/SettingsCategoryViewModel.kt | 52 +- .../repositories/PreferenceRepositoryTest.kt | 103 +-- 43 files changed, 1540 insertions(+), 456 deletions(-) create mode 100644 composeApp/src/commonMain/composeResources/drawable/category_aldr.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/category_anon.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/category_comm.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/category_comt.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/category_ctrl.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/category_cultr.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/category_date.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/category_econ.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/category_env.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/category_file.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/category_game.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/category_gmb.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/category_govt.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/category_grp.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/category_hack.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/category_hate.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/category_host.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/category_humr.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/category_igo.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/category_lgbt.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/category_milx.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/category_misc.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/category_mmed.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/category_news.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/category_polr.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/category_porn.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/category_prov.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/category_pubh.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/category_rel.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/category_srch.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/category_xed.xml delete mode 100644 composeApp/src/commonMain/composeResources/values/untraslatable.xml diff --git a/composeApp/src/commonMain/composeResources/drawable/.gitignore b/composeApp/src/commonMain/composeResources/drawable/.gitignore index f9d23428..15c3e694 100644 --- a/composeApp/src/commonMain/composeResources/drawable/.gitignore +++ b/composeApp/src/commonMain/composeResources/drawable/.gitignore @@ -1,7 +1,7 @@ -logo.xml test_experimental.xml test_performance.xml test_instant_messaging.xml test_websites.xml +logo.xml test_circumvention.xml \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/drawable/category_aldr.xml b/composeApp/src/commonMain/composeResources/drawable/category_aldr.xml new file mode 100644 index 00000000..4adffcd8 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/category_aldr.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/category_anon.xml b/composeApp/src/commonMain/composeResources/drawable/category_anon.xml new file mode 100644 index 00000000..da39c354 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/category_anon.xml @@ -0,0 +1,35 @@ + + + + + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/category_comm.xml b/composeApp/src/commonMain/composeResources/drawable/category_comm.xml new file mode 100644 index 00000000..83be7c22 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/category_comm.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/category_comt.xml b/composeApp/src/commonMain/composeResources/drawable/category_comt.xml new file mode 100644 index 00000000..36114747 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/category_comt.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/category_ctrl.xml b/composeApp/src/commonMain/composeResources/drawable/category_ctrl.xml new file mode 100644 index 00000000..185d2c48 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/category_ctrl.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/category_cultr.xml b/composeApp/src/commonMain/composeResources/drawable/category_cultr.xml new file mode 100644 index 00000000..1c582a44 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/category_cultr.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/category_date.xml b/composeApp/src/commonMain/composeResources/drawable/category_date.xml new file mode 100644 index 00000000..b75aec46 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/category_date.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/category_econ.xml b/composeApp/src/commonMain/composeResources/drawable/category_econ.xml new file mode 100644 index 00000000..ce617743 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/category_econ.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/category_env.xml b/composeApp/src/commonMain/composeResources/drawable/category_env.xml new file mode 100644 index 00000000..eef29942 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/category_env.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/category_file.xml b/composeApp/src/commonMain/composeResources/drawable/category_file.xml new file mode 100644 index 00000000..ce7ca471 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/category_file.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/category_game.xml b/composeApp/src/commonMain/composeResources/drawable/category_game.xml new file mode 100644 index 00000000..03409645 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/category_game.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/category_gmb.xml b/composeApp/src/commonMain/composeResources/drawable/category_gmb.xml new file mode 100644 index 00000000..24402937 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/category_gmb.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/category_govt.xml b/composeApp/src/commonMain/composeResources/drawable/category_govt.xml new file mode 100644 index 00000000..7e99f99a --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/category_govt.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/category_grp.xml b/composeApp/src/commonMain/composeResources/drawable/category_grp.xml new file mode 100644 index 00000000..a3276245 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/category_grp.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/category_hack.xml b/composeApp/src/commonMain/composeResources/drawable/category_hack.xml new file mode 100644 index 00000000..1ce1c6a7 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/category_hack.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/category_hate.xml b/composeApp/src/commonMain/composeResources/drawable/category_hate.xml new file mode 100644 index 00000000..57c370c0 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/category_hate.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/category_host.xml b/composeApp/src/commonMain/composeResources/drawable/category_host.xml new file mode 100644 index 00000000..638b1e56 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/category_host.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/category_humr.xml b/composeApp/src/commonMain/composeResources/drawable/category_humr.xml new file mode 100644 index 00000000..3497c37a --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/category_humr.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/category_igo.xml b/composeApp/src/commonMain/composeResources/drawable/category_igo.xml new file mode 100644 index 00000000..b2bd3d9f --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/category_igo.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/category_lgbt.xml b/composeApp/src/commonMain/composeResources/drawable/category_lgbt.xml new file mode 100644 index 00000000..890ad7cc --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/category_lgbt.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/category_milx.xml b/composeApp/src/commonMain/composeResources/drawable/category_milx.xml new file mode 100644 index 00000000..18d41657 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/category_milx.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/category_misc.xml b/composeApp/src/commonMain/composeResources/drawable/category_misc.xml new file mode 100644 index 00000000..11d3dd82 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/category_misc.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/category_mmed.xml b/composeApp/src/commonMain/composeResources/drawable/category_mmed.xml new file mode 100644 index 00000000..078222f1 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/category_mmed.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/category_news.xml b/composeApp/src/commonMain/composeResources/drawable/category_news.xml new file mode 100644 index 00000000..8da1b492 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/category_news.xml @@ -0,0 +1,35 @@ + + + + + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/category_polr.xml b/composeApp/src/commonMain/composeResources/drawable/category_polr.xml new file mode 100644 index 00000000..ea78fddd --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/category_polr.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/category_porn.xml b/composeApp/src/commonMain/composeResources/drawable/category_porn.xml new file mode 100644 index 00000000..486cf705 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/category_porn.xml @@ -0,0 +1,35 @@ + + + + + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/category_prov.xml b/composeApp/src/commonMain/composeResources/drawable/category_prov.xml new file mode 100644 index 00000000..6a2063f7 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/category_prov.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/category_pubh.xml b/composeApp/src/commonMain/composeResources/drawable/category_pubh.xml new file mode 100644 index 00000000..978227ac --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/category_pubh.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/category_rel.xml b/composeApp/src/commonMain/composeResources/drawable/category_rel.xml new file mode 100644 index 00000000..fb82c260 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/category_rel.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/category_srch.xml b/composeApp/src/commonMain/composeResources/drawable/category_srch.xml new file mode 100644 index 00000000..3b995649 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/category_srch.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/category_xed.xml b/composeApp/src/commonMain/composeResources/drawable/category_xed.xml new file mode 100644 index 00000000..95f53650 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/category_xed.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/composeApp/src/commonMain/composeResources/values/strings-common.xml b/composeApp/src/commonMain/composeResources/values/strings-common.xml index 9280e225..0436d649 100644 --- a/composeApp/src/commonMain/composeResources/values/strings-common.xml +++ b/composeApp/src/commonMain/composeResources/values/strings-common.xml @@ -39,4 +39,73 @@ Advanced Send email to support + Debug logs + See recent logs + Language Setting + Storage usage + Warn when VPN is in use + + Drugs & Alcohol + Religion + Pornography + Provocative Attire + Political Criticism + Human Rights Issues + Environment + Terrorism and Militants + Hate Speech + News Media + Sex Education + Public Health + Gambling + Circumvention tools + Online Dating + Social Networking + LGBTQ+ + File-sharing + Hacking Tools + Communication Tools + Media sharing + Hosting and Blogging + Search Engines + Gaming + Culture + Economics + Government + E-commerce + Control content + Intergovernmental Orgs. + Miscellaneous content + Use and sale of drugs and alcohol + Religious issues, both supportive and critical + Hard-core and soft-core pornography + Provocative attire and portrayal of women wearing minimal clothing + Critical political viewpoints + Human rights issues + Discussions on environmental issues + Terrorism, violent militant or separatist movements + Disparaging of particular groups based on race, sex, sexuality or other characteristics + Major news websites, regional news outlets and independent media + Sexual health issues including contraception, STD\'s, rape prevention and abortion + Public health issues, such as COVID-19, HIV/AIDS, Ebola + Online gambling and betting + Anonymization, censorship circumvention and encryption + Online dating sites + Online social networking tools and platforms + LGBTQ+ communities discussing related issues (excluding pornography) + File sharing including cloud-based file storage, torrents and P2P + Computer security tools and news + Individual and group communication tools including VoIP, messaging and webmail + Video, audio and photo sharing + Web hosting, blogging and other online publishing + Search engines and portals + Online games and gaming platforms (excluding gambling sites) + Entertainment including history, literature, music, film, satire and humour + General economic development and poverty + Government-run websites, including military + Commercial services and products + Benign or innocuous content used for control + Intergovernmental organizations including The United Nations + Sites that haven\'t been categorized yet + diff --git a/composeApp/src/commonMain/composeResources/values/untraslatable.xml b/composeApp/src/commonMain/composeResources/values/untraslatable.xml deleted file mode 100644 index f8bf623d..00000000 --- a/composeApp/src/commonMain/composeResources/values/untraslatable.xml +++ /dev/null @@ -1,69 +0,0 @@ - - notifications - test_options - privacy - proxy - advanced - send_email - about_ooni - - notifications_enabled - notifications_completion - notifications_news - automated_testing_enabled - automated_testing_wifionly - automated_testing_charging - upload_results - debugLogs - storage_usage - send_crash - warn_vpn_in_use - run_http_invalid_request_line - run_http_header_field_manipulation - test_whatsapp - test_telegram - test_facebook_messenger - test_signal - test_psiphon - test_tor - test_riseupvpn - run_ndt - run_dash - experimental - long_running_tests_in_foreground - max_runtime_enabled - max_runtime - - ALDR - REL - PORN - PROV - POLR - HUMR - ENV - MILX - HATE - NEWS - XED - PUBH - GMB - ANON - DATE - GRP - LGBT - FILE - HACK - COMT - MMED - HOST - SRCH - GAME - CULTR - ECON - GOVT - COMM - CTRL - IGO - MISC - - diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/PreferenceRepository.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/PreferenceRepository.kt index 95ed0407..1549e4f5 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/PreferenceRepository.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/PreferenceRepository.kt @@ -61,6 +61,19 @@ class PreferenceRepository( } } +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"), +} + enum class SettingsKey(val value: String) { // Notifications NOTIFICATIONS_ENABLED("notifications_enabled"), @@ -117,6 +130,7 @@ enum class SettingsKey(val value: String) { LANGUAGE_SETTING("language_setting"), DEBUG_LOGS("debugLogs"), WARN_VPN_IN_USE("warn_vpn_in_use"), + STORAGE_SIZE("storage_size"), // purely decorative // MISC DELETE_UPLOADED_JSONS("deleteUploadedJsons"), 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 b9141ddb..95d71373 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt @@ -13,6 +13,7 @@ import org.ooni.engine.OonimkallBridge import org.ooni.engine.TaskEventMapper import org.ooni.probe.Database import org.ooni.probe.data.models.ResultModel +import org.ooni.probe.data.repositories.PreferenceCategoryKey import org.ooni.probe.data.repositories.PreferenceRepository import org.ooni.probe.data.repositories.ResultRepository import org.ooni.probe.data.repositories.TestDescriptorRepository @@ -26,6 +27,7 @@ import org.ooni.probe.shared.PlatformInfo import org.ooni.probe.ui.dashboard.DashboardViewModel import org.ooni.probe.ui.result.ResultViewModel import org.ooni.probe.ui.results.ResultsViewModel +import org.ooni.probe.ui.settings.SettingsCategoryItem import org.ooni.probe.ui.settings.SettingsViewModel import org.ooni.probe.ui.settings.category.SettingsCategoryViewModel @@ -102,15 +104,17 @@ class Dependencies( fun resultsViewModel(goToResult: (ResultModel.Id) -> Unit) = ResultsViewModel(goToResult, getResults::invoke) - fun settingsViewModel(goToSettingsForCategory: (String) -> Unit) = SettingsViewModel(goToSettingsForCategory) + fun settingsViewModel(goToSettingsForCategory: (PreferenceCategoryKey) -> Unit) = SettingsViewModel(goToSettingsForCategory) fun settingsCategoryViewModel( - goToSettingsForCategory: (String) -> Unit, + goToSettingsForCategory: (PreferenceCategoryKey) -> Unit, onBack: () -> Unit, + category: SettingsCategoryItem, ) = SettingsCategoryViewModel( preferenceManager = preferenceManager, onBack = onBack, goToSettingsForCategory = goToSettingsForCategory, + category = category, ) fun resultViewModel( 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 712476a7..4442c8bb 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 @@ -9,14 +9,13 @@ import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable -import ooniprobe.composeapp.generated.resources.Res -import ooniprobe.composeapp.generated.resources.send_email -import org.jetbrains.compose.resources.stringResource import org.ooni.probe.data.models.ResultModel +import org.ooni.probe.data.repositories.PreferenceCategoryKey import org.ooni.probe.di.Dependencies import org.ooni.probe.ui.dashboard.DashboardScreen import org.ooni.probe.ui.result.ResultScreen import org.ooni.probe.ui.results.ResultsScreen +import org.ooni.probe.ui.settings.SettingsCategoryItem import org.ooni.probe.ui.settings.SettingsScreen import org.ooni.probe.ui.settings.category.SettingsCategoryScreen @@ -81,7 +80,7 @@ fun Navigation( ) { entry -> val category = entry.arguments?.getString("category") ?: return@composable when (category) { - stringResource(Res.string.send_email) -> { + PreferenceCategoryKey.SEND_EMAIL.value -> { // TODO: Implement based on platform } @@ -93,9 +92,18 @@ fun Navigation( navController.navigate(Screen.SettingsCategory(it).route) }, onBack = { navController.navigateUp() }, + category = + SettingsCategoryItem.getSettingsItem( + PreferenceCategoryKey.valueOf(category.uppercase()), + ), ) } - SettingsCategoryScreen(category = category, onEvent = viewModel::onEvent) + val state by viewModel.state.collectAsState() + + SettingsCategoryScreen( + state = state, + onEvent = viewModel::onEvent, + ) } } } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Screen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Screen.kt index 38628a63..c113c283 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Screen.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Screen.kt @@ -3,6 +3,7 @@ package org.ooni.probe.ui.navigation import androidx.navigation.NavType import androidx.navigation.navArgument import org.ooni.probe.data.models.ResultModel +import org.ooni.probe.data.repositories.PreferenceCategoryKey sealed class Screen( val route: String, @@ -22,7 +23,7 @@ sealed class Screen( } } - data class SettingsCategory(val category: String) : Screen("settings/$category") { + data class SettingsCategory(val category: PreferenceCategoryKey) : Screen("settings/${category.value}") { companion object { const val NAV_ROUTE = "settings/{category}" val ARGUMENTS = listOf(navArgument("category") { type = NavType.StringType }) diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsScreen.kt index 1a6fbf8d..ccd76488 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsScreen.kt @@ -11,29 +11,136 @@ import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import ooniprobe.composeapp.generated.resources.CategoryCode_ALDR_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_ALDR_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_ANON_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_ANON_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_COMM_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_COMM_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_COMT_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_COMT_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_CTRL_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_CTRL_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_CULTR_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_CULTR_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_DATE_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_DATE_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_ECON_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_ECON_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_ENV_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_ENV_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_FILE_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_FILE_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_GAME_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_GAME_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_GMB_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_GMB_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_GOVT_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_GOVT_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_GRP_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_GRP_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_HACK_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_HACK_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_HATE_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_HATE_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_HOST_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_HOST_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_HUMR_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_HUMR_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_IGO_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_IGO_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_LGBT_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_LGBT_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_MILX_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_MILX_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_MMED_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_MMED_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_NEWS_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_NEWS_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_POLR_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_POLR_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_PORN_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_PORN_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_PROV_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_PROV_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_PUBH_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_PUBH_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_REL_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_REL_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_SRCH_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_SRCH_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_XED_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_XED_Name +import ooniprobe.composeapp.generated.resources.Modal_EnableNotifications_Paragraph import ooniprobe.composeapp.generated.resources.Res 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_LanguageSettings_Title +import ooniprobe.composeapp.generated.resources.Settings_Advanced_RecentLogs +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_Footer +import ooniprobe.composeapp.generated.resources.Settings_AutomatedTesting_RunAutomatically_WiFiOnly +import ooniprobe.composeapp.generated.resources.Settings_Notifications_Enabled import ooniprobe.composeapp.generated.resources.Settings_Notifications_Label import ooniprobe.composeapp.generated.resources.Settings_Privacy_Label +import ooniprobe.composeapp.generated.resources.Settings_Privacy_SendCrashReports 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_Storage_Label import ooniprobe.composeapp.generated.resources.Settings_TestOptions_Label -import ooniprobe.composeapp.generated.resources.about_ooni +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_MaxRuntime +import ooniprobe.composeapp.generated.resources.Settings_Websites_MaxRuntimeEnabled import ooniprobe.composeapp.generated.resources.advanced +import ooniprobe.composeapp.generated.resources.category_aldr +import ooniprobe.composeapp.generated.resources.category_anon +import ooniprobe.composeapp.generated.resources.category_comm +import ooniprobe.composeapp.generated.resources.category_comt +import ooniprobe.composeapp.generated.resources.category_ctrl +import ooniprobe.composeapp.generated.resources.category_cultr +import ooniprobe.composeapp.generated.resources.category_date +import ooniprobe.composeapp.generated.resources.category_econ +import ooniprobe.composeapp.generated.resources.category_env +import ooniprobe.composeapp.generated.resources.category_file +import ooniprobe.composeapp.generated.resources.category_game +import ooniprobe.composeapp.generated.resources.category_gmb +import ooniprobe.composeapp.generated.resources.category_govt +import ooniprobe.composeapp.generated.resources.category_grp +import ooniprobe.composeapp.generated.resources.category_hack +import ooniprobe.composeapp.generated.resources.category_hate +import ooniprobe.composeapp.generated.resources.category_host +import ooniprobe.composeapp.generated.resources.category_humr +import ooniprobe.composeapp.generated.resources.category_igo +import ooniprobe.composeapp.generated.resources.category_lgbt +import ooniprobe.composeapp.generated.resources.category_milx +import ooniprobe.composeapp.generated.resources.category_mmed +import ooniprobe.composeapp.generated.resources.category_news +import ooniprobe.composeapp.generated.resources.category_polr +import ooniprobe.composeapp.generated.resources.category_porn +import ooniprobe.composeapp.generated.resources.category_prov +import ooniprobe.composeapp.generated.resources.category_pubh +import ooniprobe.composeapp.generated.resources.category_rel +import ooniprobe.composeapp.generated.resources.category_srch +import ooniprobe.composeapp.generated.resources.category_xed import ooniprobe.composeapp.generated.resources.ic_settings import ooniprobe.composeapp.generated.resources.notifications -import ooniprobe.composeapp.generated.resources.ooni_backend_proxy import ooniprobe.composeapp.generated.resources.outline_info import ooniprobe.composeapp.generated.resources.privacy import ooniprobe.composeapp.generated.resources.proxy import ooniprobe.composeapp.generated.resources.send_email import ooniprobe.composeapp.generated.resources.settings -import ooniprobe.composeapp.generated.resources.test_options import org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource +import org.ooni.probe.data.repositories.PreferenceCategoryKey +import org.ooni.probe.data.repositories.SettingsKey +import org.ooni.probe.ui.settings.category.SettingsDescription @Composable fun SettingsScreen(onNavigateToSettingsCategory: (SettingsViewModel.Event) -> Unit) { @@ -43,110 +150,454 @@ fun SettingsScreen(onNavigateToSettingsCategory: (SettingsViewModel.Event) -> Un Text(stringResource(Res.string.settings)) }, ) - SettingsItem( - icon = Res.drawable.notifications, - title = Res.string.Settings_Notifications_Label, - modifier = - stringResource(Res.string.notifications).let { category -> - Modifier.clickable { - onNavigateToSettingsCategory( - category.routeToSettingsCategory(), - ) - } - }, - ) - SettingsItem( - icon = Res.drawable.ic_settings, - title = Res.string.Settings_TestOptions_Label, - modifier = - stringResource(Res.string.test_options).let { category -> - Modifier.clickable { - onNavigateToSettingsCategory( - category.routeToSettingsCategory(), - ) - } - }, - ) - SettingsItem( - icon = Res.drawable.privacy, - title = Res.string.Settings_Privacy_Label, - modifier = - stringResource(Res.string.privacy).let { category -> - Modifier.clickable { - onNavigateToSettingsCategory( - category.routeToSettingsCategory(), - ) - } - }, - ) - SettingsItem( - icon = Res.drawable.proxy, - title = Res.string.Settings_Proxy_Label, - modifier = - stringResource(Res.string.ooni_backend_proxy).let { category -> - Modifier.clickable { - onNavigateToSettingsCategory( - category.routeToSettingsCategory(), - ) - } - }, - ) - SettingsItem( - icon = Res.drawable.advanced, - title = Res.string.Settings_Advanced_Label, - modifier = - stringResource(Res.string.advanced).let { category -> - Modifier.clickable { - onNavigateToSettingsCategory( - category.routeToSettingsCategory(), - ) - } - }, - ) - SettingsItem( - icon = Res.drawable.send_email, - title = Res.string.Settings_SendEmail_Label, - modifier = - stringResource(Res.string.send_email).let { category -> - Modifier.clickable { - onNavigateToSettingsCategory( - category.routeToSettingsCategory(), - ) - } - }, - ) - SettingsItem( - icon = Res.drawable.outline_info, - title = Res.string.Settings_About_Label, - modifier = - stringResource(Res.string.about_ooni).let { category -> + + SettingsCategoryItem.getSettingsItems().forEach { item -> + SettingsItemView( + icon = item.icon, + title = item.title, + modifier = Modifier.clickable { onNavigateToSettingsCategory( - category.routeToSettingsCategory(), + item.routeToSettingsCategory(), ) - } - }, - ) + }, + ) + } } } -fun String.routeToSettingsCategory() = SettingsViewModel.Event.SettingsCategoryClick(this) - @Composable -fun SettingsItem( - icon: DrawableResource, +fun SettingsItemView( + icon: DrawableResource?, title: StringResource, modifier: Modifier, ) { ListItem( leadingContent = { - Image( - modifier = Modifier.height(24.dp).width(24.dp), - painter = painterResource(icon), - contentDescription = stringResource(title), - ) + icon?.let { + Image( + modifier = Modifier.height(24.dp).width(24.dp), + painter = painterResource(it), + contentDescription = stringResource(title), + ) + } }, headlineContent = { Text(stringResource(title)) }, modifier = modifier, ) } + +open class PreferenceItem( + open val title: StringResource, + open val icon: DrawableResource? = null, + open val type: PreferenceItemType, + open val key: String, + open val supportingContent: + @Composable() + (() -> Unit)? = null, +) + +data class SettingsItem( + override val icon: DrawableResource? = null, + override val title: StringResource, + override val type: PreferenceItemType, + override val key: String, + val children: List? = emptyList(), + override val supportingContent: + @Composable() + (() -> Unit)? = null, +) : PreferenceItem(title = title, icon = icon, supportingContent = supportingContent, type = type, key = key) + +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 = route.value, + ) { + fun routeToSettingsCategory() = SettingsViewModel.Event.SettingsCategoryClick(route) + + companion object { + private val seeRecentLogsCategory = + SettingsCategoryItem( + title = Res.string.Settings_Advanced_RecentLogs, + route = PreferenceCategoryKey.SEE_RECENT_LOGS, + ) + private val webCategory = + SettingsCategoryItem( + title = Res.string.Settings_Websites_Categories_Label, + route = PreferenceCategoryKey.WEBSITES_CATEGORIES, + supportingContent = { + Text(stringResource(Res.string.Settings_Websites_Categories_Description)) + }, + settings = + listOf( + SettingsItem( + icon = Res.drawable.category_anon, + title = Res.string.CategoryCode_ANON_Name, + supportingContent = { + Text(stringResource(Res.string.CategoryCode_ANON_Description)) + }, + key = SettingsKey.ANON.value, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_comt, + title = Res.string.CategoryCode_COMT_Name, + supportingContent = { + Text(stringResource(Res.string.CategoryCode_COMT_Description)) + }, + key = SettingsKey.COMT.value, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_ctrl, + title = Res.string.CategoryCode_CTRL_Name, + supportingContent = { + Text(stringResource(Res.string.CategoryCode_CTRL_Description)) + }, + key = SettingsKey.CTRL.value, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_cultr, + title = Res.string.CategoryCode_CULTR_Name, + supportingContent = { + Text(stringResource(Res.string.CategoryCode_CULTR_Description)) + }, + key = SettingsKey.CULTR.value, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_aldr, + title = Res.string.CategoryCode_ALDR_Name, + supportingContent = { + Text(stringResource(Res.string.CategoryCode_ALDR_Description)) + }, + key = SettingsKey.ALDR.value, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_comm, + title = Res.string.CategoryCode_COMM_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_COMM_Description)) }, + key = SettingsKey.COMM.value, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_econ, + title = Res.string.CategoryCode_ECON_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_ECON_Description)) }, + key = SettingsKey.ECON.value, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_env, + title = Res.string.CategoryCode_ENV_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_ENV_Description)) }, + key = SettingsKey.ENV.value, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_file, + title = Res.string.CategoryCode_FILE_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_FILE_Description)) }, + key = SettingsKey.FILE.value, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_gmb, + title = Res.string.CategoryCode_GMB_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_GMB_Description)) }, + key = SettingsKey.GMB.value, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_game, + title = Res.string.CategoryCode_GAME_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_GAME_Description)) }, + key = SettingsKey.GAME.value, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_govt, + title = Res.string.CategoryCode_GOVT_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_GOVT_Description)) }, + key = SettingsKey.GOVT.value, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_hack, + title = Res.string.CategoryCode_HACK_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_HACK_Description)) }, + key = SettingsKey.HACK.value, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_hate, + title = Res.string.CategoryCode_HATE_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_HATE_Description)) }, + key = SettingsKey.HATE.value, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_host, + title = Res.string.CategoryCode_HOST_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_HOST_Description)) }, + key = SettingsKey.HOST.value, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_humr, + title = Res.string.CategoryCode_HUMR_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_HUMR_Description)) }, + key = SettingsKey.HUMR.value, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_igo, + title = Res.string.CategoryCode_IGO_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_IGO_Description)) }, + key = SettingsKey.IGO.value, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_lgbt, + title = Res.string.CategoryCode_LGBT_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_LGBT_Description)) }, + key = SettingsKey.LGBT.value, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_mmed, + title = Res.string.CategoryCode_MMED_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_MMED_Description)) }, + key = SettingsKey.MMED.value, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_news, + title = Res.string.CategoryCode_NEWS_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_NEWS_Description)) }, + key = SettingsKey.NEWS.value, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_date, + title = Res.string.CategoryCode_DATE_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_DATE_Description)) }, + key = SettingsKey.DATE.value, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_polr, + title = Res.string.CategoryCode_POLR_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_POLR_Description)) }, + key = SettingsKey.POLR.value, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_porn, + title = Res.string.CategoryCode_PORN_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_PORN_Description)) }, + key = SettingsKey.PORN.value, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_prov, + title = Res.string.CategoryCode_PROV_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_PROV_Description)) }, + key = SettingsKey.PROV.value, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_pubh, + title = Res.string.CategoryCode_PUBH_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_PUBH_Description)) }, + key = SettingsKey.PUBH.value, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_rel, + title = Res.string.CategoryCode_REL_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_REL_Description)) }, + key = SettingsKey.REL.value, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_srch, + title = Res.string.CategoryCode_SRCH_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_SRCH_Description)) }, + key = SettingsKey.SRCH.value, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_xed, + title = Res.string.CategoryCode_XED_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_XED_Description)) }, + key = SettingsKey.XED.value, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_grp, + title = Res.string.CategoryCode_GRP_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_GRP_Description)) }, + key = SettingsKey.GRP.value, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_milx, + title = Res.string.CategoryCode_MILX_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_MILX_Description)) }, + key = SettingsKey.MILX.value, + type = PreferenceItemType.SWITCH, + ), + ), + ) + + fun getSettingsItems() = + listOf( + SettingsCategoryItem( + icon = Res.drawable.notifications, + title = Res.string.Settings_Notifications_Label, + route = PreferenceCategoryKey.NOTIFICATIONS, + settings = + listOf( + SettingsItem( + title = Res.string.Settings_Notifications_Enabled, + key = SettingsKey.NOTIFICATIONS_ENABLED.value, + type = PreferenceItemType.SWITCH, + ), + ), + footerContent = { + SettingsDescription( + Res.string.Modal_EnableNotifications_Paragraph, + ) + }, + ), + SettingsCategoryItem( + icon = Res.drawable.ic_settings, + title = Res.string.Settings_TestOptions_Label, + route = PreferenceCategoryKey.TEST_OPTIONS, + settings = + listOf( + SettingsItem( + title = Res.string.Settings_AutomatedTesting_RunAutomatically, + key = SettingsKey.AUTOMATED_TESTING_ENABLED.value, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + title = Res.string.Settings_AutomatedTesting_RunAutomatically_WiFiOnly, + key = SettingsKey.AUTOMATED_TESTING_WIFIONLY.value, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + title = Res.string.Settings_AutomatedTesting_RunAutomatically_ChargingOnly, + key = SettingsKey.AUTOMATED_TESTING_CHARGING.value, + type = PreferenceItemType.SWITCH, + ), + webCategory, + SettingsItem( + title = Res.string.Settings_Websites_MaxRuntimeEnabled, + key = SettingsKey.MAX_RUNTIME_ENABLED.value, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + title = Res.string.Settings_Websites_MaxRuntime, + key = SettingsKey.MAX_RUNTIME.value, + type = PreferenceItemType.TEXT, + ), + ), + footerContent = { + SettingsDescription( + Res.string.Settings_AutomatedTesting_RunAutomatically_Footer, + ) + }, + ), + SettingsCategoryItem( + icon = Res.drawable.privacy, + title = Res.string.Settings_Privacy_Label, + route = PreferenceCategoryKey.PRIVACY, + settings = + listOf( + SettingsItem( + title = Res.string.Settings_Sharing_UploadResults, + key = SettingsKey.UPLOAD_RESULTS.value, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + title = Res.string.Settings_Privacy_SendCrashReports, + key = SettingsKey.SEND_CRASH.value, + type = PreferenceItemType.SWITCH, + ), + ), + ), + SettingsCategoryItem( + icon = Res.drawable.proxy, + title = Res.string.Settings_Proxy_Label, + route = PreferenceCategoryKey.PROXY, + ), + SettingsCategoryItem( + icon = Res.drawable.advanced, + title = Res.string.Settings_Advanced_Label, + route = PreferenceCategoryKey.ADVANCED, + settings = + listOf( + SettingsItem( + title = Res.string.Settings_Advanced_LanguageSettings_Title, + key = SettingsKey.LANGUAGE_SETTING.value, + type = PreferenceItemType.SELECT, + ), + seeRecentLogsCategory, + SettingsItem( + title = Res.string.Settings_Advanced_DebugLogs, + key = SettingsKey.DEBUG_LOGS.value, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + title = Res.string.Settings_Storage_Label, + key = SettingsKey.STORAGE_SIZE.value, + type = PreferenceItemType.BUTTON, + ), + SettingsItem( + title = Res.string.Settings_WarmVPNInUse_Label, + key = SettingsKey.WARN_VPN_IN_USE.value, + type = PreferenceItemType.SWITCH, + ), + ), + ), + SettingsCategoryItem( + icon = Res.drawable.send_email, + title = Res.string.Settings_SendEmail_Label, + route = PreferenceCategoryKey.SEND_EMAIL, + ), + SettingsCategoryItem( + icon = Res.drawable.outline_info, + title = Res.string.Settings_About_Label, + route = PreferenceCategoryKey.ABOUT_OONI, + ), + ) + + fun getSettingsItem(route: PreferenceCategoryKey) = + (getSettingsItems() + listOf(webCategory, seeRecentLogsCategory)).first { + it.route == route + } + } +} + +enum class PreferenceItemType { + SWITCH, + TEXT, + BUTTON, + SELECT, + ROUTE, +} diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsViewModel.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsViewModel.kt index 106b4009..7549e115 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsViewModel.kt @@ -6,9 +6,10 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import org.ooni.probe.data.repositories.PreferenceCategoryKey open class SettingsViewModel( - goToSettingsForCategory: (String) -> Unit, + goToSettingsForCategory: (PreferenceCategoryKey) -> Unit, ) : ViewModel() { private val events = MutableSharedFlow(extraBufferCapacity = 1) @@ -22,6 +23,6 @@ open class SettingsViewModel( } sealed interface Event { - data class SettingsCategoryClick(val category: String) : Event + data class SettingsCategoryClick(val category: PreferenceCategoryKey) : Event } } 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 2ea875ca..d2d1e5e5 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 @@ -1,9 +1,17 @@ package org.ooni.probe.ui.settings.category +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem @@ -12,88 +20,27 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import ooniprobe.composeapp.generated.resources.Modal_EnableNotifications_Paragraph import ooniprobe.composeapp.generated.resources.Res -import ooniprobe.composeapp.generated.resources.Settings_About_Label -import ooniprobe.composeapp.generated.resources.Settings_Advanced_Label -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_Footer -import ooniprobe.composeapp.generated.resources.Settings_AutomatedTesting_RunAutomatically_WiFiOnly -import ooniprobe.composeapp.generated.resources.Settings_Notifications_Enabled -import ooniprobe.composeapp.generated.resources.Settings_Notifications_Label -import ooniprobe.composeapp.generated.resources.Settings_Privacy_Label -import ooniprobe.composeapp.generated.resources.Settings_Privacy_SendCrashReports -import ooniprobe.composeapp.generated.resources.Settings_Proxy_Label -import ooniprobe.composeapp.generated.resources.Settings_Sharing_UploadResults -import ooniprobe.composeapp.generated.resources.Settings_TestOptions_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_MaxRuntime -import ooniprobe.composeapp.generated.resources.Settings_Websites_MaxRuntimeEnabled -import ooniprobe.composeapp.generated.resources.about_ooni -import ooniprobe.composeapp.generated.resources.advanced -import ooniprobe.composeapp.generated.resources.automated_testing_charging -import ooniprobe.composeapp.generated.resources.automated_testing_enabled -import ooniprobe.composeapp.generated.resources.automated_testing_wifionly import ooniprobe.composeapp.generated.resources.back -import ooniprobe.composeapp.generated.resources.max_runtime -import ooniprobe.composeapp.generated.resources.max_runtime_enabled -import ooniprobe.composeapp.generated.resources.notifications -import ooniprobe.composeapp.generated.resources.notifications_enabled -import ooniprobe.composeapp.generated.resources.ooni_backend_proxy -import ooniprobe.composeapp.generated.resources.privacy -import ooniprobe.composeapp.generated.resources.send_crash -import ooniprobe.composeapp.generated.resources.test_options -import ooniprobe.composeapp.generated.resources.upload_results +import ooniprobe.composeapp.generated.resources.settings import org.jetbrains.compose.resources.StringResource +import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource +import org.ooni.probe.data.repositories.PreferenceCategoryKey +import org.ooni.probe.ui.settings.PreferenceItemType @Composable fun SettingsCategoryScreen( - category: String, + state: SettingsCategoryViewModel.State, onEvent: (SettingsCategoryViewModel.Event) -> Unit, ) { - val categories = - mapOf( - stringResource(Res.string.notifications) to - SettingsCategory( - title = Res.string.Settings_Notifications_Label, - content = { NotificationsSettingsScreen(onEvent) }, - ), - stringResource(Res.string.test_options) to - SettingsCategory( - title = Res.string.Settings_TestOptions_Label, - content = { TestOptionsSettingsScreen(onEvent) }, - ), - stringResource(Res.string.privacy) to - SettingsCategory( - title = Res.string.Settings_Privacy_Label, - content = { PrivacySettingsScreen(onEvent) }, - ), - stringResource(Res.string.advanced) to - SettingsCategory( - title = Res.string.Settings_Advanced_Label, - content = { return@SettingsCategory }, - ), - stringResource(Res.string.ooni_backend_proxy) to - SettingsCategory( - title = Res.string.Settings_Proxy_Label, - content = { return@SettingsCategory }, - ), - stringResource(Res.string.about_ooni) to - SettingsCategory( - title = Res.string.Settings_About_Label, - content = { return@SettingsCategory }, - ), - ) - Column { TopAppBar( title = { - categories[category]?.let { Text(stringResource(it.title)) } + Text(stringResource(state.category.title)) }, navigationIcon = { IconButton(onClick = { onEvent(SettingsCategoryViewModel.Event.BackClicked) }) { @@ -104,166 +51,91 @@ fun SettingsCategoryScreen( } }, ) - categories[category]?.content?.invoke() - } -} + Box( + modifier = Modifier.verticalScroll(rememberScrollState()).padding(bottom = 48.dp), + ) { + Column { + state.category.settings?.forEach { preferenceItem -> + when (preferenceItem.type) { + PreferenceItemType.SWITCH -> + SwitchSettingsView( + leadingContent = + preferenceItem.icon?.let { + { + Image( + modifier = Modifier.height(24.dp).width(24.dp), + painter = painterResource(it), + contentDescription = stringResource(preferenceItem.title), + ) + } + }, + title = preferenceItem.title, + key = preferenceItem.key, + checked = + state.preference?.let { it[preferenceItem.key] as? Boolean } + ?: false, + supportingContent = preferenceItem.supportingContent, + onCheckedChange = { key, value -> + onEvent( + SettingsCategoryViewModel.Event.CheckedChangeClick( + key, + value, + ), + ) + }, + ) -data class SettingsCategory( - val title: StringResource, - val content: @Composable () -> Unit, -) + PreferenceItemType.TEXT -> + RouteSettingsView( + title = preferenceItem.title, + supportingContent = preferenceItem.supportingContent, + ) -@Composable -fun NotificationsSettingsScreen(onEvent: (SettingsCategoryViewModel.Event) -> Unit) { - Column { - SwitchSettings( - title = Res.string.Settings_Notifications_Enabled, - key = stringResource(Res.string.notifications_enabled), - checked = false, - onCheckedChange = { key, value -> - onEvent( - SettingsCategoryViewModel.Event.CheckedChangeClick( - key, - value, - ), - ) - }, - ) - SettingsDescription( - Res.string.Modal_EnableNotifications_Paragraph, - ) - } -} + PreferenceItemType.BUTTON -> + RouteSettingsView( + title = preferenceItem.title, + supportingContent = preferenceItem.supportingContent, + trailingContent = { + Button( + onClick = {}, + ) { + Text(stringResource(Res.string.settings)) + } + }, + ) -@Composable -fun TestOptionsSettingsScreen(onEvent: (SettingsCategoryViewModel.Event) -> Unit) { - Column { - SwitchSettings( - title = Res.string.Settings_AutomatedTesting_RunAutomatically, - key = stringResource(Res.string.automated_testing_enabled), - checked = false, - onCheckedChange = { key, value -> - onEvent( - SettingsCategoryViewModel.Event.CheckedChangeClick( - key, - value, - ), - ) - }, - ) - // TODO: Add dependency on status of automated testing above - SwitchSettings( - title = Res.string.Settings_AutomatedTesting_RunAutomatically_WiFiOnly, - key = stringResource(Res.string.automated_testing_wifionly), - checked = false, - onCheckedChange = { key, value -> - onEvent( - SettingsCategoryViewModel.Event.CheckedChangeClick( - key, - value, - ), - ) - }, - ) - SwitchSettings( - title = Res.string.Settings_AutomatedTesting_RunAutomatically_ChargingOnly, - key = stringResource(Res.string.automated_testing_charging), - checked = false, - onCheckedChange = { key, value -> - onEvent( - SettingsCategoryViewModel.Event.CheckedChangeClick( - key, - value, - ), - ) - }, - ) - // TODO: add proper structure to navigate to the websites categories - SwitchSettings( - title = Res.string.Settings_Websites_Categories_Label, - supportingContent = { - Text(stringResource(Res.string.Settings_Websites_Categories_Description)) - }, - key = stringResource(Res.string.automated_testing_charging), - checked = false, - onCheckedChange = { key, value -> - onEvent( - SettingsCategoryViewModel.Event.CheckedChangeClick( - key, - value, - ), - ) - }, - ) - SwitchSettings( - title = Res.string.Settings_Websites_MaxRuntimeEnabled, - key = stringResource(Res.string.max_runtime_enabled), - checked = false, - onCheckedChange = { key, value -> - onEvent( - SettingsCategoryViewModel.Event.CheckedChangeClick( - key, - value, - ), - ) - }, - ) - // Add proper view to enter and validate value. - SwitchSettings( - title = Res.string.Settings_Websites_MaxRuntime, - key = stringResource(Res.string.max_runtime), - checked = false, - onCheckedChange = { key, value -> - onEvent( - SettingsCategoryViewModel.Event.CheckedChangeClick( - key, - value, - ), - ) - }, - ) - SettingsDescription( - Res.string.Settings_AutomatedTesting_RunAutomatically_Footer, - ) - } -} + PreferenceItemType.ROUTE -> + RouteSettingsView( + title = preferenceItem.title, + supportingContent = preferenceItem.supportingContent, + modifier = + Modifier.clickable { + onEvent( + SettingsCategoryViewModel.Event.SettingsCategoryClick( + PreferenceCategoryKey.valueOf(preferenceItem.key.uppercase()), + ), + ) + }, + ) -@Composable -fun PrivacySettingsScreen(onEvent: (SettingsCategoryViewModel.Event) -> Unit) { - Column { - SwitchSettings( - title = Res.string.Settings_Sharing_UploadResults, - key = stringResource(Res.string.upload_results), - checked = false, - onCheckedChange = { key, value -> - onEvent( - SettingsCategoryViewModel.Event.CheckedChangeClick( - key, - value, - ), - ) - }, - ) - SwitchSettings( - title = Res.string.Settings_Privacy_SendCrashReports, - key = stringResource(Res.string.send_crash), - checked = false, - onCheckedChange = { key, value -> - onEvent( - SettingsCategoryViewModel.Event.CheckedChangeClick( - key, - value, - ), - ) - }, - ) + PreferenceItemType.SELECT -> + RouteSettingsView( + title = preferenceItem.title, + supportingContent = preferenceItem.supportingContent, + ) + } + } + state.category.footerContent?.invoke() + } + } } } @Composable -fun SwitchSettings( +fun SwitchSettingsView( title: StringResource, supportingContent: @Composable (() -> Unit)? = null, + leadingContent: @Composable (() -> Unit)? = null, key: String, checked: Boolean, onCheckedChange: (String, Boolean) -> Unit, @@ -271,16 +143,39 @@ fun SwitchSettings( ListItem( headlineContent = { Text(stringResource(title)) }, supportingContent = supportingContent, + leadingContent = leadingContent, trailingContent = { Switch( checked = checked, onCheckedChange = { newValue -> onCheckedChange(key, newValue) }, + modifier = Modifier.scale(0.7f), ) }, ) } +@Composable +fun RouteSettingsView( + title: StringResource, + supportingContent: @Composable (() -> Unit)? = null, + leadingContent: @Composable (() -> Unit)? = null, + trailingContent: @Composable (() -> Unit)? = null, + modifier: Modifier = Modifier, +) { + ListItem( + headlineContent = { Text(stringResource(title)) }, + supportingContent = supportingContent, + leadingContent = leadingContent, + trailingContent = trailingContent, + modifier = modifier, + ) +} + @Composable fun SettingsDescription(description: StringResource) { - Text(stringResource(description), modifier = Modifier.padding(horizontal = 16.dp), fontSize = 12.sp) + Text( + stringResource(description), + modifier = Modifier.padding(horizontal = 16.dp), + fontSize = 12.sp, + ) } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryViewModel.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryViewModel.kt index 34951614..2ac69b2b 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryViewModel.kt @@ -1,48 +1,66 @@ package org.ooni.probe.ui.settings.category import androidx.datastore.preferences.core.booleanPreferencesKey -import androidx.datastore.preferences.core.intPreferencesKey import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import org.ooni.probe.data.repositories.PreferenceCategoryKey import org.ooni.probe.data.repositories.PreferenceRepository -import org.ooni.probe.data.repositories.SettingsKey +import org.ooni.probe.ui.settings.SettingsCategoryItem class SettingsCategoryViewModel( preferenceManager: PreferenceRepository, - goToSettingsForCategory: (String) -> Unit, + goToSettingsForCategory: (PreferenceCategoryKey) -> Unit, onBack: () -> Unit, + category: SettingsCategoryItem, ) : ViewModel() { private val events = MutableSharedFlow(extraBufferCapacity = 1) + private val _state = MutableStateFlow(State(preference = null, category = category)) + val state = _state.asStateFlow() + init { + category.settings?.map { item -> booleanPreferencesKey(item.key) }?.let { preferenceKeys -> + preferenceManager.allSettings(preferenceKeys) + .onEach { result -> _state.update { it.copy(preference = result) } } + .launchIn(viewModelScope) + } + events.filterIsInstance() .onEach { goToSettingsForCategory(it.category) }.launchIn(viewModelScope) - events.filterIsInstance() - .onEach { onBack() }.launchIn(viewModelScope) + + events.filterIsInstance().onEach { + preferenceManager.setValueByKey( + booleanPreferencesKey(it.key), + it.value, + ) + _state.update { state -> + state.copy( + preference = state.preference?.plus(it.key to it.value), + ) + } + }.launchIn(viewModelScope) + + events.filterIsInstance().onEach { onBack() }.launchIn(viewModelScope) } fun onEvent(event: Event) { events.tryEmit(event) } - val settings: StateFlow?> = - preferenceManager - .allSettings(listOf(booleanPreferencesKey(SettingsKey.NOTIFICATIONS_ENABLED.value))) - .stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5000L), - null, - ) + data class State( + val preference: Map?, + val category: SettingsCategoryItem, + ) sealed interface Event { - data class SettingsCategoryClick(val category: String) : Event + data class SettingsCategoryClick(val category: PreferenceCategoryKey) : Event data class CheckedChangeClick(val key: String, val value: Boolean) : Event diff --git a/composeApp/src/commonTest/kotlin/org/ooni/probe/data/repositories/PreferenceRepositoryTest.kt b/composeApp/src/commonTest/kotlin/org/ooni/probe/data/repositories/PreferenceRepositoryTest.kt index 4d7321d0..786658a1 100644 --- a/composeApp/src/commonTest/kotlin/org/ooni/probe/data/repositories/PreferenceRepositoryTest.kt +++ b/composeApp/src/commonTest/kotlin/org/ooni/probe/data/repositories/PreferenceRepositoryTest.kt @@ -20,28 +20,30 @@ class PreferenceRepositoryTest { } @AfterTest - fun after() = runBlocking { - runTest { - preferenceRepository.clear() + fun after() = + runBlocking { + runTest { + preferenceRepository.clear() + } } - } @Test - fun testAllSettings() = runBlocking { - runTest { - val key = stringPreferencesKey(SettingsKey.LANGUAGE_SETTING.value) - val value = "value" - preferenceRepository.setValueByKey(key, value) - val setting: Map = preferenceRepository.allSettings(listOf(key)).first() - assertEquals(value, setting.values.first()) + fun testAllSettings() = + runBlocking { + runTest { + val key = stringPreferencesKey(SettingsKey.LANGUAGE_SETTING.value) + val value = "value" + preferenceRepository.setValueByKey(key, value) + val setting: Map = preferenceRepository.allSettings(listOf(key)).first() + assertEquals(value, setting.values.first()) + } } - } @Test fun testGetPreferenceKey() { assertEquals( SettingsKey.LANGUAGE_SETTING.value, - preferenceRepository.getPreferenceKey(SettingsKey.LANGUAGE_SETTING.value) + preferenceRepository.getPreferenceKey(SettingsKey.LANGUAGE_SETTING.value), ) assertEquals( "prefix_${SettingsKey.LANGUAGE_SETTING.value}", @@ -50,59 +52,66 @@ class PreferenceRepositoryTest { assertEquals( "${SettingsKey.LANGUAGE_SETTING.value}_autorun", preferenceRepository.getPreferenceKey( - SettingsKey.LANGUAGE_SETTING.value, autoRun = true + SettingsKey.LANGUAGE_SETTING.value, + autoRun = true, ), ) assertEquals( "prefix_${SettingsKey.LANGUAGE_SETTING.value}_autorun", preferenceRepository.getPreferenceKey( - SettingsKey.LANGUAGE_SETTING.value, "prefix", true + SettingsKey.LANGUAGE_SETTING.value, + "prefix", + true, ), ) } @Test - fun testGetValueByKey() = runTest { - val key = stringPreferencesKey(SettingsKey.LANGUAGE_SETTING.value) - val value = "value" - preferenceRepository.setValueByKey(key, value) - assertEquals(value, preferenceRepository.getValueByKey(key = key).first()) - } - - - @Test - fun testSetValueByKey() = runTest { - val key = stringPreferencesKey(SettingsKey.LANGUAGE_SETTING.value) - val value = "value" - preferenceRepository.setValueByKey(key, value) - assertEquals(value, preferenceRepository.getValueByKey(key).first()) - } + fun testGetValueByKey() = + runTest { + val key = stringPreferencesKey(SettingsKey.LANGUAGE_SETTING.value) + val value = "value" + preferenceRepository.setValueByKey(key, value) + assertEquals(value, preferenceRepository.getValueByKey(key = key).first()) + } @Test - fun testClear() = runTest { - val key = stringPreferencesKey(SettingsKey.LANGUAGE_SETTING.value) - val value = "value" - preferenceRepository.setValueByKey(key, value) - preferenceRepository.clear() - assertNull(preferenceRepository.getValueByKey(key).first()) - } + fun testSetValueByKey() = + runTest { + val key = stringPreferencesKey(SettingsKey.LANGUAGE_SETTING.value) + val value = "value" + preferenceRepository.setValueByKey(key, value) + assertEquals(value, preferenceRepository.getValueByKey(key).first()) + } @Test - fun testRemove() = runTest { - val key = stringPreferencesKey(SettingsKey.LANGUAGE_SETTING.value) - val value = "value" - preferenceRepository.setValueByKey(key, value) - preferenceRepository.remove(key) - assertNull(preferenceRepository.getValueByKey(key).first()) - } + fun testClear() = + runTest { + val key = stringPreferencesKey(SettingsKey.LANGUAGE_SETTING.value) + val value = "value" + preferenceRepository.setValueByKey(key, value) + preferenceRepository.clear() + assertNull(preferenceRepository.getValueByKey(key).first()) + } @Test - fun testContains() = runBlocking { + fun testRemove() = runTest { val key = stringPreferencesKey(SettingsKey.LANGUAGE_SETTING.value) val value = "value" preferenceRepository.setValueByKey(key, value) - assertEquals(true, preferenceRepository.contains(key)) + preferenceRepository.remove(key) + assertNull(preferenceRepository.getValueByKey(key).first()) + } + + @Test + fun testContains() = + runBlocking { + runTest { + val key = stringPreferencesKey(SettingsKey.LANGUAGE_SETTING.value) + val value = "value" + preferenceRepository.setValueByKey(key, value) + assertEquals(true, preferenceRepository.contains(key)) + } } - } } From 13d6f41aac958fb6fa546fea46b332f9b87e3f34 Mon Sep 17 00:00:00 2001 From: Norbel AMBANUMBEN Date: Mon, 19 Aug 2024 12:11:49 +0100 Subject: [PATCH 04/12] chore: remove synchronize lock --- .../org/ooni/probe/AndroidApplication.kt | 4 +-- .../ooni/testing/CreatePreferenceDataStore.kt | 7 ++-- .../kotlin/org/ooni/probe/CreateDataStore.kt | 34 ------------------- .../kotlin/org/ooni/probe/di/Dependencies.kt | 24 +++++++++++++ .../org/ooni/probe/SetupDependencies.kt | 4 +-- .../ooni/testing/CreatePreferenceDataStore.kt | 7 ++-- gradle/libs.versions.toml | 1 - 7 files changed, 34 insertions(+), 47 deletions(-) delete mode 100644 composeApp/src/commonMain/kotlin/org/ooni/probe/CreateDataStore.kt diff --git a/composeApp/src/androidMain/kotlin/org/ooni/probe/AndroidApplication.kt b/composeApp/src/androidMain/kotlin/org/ooni/probe/AndroidApplication.kt index 1e5f9e0c..1690755c 100644 --- a/composeApp/src/androidMain/kotlin/org/ooni/probe/AndroidApplication.kt +++ b/composeApp/src/androidMain/kotlin/org/ooni/probe/AndroidApplication.kt @@ -47,8 +47,8 @@ class AndroidApplication : Application() { private fun readAssetFile(path: String) = assets.open(path).bufferedReader().use { it.readText() } private fun buildDataStore(): DataStore = - getDataStore( - producePath = { this.filesDir.resolve(DATA_STORE_FILE_NAME).absolutePath }, + Dependencies.getDataStore( + producePath = { this.filesDir.resolve(Dependencies.Companion.DATA_STORE_FILE_NAME).absolutePath }, migrations = listOf(SharedPreferencesMigration(this, "notifications_enabled")), ) } diff --git a/composeApp/src/androidUnitTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt b/composeApp/src/androidUnitTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt index 900b756b..1a28749e 100644 --- a/composeApp/src/androidUnitTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt +++ b/composeApp/src/androidUnitTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt @@ -4,12 +4,11 @@ import android.app.Application import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.test.core.app.ApplicationProvider -import org.ooni.probe.DATA_STORE_FILE_NAME -import org.ooni.probe.getDataStore +import org.ooni.probe.di.Dependencies internal actual fun createPreferenceDataStore(): DataStore { val app = ApplicationProvider.getApplicationContext() - return getDataStore( - producePath = { app.filesDir.resolve(DATA_STORE_FILE_NAME).absolutePath }, + return Dependencies.getDataStore( + producePath = { app.filesDir.resolve(Dependencies.Companion.DATA_STORE_FILE_NAME).absolutePath }, ) } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/CreateDataStore.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/CreateDataStore.kt deleted file mode 100644 index 898edf7b..00000000 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/CreateDataStore.kt +++ /dev/null @@ -1,34 +0,0 @@ -package org.ooni.probe - -import androidx.datastore.core.DataMigration -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.PreferenceDataStoreFactory -import androidx.datastore.preferences.core.Preferences -import kotlinx.atomicfu.locks.SynchronizedObject -import kotlinx.atomicfu.locks.synchronized -import okio.Path.Companion.toPath - -private lateinit var dataStore: DataStore - -private val lock = SynchronizedObject() - -/** - * Gets the singleton DataStore instance, creating it if necessary. - */ -fun getDataStore( - producePath: () -> String, - migrations: List> = listOf(), -): DataStore = - synchronized(lock) { - if (::dataStore.isInitialized) { - dataStore - } else { - PreferenceDataStoreFactory.createWithPath( - produceFile = { producePath().toPath() }, - migrations = migrations, - ) - .also { dataStore = it } - } - } - -internal const val DATA_STORE_FILE_NAME = "probe.preferences_pb" 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 95d71373..ae52779c 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt @@ -1,12 +1,15 @@ package org.ooni.probe.di import androidx.annotation.VisibleForTesting +import androidx.datastore.core.DataMigration import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.PreferenceDataStoreFactory import androidx.datastore.preferences.core.Preferences import app.cash.sqldelight.db.SqlDriver import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import kotlinx.serialization.json.Json +import okio.Path.Companion.toPath import org.ooni.engine.Engine import org.ooni.engine.NetworkTypeFinder import org.ooni.engine.OonimkallBridge @@ -132,5 +135,26 @@ class Dependencies( @VisibleForTesting fun buildDatabase(driverFactory: () -> SqlDriver): Database = Database(driverFactory()) + + private lateinit var dataStore: DataStore + internal const val DATA_STORE_FILE_NAME = "probe.preferences_pb" + + /** + * Gets the singleton DataStore instance, creating it if necessary. + */ + fun getDataStore( + producePath: () -> String, + migrations: List> = listOf(), + ): DataStore = + if (::dataStore.isInitialized) { + dataStore + } else { + PreferenceDataStoreFactory.createWithPath( + produceFile = { producePath().toPath() }, + migrations = migrations, + ) + .also { dataStore = it } + } + } } diff --git a/composeApp/src/iosMain/kotlin/org/ooni/probe/SetupDependencies.kt b/composeApp/src/iosMain/kotlin/org/ooni/probe/SetupDependencies.kt index b0dfb065..30e8243c 100644 --- a/composeApp/src/iosMain/kotlin/org/ooni/probe/SetupDependencies.kt +++ b/composeApp/src/iosMain/kotlin/org/ooni/probe/SetupDependencies.kt @@ -87,7 +87,7 @@ private class BundleMarker : NSObject() { @OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) fun buildDataStore(): DataStore = - getDataStore( + Dependencies.getDataStore( producePath = { val documentDirectory: NSURL? = NSFileManager.defaultManager.URLForDirectory( @@ -97,6 +97,6 @@ fun buildDataStore(): DataStore = create = false, error = null, ) - requireNotNull(documentDirectory).path + "/$DATA_STORE_FILE_NAME" + requireNotNull(documentDirectory).path + "/${Dependencies.Companion.DATA_STORE_FILE_NAME}" }, ) diff --git a/composeApp/src/iosTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt b/composeApp/src/iosTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt index 5b17da7b..69e44bfb 100644 --- a/composeApp/src/iosTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt +++ b/composeApp/src/iosTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt @@ -2,8 +2,7 @@ package org.ooni.testing import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences -import org.ooni.probe.DATA_STORE_FILE_NAME -import org.ooni.probe.getDataStore +import org.ooni.probe.di.Dependencies import platform.Foundation.NSDocumentDirectory import platform.Foundation.NSFileManager import platform.Foundation.NSURL @@ -11,7 +10,7 @@ import platform.Foundation.NSUserDomainMask @OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) internal actual fun createPreferenceDataStore(): DataStore { - return getDataStore( + return Dependencies.getDataStore( producePath = { val documentDirectory: NSURL? = NSFileManager.defaultManager.URLForDirectory( @@ -21,7 +20,7 @@ internal actual fun createPreferenceDataStore(): DataStore { create = false, error = null, ) - requireNotNull(documentDirectory).path + "/$DATA_STORE_FILE_NAME" + requireNotNull(documentDirectory).path + "/${Dependencies.Companion.DATA_STORE_FILE_NAME}" }, ) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f6a25e27..54c31c13 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -66,7 +66,6 @@ ui = [ tooling = [ "kermit", "sqldelight-coroutines", - "kotlinx-atomicfu", "androidx-datastore-core-okio", "androidx-datastore-preferences-core", ] From 1c5f18f8fc67708b33d7e7ff020ec3ec24164990 Mon Sep 17 00:00:00 2001 From: Norbel AMBANUMBEN Date: Mon, 19 Aug 2024 12:25:54 +0100 Subject: [PATCH 05/12] chore: remove synchronize lock --- .../src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt | 1 - gradle/libs.versions.toml | 1 - 2 files changed, 2 deletions(-) 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 ae52779c..beaaf7e9 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt @@ -155,6 +155,5 @@ class Dependencies( ) .also { dataStore = it } } - } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 54c31c13..bfb28e06 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -28,7 +28,6 @@ sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } # Kotlin kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.7.1" } kotlin-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version = "0.6.0" } -kotlinx-atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version = "0.23.2" } # UI android-activity = { module = "androidx.activity:activity-ktx", version = "1.9.1" } From 5afc2603a6e819193983ddb487b30a3cbadb3152 Mon Sep 17 00:00:00 2001 From: Norbel AMBANUMBEN Date: Mon, 19 Aug 2024 14:22:00 +0100 Subject: [PATCH 06/12] chore: update based on review --- composeApp/build.gradle.kts | 2 +- .../ooni/testing/CreatePreferenceDataStore.kt | 2 +- .../ooni/probe/ui/navigation/Navigation.kt | 4 +-- .../org/ooni/probe/ui/navigation/Screen.kt | 2 +- .../repositories/PreferenceRepositoryTest.kt | 33 ++++++++----------- .../org/ooni/probe/SetupDependencies.kt | 1 - .../ooni/testing/CreatePreferenceDataStore.kt | 2 +- 7 files changed, 19 insertions(+), 27 deletions(-) diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 4f4025bb..99a03f09 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -104,7 +104,7 @@ kotlin { all { languageSettings { optIn("kotlin.ExperimentalStdlibApi") - // optIn("kotlinx.cinterop.ExperimentalForeignApi") + optIn("kotlinx.cinterop.ExperimentalForeignApi") optIn("kotlinx.cinterop.BetaInteropApi") optIn("kotlinx.coroutines.ExperimentalCoroutinesApi") optIn("androidx.compose.foundation.ExperimentalFoundationApi") diff --git a/composeApp/src/androidUnitTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt b/composeApp/src/androidUnitTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt index 1a28749e..158d2d5b 100644 --- a/composeApp/src/androidUnitTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt +++ b/composeApp/src/androidUnitTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt @@ -9,6 +9,6 @@ import org.ooni.probe.di.Dependencies internal actual fun createPreferenceDataStore(): DataStore { val app = ApplicationProvider.getApplicationContext() return Dependencies.getDataStore( - producePath = { app.filesDir.resolve(Dependencies.Companion.DATA_STORE_FILE_NAME).absolutePath }, + producePath = { app.filesDir.resolve(Dependencies.Companion.DATA_STORE_FILE_NAME+".test").absolutePath }, ) } 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 4442c8bb..523e6dd3 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 @@ -80,7 +80,7 @@ fun Navigation( ) { entry -> val category = entry.arguments?.getString("category") ?: return@composable when (category) { - PreferenceCategoryKey.SEND_EMAIL.value -> { + PreferenceCategoryKey.SEND_EMAIL.name -> { // TODO: Implement based on platform } @@ -94,7 +94,7 @@ fun Navigation( onBack = { navController.navigateUp() }, category = SettingsCategoryItem.getSettingsItem( - PreferenceCategoryKey.valueOf(category.uppercase()), + PreferenceCategoryKey.valueOf(category), ), ) } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Screen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Screen.kt index c113c283..911b4866 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Screen.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Screen.kt @@ -23,7 +23,7 @@ sealed class Screen( } } - data class SettingsCategory(val category: PreferenceCategoryKey) : Screen("settings/${category.value}") { + data class SettingsCategory(val category: PreferenceCategoryKey) : Screen("settings/${category.name}") { companion object { const val NAV_ROUTE = "settings/{category}" val ARGUMENTS = listOf(navArgument("category") { type = NavType.StringType }) diff --git a/composeApp/src/commonTest/kotlin/org/ooni/probe/data/repositories/PreferenceRepositoryTest.kt b/composeApp/src/commonTest/kotlin/org/ooni/probe/data/repositories/PreferenceRepositoryTest.kt index 786658a1..810f32e8 100644 --- a/composeApp/src/commonTest/kotlin/org/ooni/probe/data/repositories/PreferenceRepositoryTest.kt +++ b/composeApp/src/commonTest/kotlin/org/ooni/probe/data/repositories/PreferenceRepositoryTest.kt @@ -2,7 +2,6 @@ package org.ooni.probe.data.repositories import androidx.datastore.preferences.core.stringPreferencesKey import kotlinx.coroutines.flow.first -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import org.ooni.testing.createPreferenceDataStore import kotlin.test.AfterTest @@ -21,22 +20,18 @@ class PreferenceRepositoryTest { @AfterTest fun after() = - runBlocking { - runTest { - preferenceRepository.clear() - } + runTest { + preferenceRepository.clear() } @Test fun testAllSettings() = - runBlocking { - runTest { - val key = stringPreferencesKey(SettingsKey.LANGUAGE_SETTING.value) - val value = "value" - preferenceRepository.setValueByKey(key, value) - val setting: Map = preferenceRepository.allSettings(listOf(key)).first() - assertEquals(value, setting.values.first()) - } + runTest { + val key = stringPreferencesKey(SettingsKey.LANGUAGE_SETTING.value) + val value = "value" + preferenceRepository.setValueByKey(key, value) + val setting: Map = preferenceRepository.allSettings(listOf(key)).first() + assertEquals(value, setting.values.first()) } @Test @@ -106,12 +101,10 @@ class PreferenceRepositoryTest { @Test fun testContains() = - runBlocking { - runTest { - val key = stringPreferencesKey(SettingsKey.LANGUAGE_SETTING.value) - val value = "value" - preferenceRepository.setValueByKey(key, value) - assertEquals(true, preferenceRepository.contains(key)) - } + runTest { + val key = stringPreferencesKey(SettingsKey.LANGUAGE_SETTING.value) + val value = "value" + preferenceRepository.setValueByKey(key, value) + assertEquals(true, preferenceRepository.contains(key)) } } diff --git a/composeApp/src/iosMain/kotlin/org/ooni/probe/SetupDependencies.kt b/composeApp/src/iosMain/kotlin/org/ooni/probe/SetupDependencies.kt index 30e8243c..1f9e6a8d 100644 --- a/composeApp/src/iosMain/kotlin/org/ooni/probe/SetupDependencies.kt +++ b/composeApp/src/iosMain/kotlin/org/ooni/probe/SetupDependencies.kt @@ -85,7 +85,6 @@ private class BundleMarker : NSObject() { companion object : NSObjectMeta() } -@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) fun buildDataStore(): DataStore = Dependencies.getDataStore( producePath = { diff --git a/composeApp/src/iosTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt b/composeApp/src/iosTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt index 69e44bfb..faef5211 100644 --- a/composeApp/src/iosTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt +++ b/composeApp/src/iosTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt @@ -20,7 +20,7 @@ internal actual fun createPreferenceDataStore(): DataStore { create = false, error = null, ) - requireNotNull(documentDirectory).path + "/${Dependencies.Companion.DATA_STORE_FILE_NAME}" + requireNotNull(documentDirectory).path + "/${Dependencies.Companion.DATA_STORE_FILE_NAME}.test" }, ) } From ed9649d9d660efeaef073c260feaeb3dbf0e57cc Mon Sep 17 00:00:00 2001 From: Norbel AMBANUMBEN Date: Mon, 19 Aug 2024 15:55:10 +0100 Subject: [PATCH 07/12] chore: update based on review --- .../data/repositories/PreferenceRepository.kt | 2 + .../ooni/probe/ui/settings/SettingsScreen.kt | 90 +++++++++---------- .../category/SettingsCategoryScreen.kt | 14 +-- .../category/SettingsCategoryViewModel.kt | 9 +- 4 files changed, 61 insertions(+), 54 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/PreferenceRepository.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/PreferenceRepository.kt index 1549e4f5..fbcae3ac 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/PreferenceRepository.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/PreferenceRepository.kt @@ -151,4 +151,6 @@ enum class SettingsKey(val value: String) { RUN_HTTP_HEADER_FIELD_MANIPULATION("run_http_header_field_manipulation"), EXPERIMENTAL("experimental"), TEST_WHATSAPP("test_whatsapp"), + + ROUTE("route"), } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsScreen.kt index ccd76488..a8b159f7 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsScreen.kt @@ -191,7 +191,7 @@ open class PreferenceItem( open val title: StringResource, open val icon: DrawableResource? = null, open val type: PreferenceItemType, - open val key: String, + open val key: SettingsKey, open val supportingContent: @Composable() (() -> Unit)? = null, @@ -201,7 +201,7 @@ data class SettingsItem( override val icon: DrawableResource? = null, override val title: StringResource, override val type: PreferenceItemType, - override val key: String, + override val key: SettingsKey, val children: List? = emptyList(), override val supportingContent: @Composable() @@ -222,7 +222,7 @@ data class SettingsCategoryItem( icon = icon, supportingContent = supportingContent, type = PreferenceItemType.ROUTE, - key = route.value, + key = SettingsKey.ROUTE, ) { fun routeToSettingsCategory() = SettingsViewModel.Event.SettingsCategoryClick(route) @@ -247,7 +247,7 @@ data class SettingsCategoryItem( supportingContent = { Text(stringResource(Res.string.CategoryCode_ANON_Description)) }, - key = SettingsKey.ANON.value, + key = SettingsKey.ANON, type = PreferenceItemType.SWITCH, ), SettingsItem( @@ -256,7 +256,7 @@ data class SettingsCategoryItem( supportingContent = { Text(stringResource(Res.string.CategoryCode_COMT_Description)) }, - key = SettingsKey.COMT.value, + key = SettingsKey.COMT, type = PreferenceItemType.SWITCH, ), SettingsItem( @@ -265,7 +265,7 @@ data class SettingsCategoryItem( supportingContent = { Text(stringResource(Res.string.CategoryCode_CTRL_Description)) }, - key = SettingsKey.CTRL.value, + key = SettingsKey.CTRL, type = PreferenceItemType.SWITCH, ), SettingsItem( @@ -274,7 +274,7 @@ data class SettingsCategoryItem( supportingContent = { Text(stringResource(Res.string.CategoryCode_CULTR_Description)) }, - key = SettingsKey.CULTR.value, + key = SettingsKey.CULTR, type = PreferenceItemType.SWITCH, ), SettingsItem( @@ -283,182 +283,182 @@ data class SettingsCategoryItem( supportingContent = { Text(stringResource(Res.string.CategoryCode_ALDR_Description)) }, - key = SettingsKey.ALDR.value, + key = SettingsKey.ALDR, type = PreferenceItemType.SWITCH, ), SettingsItem( icon = Res.drawable.category_comm, title = Res.string.CategoryCode_COMM_Name, supportingContent = { Text(stringResource(Res.string.CategoryCode_COMM_Description)) }, - key = SettingsKey.COMM.value, + key = SettingsKey.COMM, type = PreferenceItemType.SWITCH, ), SettingsItem( icon = Res.drawable.category_econ, title = Res.string.CategoryCode_ECON_Name, supportingContent = { Text(stringResource(Res.string.CategoryCode_ECON_Description)) }, - key = SettingsKey.ECON.value, + key = SettingsKey.ECON, type = PreferenceItemType.SWITCH, ), SettingsItem( icon = Res.drawable.category_env, title = Res.string.CategoryCode_ENV_Name, supportingContent = { Text(stringResource(Res.string.CategoryCode_ENV_Description)) }, - key = SettingsKey.ENV.value, + key = SettingsKey.ENV, type = PreferenceItemType.SWITCH, ), SettingsItem( icon = Res.drawable.category_file, title = Res.string.CategoryCode_FILE_Name, supportingContent = { Text(stringResource(Res.string.CategoryCode_FILE_Description)) }, - key = SettingsKey.FILE.value, + key = SettingsKey.FILE, type = PreferenceItemType.SWITCH, ), SettingsItem( icon = Res.drawable.category_gmb, title = Res.string.CategoryCode_GMB_Name, supportingContent = { Text(stringResource(Res.string.CategoryCode_GMB_Description)) }, - key = SettingsKey.GMB.value, + key = SettingsKey.GMB, type = PreferenceItemType.SWITCH, ), SettingsItem( icon = Res.drawable.category_game, title = Res.string.CategoryCode_GAME_Name, supportingContent = { Text(stringResource(Res.string.CategoryCode_GAME_Description)) }, - key = SettingsKey.GAME.value, + key = SettingsKey.GAME, type = PreferenceItemType.SWITCH, ), SettingsItem( icon = Res.drawable.category_govt, title = Res.string.CategoryCode_GOVT_Name, supportingContent = { Text(stringResource(Res.string.CategoryCode_GOVT_Description)) }, - key = SettingsKey.GOVT.value, + key = SettingsKey.GOVT, type = PreferenceItemType.SWITCH, ), SettingsItem( icon = Res.drawable.category_hack, title = Res.string.CategoryCode_HACK_Name, supportingContent = { Text(stringResource(Res.string.CategoryCode_HACK_Description)) }, - key = SettingsKey.HACK.value, + key = SettingsKey.HACK, type = PreferenceItemType.SWITCH, ), SettingsItem( icon = Res.drawable.category_hate, title = Res.string.CategoryCode_HATE_Name, supportingContent = { Text(stringResource(Res.string.CategoryCode_HATE_Description)) }, - key = SettingsKey.HATE.value, + key = SettingsKey.HATE, type = PreferenceItemType.SWITCH, ), SettingsItem( icon = Res.drawable.category_host, title = Res.string.CategoryCode_HOST_Name, supportingContent = { Text(stringResource(Res.string.CategoryCode_HOST_Description)) }, - key = SettingsKey.HOST.value, + key = SettingsKey.HOST, type = PreferenceItemType.SWITCH, ), SettingsItem( icon = Res.drawable.category_humr, title = Res.string.CategoryCode_HUMR_Name, supportingContent = { Text(stringResource(Res.string.CategoryCode_HUMR_Description)) }, - key = SettingsKey.HUMR.value, + key = SettingsKey.HUMR, type = PreferenceItemType.SWITCH, ), SettingsItem( icon = Res.drawable.category_igo, title = Res.string.CategoryCode_IGO_Name, supportingContent = { Text(stringResource(Res.string.CategoryCode_IGO_Description)) }, - key = SettingsKey.IGO.value, + key = SettingsKey.IGO, type = PreferenceItemType.SWITCH, ), SettingsItem( icon = Res.drawable.category_lgbt, title = Res.string.CategoryCode_LGBT_Name, supportingContent = { Text(stringResource(Res.string.CategoryCode_LGBT_Description)) }, - key = SettingsKey.LGBT.value, + key = SettingsKey.LGBT, type = PreferenceItemType.SWITCH, ), SettingsItem( icon = Res.drawable.category_mmed, title = Res.string.CategoryCode_MMED_Name, supportingContent = { Text(stringResource(Res.string.CategoryCode_MMED_Description)) }, - key = SettingsKey.MMED.value, + key = SettingsKey.MMED, type = PreferenceItemType.SWITCH, ), SettingsItem( icon = Res.drawable.category_news, title = Res.string.CategoryCode_NEWS_Name, supportingContent = { Text(stringResource(Res.string.CategoryCode_NEWS_Description)) }, - key = SettingsKey.NEWS.value, + key = SettingsKey.NEWS, type = PreferenceItemType.SWITCH, ), SettingsItem( icon = Res.drawable.category_date, title = Res.string.CategoryCode_DATE_Name, supportingContent = { Text(stringResource(Res.string.CategoryCode_DATE_Description)) }, - key = SettingsKey.DATE.value, + key = SettingsKey.DATE, type = PreferenceItemType.SWITCH, ), SettingsItem( icon = Res.drawable.category_polr, title = Res.string.CategoryCode_POLR_Name, supportingContent = { Text(stringResource(Res.string.CategoryCode_POLR_Description)) }, - key = SettingsKey.POLR.value, + key = SettingsKey.POLR, type = PreferenceItemType.SWITCH, ), SettingsItem( icon = Res.drawable.category_porn, title = Res.string.CategoryCode_PORN_Name, supportingContent = { Text(stringResource(Res.string.CategoryCode_PORN_Description)) }, - key = SettingsKey.PORN.value, + key = SettingsKey.PORN, type = PreferenceItemType.SWITCH, ), SettingsItem( icon = Res.drawable.category_prov, title = Res.string.CategoryCode_PROV_Name, supportingContent = { Text(stringResource(Res.string.CategoryCode_PROV_Description)) }, - key = SettingsKey.PROV.value, + key = SettingsKey.PROV, type = PreferenceItemType.SWITCH, ), SettingsItem( icon = Res.drawable.category_pubh, title = Res.string.CategoryCode_PUBH_Name, supportingContent = { Text(stringResource(Res.string.CategoryCode_PUBH_Description)) }, - key = SettingsKey.PUBH.value, + key = SettingsKey.PUBH, type = PreferenceItemType.SWITCH, ), SettingsItem( icon = Res.drawable.category_rel, title = Res.string.CategoryCode_REL_Name, supportingContent = { Text(stringResource(Res.string.CategoryCode_REL_Description)) }, - key = SettingsKey.REL.value, + key = SettingsKey.REL, type = PreferenceItemType.SWITCH, ), SettingsItem( icon = Res.drawable.category_srch, title = Res.string.CategoryCode_SRCH_Name, supportingContent = { Text(stringResource(Res.string.CategoryCode_SRCH_Description)) }, - key = SettingsKey.SRCH.value, + key = SettingsKey.SRCH, type = PreferenceItemType.SWITCH, ), SettingsItem( icon = Res.drawable.category_xed, title = Res.string.CategoryCode_XED_Name, supportingContent = { Text(stringResource(Res.string.CategoryCode_XED_Description)) }, - key = SettingsKey.XED.value, + key = SettingsKey.XED, type = PreferenceItemType.SWITCH, ), SettingsItem( icon = Res.drawable.category_grp, title = Res.string.CategoryCode_GRP_Name, supportingContent = { Text(stringResource(Res.string.CategoryCode_GRP_Description)) }, - key = SettingsKey.GRP.value, + key = SettingsKey.GRP, type = PreferenceItemType.SWITCH, ), SettingsItem( icon = Res.drawable.category_milx, title = Res.string.CategoryCode_MILX_Name, supportingContent = { Text(stringResource(Res.string.CategoryCode_MILX_Description)) }, - key = SettingsKey.MILX.value, + key = SettingsKey.MILX, type = PreferenceItemType.SWITCH, ), ), @@ -474,7 +474,7 @@ data class SettingsCategoryItem( listOf( SettingsItem( title = Res.string.Settings_Notifications_Enabled, - key = SettingsKey.NOTIFICATIONS_ENABLED.value, + key = SettingsKey.NOTIFICATIONS_ENABLED, type = PreferenceItemType.SWITCH, ), ), @@ -492,28 +492,28 @@ data class SettingsCategoryItem( listOf( SettingsItem( title = Res.string.Settings_AutomatedTesting_RunAutomatically, - key = SettingsKey.AUTOMATED_TESTING_ENABLED.value, + key = SettingsKey.AUTOMATED_TESTING_ENABLED, type = PreferenceItemType.SWITCH, ), SettingsItem( title = Res.string.Settings_AutomatedTesting_RunAutomatically_WiFiOnly, - key = SettingsKey.AUTOMATED_TESTING_WIFIONLY.value, + key = SettingsKey.AUTOMATED_TESTING_WIFIONLY, type = PreferenceItemType.SWITCH, ), SettingsItem( title = Res.string.Settings_AutomatedTesting_RunAutomatically_ChargingOnly, - key = SettingsKey.AUTOMATED_TESTING_CHARGING.value, + key = SettingsKey.AUTOMATED_TESTING_CHARGING, type = PreferenceItemType.SWITCH, ), webCategory, SettingsItem( title = Res.string.Settings_Websites_MaxRuntimeEnabled, - key = SettingsKey.MAX_RUNTIME_ENABLED.value, + key = SettingsKey.MAX_RUNTIME_ENABLED, type = PreferenceItemType.SWITCH, ), SettingsItem( title = Res.string.Settings_Websites_MaxRuntime, - key = SettingsKey.MAX_RUNTIME.value, + key = SettingsKey.MAX_RUNTIME, type = PreferenceItemType.TEXT, ), ), @@ -531,12 +531,12 @@ data class SettingsCategoryItem( listOf( SettingsItem( title = Res.string.Settings_Sharing_UploadResults, - key = SettingsKey.UPLOAD_RESULTS.value, + key = SettingsKey.UPLOAD_RESULTS, type = PreferenceItemType.SWITCH, ), SettingsItem( title = Res.string.Settings_Privacy_SendCrashReports, - key = SettingsKey.SEND_CRASH.value, + key = SettingsKey.SEND_CRASH, type = PreferenceItemType.SWITCH, ), ), @@ -554,23 +554,23 @@ data class SettingsCategoryItem( listOf( SettingsItem( title = Res.string.Settings_Advanced_LanguageSettings_Title, - key = SettingsKey.LANGUAGE_SETTING.value, + key = SettingsKey.LANGUAGE_SETTING, type = PreferenceItemType.SELECT, ), seeRecentLogsCategory, SettingsItem( title = Res.string.Settings_Advanced_DebugLogs, - key = SettingsKey.DEBUG_LOGS.value, + key = SettingsKey.DEBUG_LOGS, type = PreferenceItemType.SWITCH, ), SettingsItem( title = Res.string.Settings_Storage_Label, - key = SettingsKey.STORAGE_SIZE.value, + key = SettingsKey.STORAGE_SIZE, type = PreferenceItemType.BUTTON, ), SettingsItem( title = Res.string.Settings_WarmVPNInUse_Label, - key = SettingsKey.WARN_VPN_IN_USE.value, + key = SettingsKey.WARN_VPN_IN_USE, type = PreferenceItemType.SWITCH, ), ), 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 d2d1e5e5..fbc90db6 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 @@ -30,7 +30,9 @@ import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import org.ooni.probe.data.repositories.PreferenceCategoryKey +import org.ooni.probe.data.repositories.SettingsKey import org.ooni.probe.ui.settings.PreferenceItemType +import org.ooni.probe.ui.settings.SettingsCategoryItem @Composable fun SettingsCategoryScreen( @@ -72,7 +74,7 @@ fun SettingsCategoryScreen( title = preferenceItem.title, key = preferenceItem.key, checked = - state.preference?.let { it[preferenceItem.key] as? Boolean } + state.preference?.let { it[preferenceItem.key.value] as? Boolean } ?: false, supportingContent = preferenceItem.supportingContent, onCheckedChange = { key, value -> @@ -110,12 +112,14 @@ fun SettingsCategoryScreen( supportingContent = preferenceItem.supportingContent, modifier = Modifier.clickable { + if(preferenceItem is SettingsCategoryItem) { onEvent( SettingsCategoryViewModel.Event.SettingsCategoryClick( - PreferenceCategoryKey.valueOf(preferenceItem.key.uppercase()), + PreferenceCategoryKey.valueOf(preferenceItem.route.name), ), ) - }, + } + }, ) PreferenceItemType.SELECT -> @@ -136,9 +140,9 @@ fun SwitchSettingsView( title: StringResource, supportingContent: @Composable (() -> Unit)? = null, leadingContent: @Composable (() -> Unit)? = null, - key: String, + key: SettingsKey, checked: Boolean, - onCheckedChange: (String, Boolean) -> Unit, + onCheckedChange: (SettingsKey, Boolean) -> Unit, ) { ListItem( headlineContent = { Text(stringResource(title)) }, diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryViewModel.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryViewModel.kt index 2ac69b2b..cfcd6997 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryViewModel.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import org.ooni.probe.data.repositories.PreferenceCategoryKey import org.ooni.probe.data.repositories.PreferenceRepository +import org.ooni.probe.data.repositories.SettingsKey import org.ooni.probe.ui.settings.SettingsCategoryItem class SettingsCategoryViewModel( @@ -26,7 +27,7 @@ class SettingsCategoryViewModel( val state = _state.asStateFlow() init { - category.settings?.map { item -> booleanPreferencesKey(item.key) }?.let { preferenceKeys -> + category.settings?.map { item -> booleanPreferencesKey(item.key.value) }?.let { preferenceKeys -> preferenceManager.allSettings(preferenceKeys) .onEach { result -> _state.update { it.copy(preference = result) } } .launchIn(viewModelScope) @@ -37,12 +38,12 @@ class SettingsCategoryViewModel( events.filterIsInstance().onEach { preferenceManager.setValueByKey( - booleanPreferencesKey(it.key), + booleanPreferencesKey(it.key.value), it.value, ) _state.update { state -> state.copy( - preference = state.preference?.plus(it.key to it.value), + preference = state.preference?.plus(it.key.value to it.value), ) } }.launchIn(viewModelScope) @@ -62,7 +63,7 @@ class SettingsCategoryViewModel( sealed interface Event { data class SettingsCategoryClick(val category: PreferenceCategoryKey) : Event - data class CheckedChangeClick(val key: String, val value: Boolean) : Event + data class CheckedChangeClick(val key: SettingsKey, val value: Boolean) : Event data object BackClicked : Event } From c04d094877617b51ef49e2efa1c4ad96a2d841a0 Mon Sep 17 00:00:00 2001 From: Norbel AMBANUMBEN Date: Mon, 19 Aug 2024 18:55:02 +0100 Subject: [PATCH 08/12] chore: update based on review --- .../ooni/testing/CreatePreferenceDataStore.kt | 2 +- .../data/repositories/PreferenceRepository.kt | 27 +++++++++++++++++-- .../category/SettingsCategoryScreen.kt | 18 ++++++------- .../category/SettingsCategoryViewModel.kt | 18 +++++++------ .../repositories/PreferenceRepositoryTest.kt | 19 +++++++------ .../ooni/testing/CreatePreferenceDataStore.kt | 2 +- 6 files changed, 57 insertions(+), 29 deletions(-) diff --git a/composeApp/src/androidUnitTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt b/composeApp/src/androidUnitTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt index 158d2d5b..26c271e0 100644 --- a/composeApp/src/androidUnitTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt +++ b/composeApp/src/androidUnitTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt @@ -9,6 +9,6 @@ import org.ooni.probe.di.Dependencies internal actual fun createPreferenceDataStore(): DataStore { val app = ApplicationProvider.getApplicationContext() return Dependencies.getDataStore( - producePath = { app.filesDir.resolve(Dependencies.Companion.DATA_STORE_FILE_NAME+".test").absolutePath }, + producePath = { app.filesDir.resolve("test" + Dependencies.Companion.DATA_STORE_FILE_NAME).absolutePath }, ) } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/PreferenceRepository.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/PreferenceRepository.kt index fbcae3ac..87161d27 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/PreferenceRepository.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/PreferenceRepository.kt @@ -2,7 +2,10 @@ package org.ooni.probe.data.repositories import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map @@ -32,9 +35,29 @@ class PreferenceRepository( return "${prefix?.let { "${it}_" } ?: ""}$name${if (autoRun) "_autorun" else ""}" } - fun allSettings(keys: List>): Flow> = + fun preferenceKeyFromSettingsKey( + key: SettingsKey, + prefix: String? = null, + autoRun: Boolean = false, + ): Preferences.Key<*> { + val preferenceKey = getPreferenceKey(name = key.value, prefix = prefix, autoRun = autoRun) + return when (key) { + SettingsKey.MAX_RUNTIME -> intPreferencesKey(preferenceKey) + SettingsKey.PROXY_PORT -> intPreferencesKey(preferenceKey) + SettingsKey.PROXY_HOSTNAME -> stringPreferencesKey(preferenceKey) + SettingsKey.PROXY_PROTOCOL -> stringPreferencesKey(preferenceKey) + SettingsKey.LANGUAGE_SETTING -> stringPreferencesKey(preferenceKey) + else -> booleanPreferencesKey(preferenceKey) + } + } + + fun allSettings( + keys: List, + prefix: String? = null, + autoRun: Boolean = false, + ): Flow> = dataStore.data.map { - keys.map { key -> key.name to it[key] }.toMap() + keys.map { key -> key to it[preferenceKeyFromSettingsKey(key, prefix, autoRun)] }.toMap() } fun getValueByKey(key: Preferences.Key): Flow { 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 fbc90db6..5c65e578 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 @@ -74,7 +74,7 @@ fun SettingsCategoryScreen( title = preferenceItem.title, key = preferenceItem.key, checked = - state.preference?.let { it[preferenceItem.key.value] as? Boolean } + state.preference?.let { it[preferenceItem.key] as? Boolean } ?: false, supportingContent = preferenceItem.supportingContent, onCheckedChange = { key, value -> @@ -112,14 +112,14 @@ fun SettingsCategoryScreen( supportingContent = preferenceItem.supportingContent, modifier = Modifier.clickable { - if(preferenceItem is SettingsCategoryItem) { - onEvent( - SettingsCategoryViewModel.Event.SettingsCategoryClick( - PreferenceCategoryKey.valueOf(preferenceItem.route.name), - ), - ) - } - }, + if (preferenceItem is SettingsCategoryItem) { + onEvent( + SettingsCategoryViewModel.Event.SettingsCategoryClick( + PreferenceCategoryKey.valueOf(preferenceItem.route.name), + ), + ) + } + }, ) PreferenceItemType.SELECT -> diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryViewModel.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryViewModel.kt index cfcd6997..e068da9b 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryViewModel.kt @@ -1,6 +1,6 @@ package org.ooni.probe.ui.settings.category -import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.Preferences import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableSharedFlow @@ -27,7 +27,7 @@ class SettingsCategoryViewModel( val state = _state.asStateFlow() init { - category.settings?.map { item -> booleanPreferencesKey(item.key.value) }?.let { preferenceKeys -> + category.settings?.map { item -> item.key }?.let { preferenceKeys -> preferenceManager.allSettings(preferenceKeys) .onEach { result -> _state.update { it.copy(preference = result) } } .launchIn(viewModelScope) @@ -37,13 +37,15 @@ class SettingsCategoryViewModel( .onEach { goToSettingsForCategory(it.category) }.launchIn(viewModelScope) events.filterIsInstance().onEach { - preferenceManager.setValueByKey( - booleanPreferencesKey(it.key.value), - it.value, - ) + (preferenceManager.preferenceKeyFromSettingsKey(it.key) as? Preferences.Key)?.let { key -> + preferenceManager.setValueByKey( + key, + it.value, + ) + } _state.update { state -> state.copy( - preference = state.preference?.plus(it.key.value to it.value), + preference = state.preference?.plus(it.key to it.value), ) } }.launchIn(viewModelScope) @@ -56,7 +58,7 @@ class SettingsCategoryViewModel( } data class State( - val preference: Map?, + val preference: Map?, val category: SettingsCategoryItem, ) diff --git a/composeApp/src/commonTest/kotlin/org/ooni/probe/data/repositories/PreferenceRepositoryTest.kt b/composeApp/src/commonTest/kotlin/org/ooni/probe/data/repositories/PreferenceRepositoryTest.kt index 810f32e8..cd2d4457 100644 --- a/composeApp/src/commonTest/kotlin/org/ooni/probe/data/repositories/PreferenceRepositoryTest.kt +++ b/composeApp/src/commonTest/kotlin/org/ooni/probe/data/repositories/PreferenceRepositoryTest.kt @@ -1,6 +1,6 @@ package org.ooni.probe.data.repositories -import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.core.Preferences import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import org.ooni.testing.createPreferenceDataStore @@ -27,10 +27,13 @@ class PreferenceRepositoryTest { @Test fun testAllSettings() = runTest { - val key = stringPreferencesKey(SettingsKey.LANGUAGE_SETTING.value) + val key: Preferences.Key = + preferenceRepository.preferenceKeyFromSettingsKey( + SettingsKey.LANGUAGE_SETTING, + ) as Preferences.Key val value = "value" preferenceRepository.setValueByKey(key, value) - val setting: Map = preferenceRepository.allSettings(listOf(key)).first() + val setting: Map = preferenceRepository.allSettings(listOf(SettingsKey.LANGUAGE_SETTING)).first() assertEquals(value, setting.values.first()) } @@ -64,7 +67,7 @@ class PreferenceRepositoryTest { @Test fun testGetValueByKey() = runTest { - val key = stringPreferencesKey(SettingsKey.LANGUAGE_SETTING.value) + val key = preferenceRepository.preferenceKeyFromSettingsKey(SettingsKey.LANGUAGE_SETTING) as Preferences.Key val value = "value" preferenceRepository.setValueByKey(key, value) assertEquals(value, preferenceRepository.getValueByKey(key = key).first()) @@ -73,7 +76,7 @@ class PreferenceRepositoryTest { @Test fun testSetValueByKey() = runTest { - val key = stringPreferencesKey(SettingsKey.LANGUAGE_SETTING.value) + val key = preferenceRepository.preferenceKeyFromSettingsKey(SettingsKey.LANGUAGE_SETTING) as Preferences.Key val value = "value" preferenceRepository.setValueByKey(key, value) assertEquals(value, preferenceRepository.getValueByKey(key).first()) @@ -82,7 +85,7 @@ class PreferenceRepositoryTest { @Test fun testClear() = runTest { - val key = stringPreferencesKey(SettingsKey.LANGUAGE_SETTING.value) + val key = preferenceRepository.preferenceKeyFromSettingsKey(SettingsKey.LANGUAGE_SETTING) as Preferences.Key val value = "value" preferenceRepository.setValueByKey(key, value) preferenceRepository.clear() @@ -92,7 +95,7 @@ class PreferenceRepositoryTest { @Test fun testRemove() = runTest { - val key = stringPreferencesKey(SettingsKey.LANGUAGE_SETTING.value) + val key = preferenceRepository.preferenceKeyFromSettingsKey(SettingsKey.LANGUAGE_SETTING) as Preferences.Key val value = "value" preferenceRepository.setValueByKey(key, value) preferenceRepository.remove(key) @@ -102,7 +105,7 @@ class PreferenceRepositoryTest { @Test fun testContains() = runTest { - val key = stringPreferencesKey(SettingsKey.LANGUAGE_SETTING.value) + val key = preferenceRepository.preferenceKeyFromSettingsKey(SettingsKey.LANGUAGE_SETTING) as Preferences.Key val value = "value" preferenceRepository.setValueByKey(key, value) assertEquals(true, preferenceRepository.contains(key)) diff --git a/composeApp/src/iosTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt b/composeApp/src/iosTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt index faef5211..393f4a9a 100644 --- a/composeApp/src/iosTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt +++ b/composeApp/src/iosTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt @@ -20,7 +20,7 @@ internal actual fun createPreferenceDataStore(): DataStore { create = false, error = null, ) - requireNotNull(documentDirectory).path + "/${Dependencies.Companion.DATA_STORE_FILE_NAME}.test" + requireNotNull(documentDirectory).path + "/test.${Dependencies.Companion.DATA_STORE_FILE_NAME}" }, ) } From 4ce823fbd278204433f57117c9fff76c0da06d5e Mon Sep 17 00:00:00 2001 From: Norbel AMBANUMBEN Date: Mon, 19 Aug 2024 19:31:34 +0100 Subject: [PATCH 09/12] chore: update based on review --- .../src/androidMain/kotlin/org/ooni/probe/AndroidApplication.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composeApp/src/androidMain/kotlin/org/ooni/probe/AndroidApplication.kt b/composeApp/src/androidMain/kotlin/org/ooni/probe/AndroidApplication.kt index 1690755c..f561e698 100644 --- a/composeApp/src/androidMain/kotlin/org/ooni/probe/AndroidApplication.kt +++ b/composeApp/src/androidMain/kotlin/org/ooni/probe/AndroidApplication.kt @@ -49,6 +49,6 @@ class AndroidApplication : Application() { private fun buildDataStore(): DataStore = Dependencies.getDataStore( producePath = { this.filesDir.resolve(Dependencies.Companion.DATA_STORE_FILE_NAME).absolutePath }, - migrations = listOf(SharedPreferencesMigration(this, "notifications_enabled")), + migrations = listOf(SharedPreferencesMigration(this, "${packageName}_preferences")), ) } From e23d3e8111844958c5961aa8676917d52a8159d6 Mon Sep 17 00:00:00 2001 From: Norbel AMBANUMBEN Date: Tue, 20 Aug 2024 10:20:18 +0100 Subject: [PATCH 10/12] chore: add comment for enabled categories. --- .../kotlin/org/ooni/probe/ui/settings/SettingsScreen.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsScreen.kt index a8b159f7..0c81d3be 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsScreen.kt @@ -237,6 +237,7 @@ data class SettingsCategoryItem( title = Res.string.Settings_Websites_Categories_Label, route = PreferenceCategoryKey.WEBSITES_CATEGORIES, supportingContent = { + // TODO(norbel): add enabled categories Text(stringResource(Res.string.Settings_Websites_Categories_Description)) }, settings = From 9d6b7343f9cf9124c04cb3dca0cda457ad533dea Mon Sep 17 00:00:00 2001 From: Norbel AMBANUMBEN Date: Tue, 20 Aug 2024 10:23:40 +0100 Subject: [PATCH 11/12] revert: pod copy removed from ios project --- iosApp/iosApp.xcodeproj/project.pbxproj | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index 09a16efb..c858e735 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -200,6 +200,7 @@ 7555FF79242A565900829871 /* Resources */, 93E977732C4FE022009CCABC /* ShellScript */, F57E468EACCE9B29FB4C68FC /* [CP] Copy Pods Resources */, + 11F17E981064A71B74C323DA /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -221,6 +222,7 @@ 79FBD0122C5A70AF004E041C /* Resources */, 79FBD0152C5A70AF004E041C /* ShellScript */, 06E00FBBFEA4B57236F98854 /* [CP] Copy Pods Resources */, + 3A3900471DFA31082B6AA60E /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -306,6 +308,23 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NewsMediaScan/Pods-NewsMediaScan-resources.sh\"\n"; showEnvVarsInLog = 0; }; + 11F17E981064A71B74C323DA /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-OONIProbe/Pods-OONIProbe-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-OONIProbe/Pods-OONIProbe-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-OONIProbe/Pods-OONIProbe-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 2221CAC0E44786DF6A5501B8 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; From acf8d07c7abeb68fbe423cc033c583b389a182bf Mon Sep 17 00:00:00 2001 From: Norbel AMBANUMBEN Date: Tue, 20 Aug 2024 12:40:56 +0100 Subject: [PATCH 12/12] chore: update from review --- .../ooni/probe/data/models/PreferenceModel.kt | 641 ++++++++++++++++++ .../data/repositories/PreferenceRepository.kt | 161 ++--- .../kotlin/org/ooni/probe/di/Dependencies.kt | 4 +- .../ooni/probe/ui/navigation/Navigation.kt | 4 +- .../org/ooni/probe/ui/navigation/Screen.kt | 2 +- .../ooni/probe/ui/settings/SettingsScreen.kt | 541 +-------------- .../probe/ui/settings/SettingsViewModel.kt | 2 +- .../category/SettingsCategoryScreen.kt | 8 +- .../category/SettingsCategoryViewModel.kt | 22 +- .../repositories/PreferenceRepositoryTest.kt | 35 +- .../ooni/testing/CreatePreferenceDataStore.kt | 1 - 11 files changed, 722 insertions(+), 699 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/PreferenceModel.kt diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/PreferenceModel.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/PreferenceModel.kt new file mode 100644 index 00000000..93de9e00 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/PreferenceModel.kt @@ -0,0 +1,641 @@ +package org.ooni.probe.data.models + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import ooniprobe.composeapp.generated.resources.CategoryCode_ALDR_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_ALDR_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_ANON_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_ANON_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_COMM_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_COMM_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_COMT_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_COMT_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_CTRL_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_CTRL_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_CULTR_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_CULTR_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_DATE_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_DATE_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_ECON_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_ECON_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_ENV_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_ENV_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_FILE_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_FILE_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_GAME_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_GAME_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_GMB_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_GMB_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_GOVT_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_GOVT_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_GRP_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_GRP_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_HACK_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_HACK_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_HATE_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_HATE_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_HOST_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_HOST_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_HUMR_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_HUMR_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_IGO_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_IGO_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_LGBT_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_LGBT_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_MILX_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_MILX_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_MMED_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_MMED_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_NEWS_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_NEWS_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_POLR_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_POLR_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_PORN_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_PORN_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_PROV_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_PROV_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_PUBH_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_PUBH_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_REL_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_REL_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_SRCH_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_SRCH_Name +import ooniprobe.composeapp.generated.resources.CategoryCode_XED_Description +import ooniprobe.composeapp.generated.resources.CategoryCode_XED_Name +import ooniprobe.composeapp.generated.resources.Modal_EnableNotifications_Paragraph +import ooniprobe.composeapp.generated.resources.Res +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_LanguageSettings_Title +import ooniprobe.composeapp.generated.resources.Settings_Advanced_RecentLogs +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_Footer +import ooniprobe.composeapp.generated.resources.Settings_AutomatedTesting_RunAutomatically_WiFiOnly +import ooniprobe.composeapp.generated.resources.Settings_Notifications_Enabled +import ooniprobe.composeapp.generated.resources.Settings_Notifications_Label +import ooniprobe.composeapp.generated.resources.Settings_Privacy_Label +import ooniprobe.composeapp.generated.resources.Settings_Privacy_SendCrashReports +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_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_MaxRuntime +import ooniprobe.composeapp.generated.resources.Settings_Websites_MaxRuntimeEnabled +import ooniprobe.composeapp.generated.resources.advanced +import ooniprobe.composeapp.generated.resources.category_aldr +import ooniprobe.composeapp.generated.resources.category_anon +import ooniprobe.composeapp.generated.resources.category_comm +import ooniprobe.composeapp.generated.resources.category_comt +import ooniprobe.composeapp.generated.resources.category_ctrl +import ooniprobe.composeapp.generated.resources.category_cultr +import ooniprobe.composeapp.generated.resources.category_date +import ooniprobe.composeapp.generated.resources.category_econ +import ooniprobe.composeapp.generated.resources.category_env +import ooniprobe.composeapp.generated.resources.category_file +import ooniprobe.composeapp.generated.resources.category_game +import ooniprobe.composeapp.generated.resources.category_gmb +import ooniprobe.composeapp.generated.resources.category_govt +import ooniprobe.composeapp.generated.resources.category_grp +import ooniprobe.composeapp.generated.resources.category_hack +import ooniprobe.composeapp.generated.resources.category_hate +import ooniprobe.composeapp.generated.resources.category_host +import ooniprobe.composeapp.generated.resources.category_humr +import ooniprobe.composeapp.generated.resources.category_igo +import ooniprobe.composeapp.generated.resources.category_lgbt +import ooniprobe.composeapp.generated.resources.category_milx +import ooniprobe.composeapp.generated.resources.category_mmed +import ooniprobe.composeapp.generated.resources.category_news +import ooniprobe.composeapp.generated.resources.category_polr +import ooniprobe.composeapp.generated.resources.category_porn +import ooniprobe.composeapp.generated.resources.category_prov +import ooniprobe.composeapp.generated.resources.category_pubh +import ooniprobe.composeapp.generated.resources.category_rel +import ooniprobe.composeapp.generated.resources.category_srch +import ooniprobe.composeapp.generated.resources.category_xed +import ooniprobe.composeapp.generated.resources.ic_settings +import ooniprobe.composeapp.generated.resources.notifications +import ooniprobe.composeapp.generated.resources.outline_info +import ooniprobe.composeapp.generated.resources.privacy +import ooniprobe.composeapp.generated.resources.proxy +import ooniprobe.composeapp.generated.resources.send_email +import org.jetbrains.compose.resources.DrawableResource +import org.jetbrains.compose.resources.StringResource +import org.jetbrains.compose.resources.stringResource +import org.ooni.probe.ui.settings.SettingsViewModel +import org.ooni.probe.ui.settings.category.SettingsDescription + +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, +) + +data class SettingsItem( + override val icon: DrawableResource? = null, + override val title: StringResource, + override val type: PreferenceItemType, + override val key: SettingsKey, + val children: List? = emptyList(), + override val supportingContent: + @Composable() + (() -> Unit)? = null, +) : PreferenceItem(title = title, icon = icon, supportingContent = supportingContent, type = type, key = key) + +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, + ) { + fun routeToSettingsCategory() = SettingsViewModel.Event.SettingsCategoryClick(route) + + companion object { + private val seeRecentLogsCategory = + SettingsCategoryItem( + title = Res.string.Settings_Advanced_RecentLogs, + route = PreferenceCategoryKey.SEE_RECENT_LOGS, + ) + private val webCategory = + SettingsCategoryItem( + title = Res.string.Settings_Websites_Categories_Label, + route = PreferenceCategoryKey.WEBSITES_CATEGORIES, + supportingContent = { + // TODO(norbel): add enabled categories + Text(stringResource(Res.string.Settings_Websites_Categories_Description)) + }, + settings = + listOf( + SettingsItem( + icon = Res.drawable.category_anon, + title = Res.string.CategoryCode_ANON_Name, + supportingContent = { + Text(stringResource(Res.string.CategoryCode_ANON_Description)) + }, + key = SettingsKey.ANON, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_comt, + title = Res.string.CategoryCode_COMT_Name, + supportingContent = { + Text(stringResource(Res.string.CategoryCode_COMT_Description)) + }, + key = SettingsKey.COMT, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_ctrl, + title = Res.string.CategoryCode_CTRL_Name, + supportingContent = { + Text(stringResource(Res.string.CategoryCode_CTRL_Description)) + }, + key = SettingsKey.CTRL, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_cultr, + title = Res.string.CategoryCode_CULTR_Name, + supportingContent = { + Text(stringResource(Res.string.CategoryCode_CULTR_Description)) + }, + key = SettingsKey.CULTR, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_aldr, + title = Res.string.CategoryCode_ALDR_Name, + supportingContent = { + Text(stringResource(Res.string.CategoryCode_ALDR_Description)) + }, + key = SettingsKey.ALDR, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_comm, + title = Res.string.CategoryCode_COMM_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_COMM_Description)) }, + key = SettingsKey.COMM, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_econ, + title = Res.string.CategoryCode_ECON_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_ECON_Description)) }, + key = SettingsKey.ECON, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_env, + title = Res.string.CategoryCode_ENV_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_ENV_Description)) }, + key = SettingsKey.ENV, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_file, + title = Res.string.CategoryCode_FILE_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_FILE_Description)) }, + key = SettingsKey.FILE, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_gmb, + title = Res.string.CategoryCode_GMB_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_GMB_Description)) }, + key = SettingsKey.GMB, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_game, + title = Res.string.CategoryCode_GAME_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_GAME_Description)) }, + key = SettingsKey.GAME, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_govt, + title = Res.string.CategoryCode_GOVT_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_GOVT_Description)) }, + key = SettingsKey.GOVT, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_hack, + title = Res.string.CategoryCode_HACK_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_HACK_Description)) }, + key = SettingsKey.HACK, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_hate, + title = Res.string.CategoryCode_HATE_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_HATE_Description)) }, + key = SettingsKey.HATE, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_host, + title = Res.string.CategoryCode_HOST_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_HOST_Description)) }, + key = SettingsKey.HOST, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_humr, + title = Res.string.CategoryCode_HUMR_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_HUMR_Description)) }, + key = SettingsKey.HUMR, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_igo, + title = Res.string.CategoryCode_IGO_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_IGO_Description)) }, + key = SettingsKey.IGO, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_lgbt, + title = Res.string.CategoryCode_LGBT_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_LGBT_Description)) }, + key = SettingsKey.LGBT, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_mmed, + title = Res.string.CategoryCode_MMED_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_MMED_Description)) }, + key = SettingsKey.MMED, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_news, + title = Res.string.CategoryCode_NEWS_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_NEWS_Description)) }, + key = SettingsKey.NEWS, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_date, + title = Res.string.CategoryCode_DATE_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_DATE_Description)) }, + key = SettingsKey.DATE, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_polr, + title = Res.string.CategoryCode_POLR_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_POLR_Description)) }, + key = SettingsKey.POLR, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_porn, + title = Res.string.CategoryCode_PORN_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_PORN_Description)) }, + key = SettingsKey.PORN, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_prov, + title = Res.string.CategoryCode_PROV_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_PROV_Description)) }, + key = SettingsKey.PROV, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_pubh, + title = Res.string.CategoryCode_PUBH_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_PUBH_Description)) }, + key = SettingsKey.PUBH, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_rel, + title = Res.string.CategoryCode_REL_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_REL_Description)) }, + key = SettingsKey.REL, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_srch, + title = Res.string.CategoryCode_SRCH_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_SRCH_Description)) }, + key = SettingsKey.SRCH, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_xed, + title = Res.string.CategoryCode_XED_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_XED_Description)) }, + key = SettingsKey.XED, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_grp, + title = Res.string.CategoryCode_GRP_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_GRP_Description)) }, + key = SettingsKey.GRP, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + icon = Res.drawable.category_milx, + title = Res.string.CategoryCode_MILX_Name, + supportingContent = { Text(stringResource(Res.string.CategoryCode_MILX_Description)) }, + key = SettingsKey.MILX, + type = PreferenceItemType.SWITCH, + ), + ), + ) + + fun getSettingsItems() = + listOf( + SettingsCategoryItem( + icon = Res.drawable.notifications, + title = Res.string.Settings_Notifications_Label, + route = PreferenceCategoryKey.NOTIFICATIONS, + settings = + listOf( + SettingsItem( + title = Res.string.Settings_Notifications_Enabled, + key = SettingsKey.NOTIFICATIONS_ENABLED, + type = PreferenceItemType.SWITCH, + ), + ), + footerContent = { + SettingsDescription( + Res.string.Modal_EnableNotifications_Paragraph, + ) + }, + ), + SettingsCategoryItem( + icon = Res.drawable.ic_settings, + title = Res.string.Settings_TestOptions_Label, + route = PreferenceCategoryKey.TEST_OPTIONS, + settings = + listOf( + SettingsItem( + title = Res.string.Settings_AutomatedTesting_RunAutomatically, + key = SettingsKey.AUTOMATED_TESTING_ENABLED, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + title = Res.string.Settings_AutomatedTesting_RunAutomatically_WiFiOnly, + key = SettingsKey.AUTOMATED_TESTING_WIFIONLY, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + title = Res.string.Settings_AutomatedTesting_RunAutomatically_ChargingOnly, + key = SettingsKey.AUTOMATED_TESTING_CHARGING, + type = PreferenceItemType.SWITCH, + ), + webCategory, + 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.TEXT, + ), + ), + footerContent = { + SettingsDescription( + Res.string.Settings_AutomatedTesting_RunAutomatically_Footer, + ) + }, + ), + SettingsCategoryItem( + icon = Res.drawable.privacy, + title = Res.string.Settings_Privacy_Label, + route = PreferenceCategoryKey.PRIVACY, + settings = + listOf( + SettingsItem( + title = Res.string.Settings_Sharing_UploadResults, + key = SettingsKey.UPLOAD_RESULTS, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + title = Res.string.Settings_Privacy_SendCrashReports, + key = SettingsKey.SEND_CRASH, + type = PreferenceItemType.SWITCH, + ), + ), + ), + SettingsCategoryItem( + icon = Res.drawable.proxy, + title = Res.string.Settings_Proxy_Label, + route = PreferenceCategoryKey.PROXY, + ), + SettingsCategoryItem( + icon = Res.drawable.advanced, + title = Res.string.Settings_Advanced_Label, + route = PreferenceCategoryKey.ADVANCED, + settings = + listOf( + SettingsItem( + title = Res.string.Settings_Advanced_LanguageSettings_Title, + key = SettingsKey.LANGUAGE_SETTING, + type = PreferenceItemType.SELECT, + ), + seeRecentLogsCategory, + SettingsItem( + title = Res.string.Settings_Advanced_DebugLogs, + key = SettingsKey.DEBUG_LOGS, + type = PreferenceItemType.SWITCH, + ), + SettingsItem( + title = Res.string.Settings_Storage_Label, + key = SettingsKey.STORAGE_SIZE, + type = PreferenceItemType.BUTTON, + ), + SettingsItem( + title = Res.string.Settings_WarmVPNInUse_Label, + key = SettingsKey.WARN_VPN_IN_USE, + type = PreferenceItemType.SWITCH, + ), + ), + ), + SettingsCategoryItem( + icon = Res.drawable.send_email, + title = Res.string.Settings_SendEmail_Label, + route = PreferenceCategoryKey.SEND_EMAIL, + ), + SettingsCategoryItem( + icon = Res.drawable.outline_info, + title = Res.string.Settings_About_Label, + route = PreferenceCategoryKey.ABOUT_OONI, + ), + ) + + fun getSettingsItem(route: PreferenceCategoryKey) = + (getSettingsItems() + listOf(webCategory, seeRecentLogsCategory)).first { + it.route == route + } + } +} + +enum class PreferenceItemType { + SWITCH, + TEXT, + 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"), +} + +enum class SettingsKey(val value: String) { + // Notifications + NOTIFICATIONS_ENABLED("notifications_enabled"), + + // Test Options + AUTOMATED_TESTING_ENABLED("automated_testing_enabled"), + AUTOMATED_TESTING_WIFIONLY("automated_testing_wifionly"), + AUTOMATED_TESTING_CHARGING("automated_testing_charging"), + MAX_RUNTIME_ENABLED("max_runtime_enabled"), + MAX_RUNTIME("max_runtime"), + + // Website categories + SRCH("SRCH"), + PORN("PORN"), + COMM("COMM"), + COMT("COMT"), + MMED("MMED"), + HATE("HATE"), + POLR("POLR"), + PUBH("PUBH"), + GAME("GAME"), + PROV("PROV"), + HACK("HACK"), + MILX("MILX"), + DATE("DATE"), + ANON("ANON"), + ALDR("ALDR"), + GMB("GMB"), + XED("XED"), + REL("REL"), + GRP("GRP"), + GOVT("GOVT"), + ECON("ECON"), + LGBT("LGBT"), + FILE("FILE"), + HOST("HOST"), + HUMR("HUMR"), + NEWS("NEWS"), + ENV("ENV"), + CULTR("CULTR"), + CTRL("CTRL"), + IGO("IGO"), + + // Privacy + UPLOAD_RESULTS("upload_results"), + SEND_CRASH("send_crash"), + + // Proxy + PROXY_HOSTNAME("proxy_hostname"), + PROXY_PORT("proxy_port"), + + // Advanced + THEME_ENABLED("theme_enabled"), + LANGUAGE_SETTING("language_setting"), + DEBUG_LOGS("debugLogs"), + WARN_VPN_IN_USE("warn_vpn_in_use"), + STORAGE_SIZE("storage_size"), // purely decorative + + // MISC + DELETE_UPLOADED_JSONS("deleteUploadedJsons"), + IS_NOTIFICATION_DIALOG("isNotificationDialog"), + FIRST_RUN("first_run"), + + // Run Tests + TEST_SIGNAL("test_signal"), + RUN_HTTP_INVALID_REQUEST_LINE("run_http_invalid_request_line"), + TEST_FACEBOOK_MESSENGER("test_facebook_messenger"), + RUN_DASH("run_dash"), + WEB_CONNECTIVITY("web_connectivity"), + RUN_NDT("run_ndt"), + TEST_PSIPHON("test_psiphon"), + TEST_TOR("test_tor"), + PROXY_PROTOCOL("proxy_protocol"), + TEST_TELEGRAM("test_telegram"), + RUN_HTTP_HEADER_FIELD_MANIPULATION("run_http_header_field_manipulation"), + EXPERIMENTAL("experimental"), + TEST_WHATSAPP("test_whatsapp"), + + ROUTE("route"), +} diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/PreferenceRepository.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/PreferenceRepository.kt index 87161d27..46c9f98e 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/PreferenceRepository.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/PreferenceRepository.kt @@ -1,5 +1,6 @@ package org.ooni.probe.data.repositories +import androidx.annotation.VisibleForTesting import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey @@ -9,6 +10,19 @@ import androidx.datastore.preferences.core.stringPreferencesKey import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map +import org.ooni.probe.data.models.SettingsKey + +sealed class PreferenceKey(val preferenceKey: Preferences.Key) { + class IntKey(preferenceKey: Preferences.Key) : PreferenceKey(preferenceKey) + + class StringKey(preferenceKey: Preferences.Key) : PreferenceKey(preferenceKey) + + class BooleanKey(preferenceKey: Preferences.Key) : PreferenceKey(preferenceKey) + + class FloatKey(preferenceKey: Preferences.Key) : PreferenceKey(preferenceKey) + + class LongKey(preferenceKey: Preferences.Key) : PreferenceKey(preferenceKey) +} class PreferenceRepository( private val dataStore: DataStore, @@ -27,6 +41,7 @@ class PreferenceRepository( * @param autoRun If the preference is for auto run * @return The preference key */ + @VisibleForTesting fun getPreferenceKey( name: String, prefix: String? = null, @@ -35,19 +50,21 @@ class PreferenceRepository( return "${prefix?.let { "${it}_" } ?: ""}$name${if (autoRun) "_autorun" else ""}" } - fun preferenceKeyFromSettingsKey( + private fun preferenceKeyFromSettingsKey( key: SettingsKey, prefix: String? = null, autoRun: Boolean = false, - ): Preferences.Key<*> { + ): PreferenceKey<*> { val preferenceKey = getPreferenceKey(name = key.value, prefix = prefix, autoRun = autoRun) return when (key) { - SettingsKey.MAX_RUNTIME -> intPreferencesKey(preferenceKey) - SettingsKey.PROXY_PORT -> intPreferencesKey(preferenceKey) - SettingsKey.PROXY_HOSTNAME -> stringPreferencesKey(preferenceKey) - SettingsKey.PROXY_PROTOCOL -> stringPreferencesKey(preferenceKey) - SettingsKey.LANGUAGE_SETTING -> stringPreferencesKey(preferenceKey) - else -> booleanPreferencesKey(preferenceKey) + SettingsKey.MAX_RUNTIME, + SettingsKey.PROXY_PORT, + -> PreferenceKey.IntKey(intPreferencesKey(preferenceKey)) + SettingsKey.PROXY_HOSTNAME, + SettingsKey.PROXY_PROTOCOL, + SettingsKey.LANGUAGE_SETTING, + -> PreferenceKey.StringKey(stringPreferencesKey(preferenceKey)) + else -> PreferenceKey.BooleanKey(booleanPreferencesKey(preferenceKey)) } } @@ -57,123 +74,45 @@ class PreferenceRepository( autoRun: Boolean = false, ): Flow> = dataStore.data.map { - keys.map { key -> key to it[preferenceKeyFromSettingsKey(key, prefix, autoRun)] }.toMap() + keys.map { key -> key to it[preferenceKeyFromSettingsKey(key, prefix, autoRun).preferenceKey] }.toMap() } - fun getValueByKey(key: Preferences.Key): Flow { - return dataStore.data.map { it[key] } + fun getValueByKey(key: SettingsKey): Flow { + return dataStore.data.map { + when (val preferenceKey = preferenceKeyFromSettingsKey(key)) { + is PreferenceKey.IntKey -> it[preferenceKey.preferenceKey] + is PreferenceKey.StringKey -> it[preferenceKey.preferenceKey] + is PreferenceKey.BooleanKey -> it[preferenceKey.preferenceKey] + is PreferenceKey.FloatKey -> it[preferenceKey.preferenceKey] + is PreferenceKey.LongKey -> it[preferenceKey.preferenceKey] + } + } } suspend fun setValueByKey( - key: Preferences.Key, + key: SettingsKey, value: T, ) { - dataStore.edit { it[key] = value } + dataStore.edit { + when (val preferenceKey = preferenceKeyFromSettingsKey(key)) { + is PreferenceKey.IntKey -> it[preferenceKey.preferenceKey] = value as Int + is PreferenceKey.StringKey -> it[preferenceKey.preferenceKey] = value as String + is PreferenceKey.BooleanKey -> it[preferenceKey.preferenceKey] = value as Boolean + is PreferenceKey.FloatKey -> it[preferenceKey.preferenceKey] = value as Float + is PreferenceKey.LongKey -> it[preferenceKey.preferenceKey] = value as Long + } + } } suspend fun clear() { dataStore.edit { it.clear() } } - suspend fun remove(key: Preferences.Key<*>) { - dataStore.edit { it.remove(key) } + suspend fun remove(key: SettingsKey) { + dataStore.edit { it.remove(preferenceKeyFromSettingsKey(key).preferenceKey) } } - suspend fun contains(key: Preferences.Key<*>): Boolean { - return dataStore.data.map { it.contains(key) }.firstOrNull() ?: false + suspend fun contains(key: SettingsKey): Boolean { + return dataStore.data.map { it.contains(preferenceKeyFromSettingsKey(key).preferenceKey) }.firstOrNull() ?: false } } - -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"), -} - -enum class SettingsKey(val value: String) { - // Notifications - NOTIFICATIONS_ENABLED("notifications_enabled"), - - // Test Options - AUTOMATED_TESTING_ENABLED("automated_testing_enabled"), - AUTOMATED_TESTING_WIFIONLY("automated_testing_wifionly"), - AUTOMATED_TESTING_CHARGING("automated_testing_charging"), - MAX_RUNTIME_ENABLED("max_runtime_enabled"), - MAX_RUNTIME("max_runtime"), - - // Website categories - SRCH("SRCH"), - PORN("PORN"), - COMM("COMM"), - COMT("COMT"), - MMED("MMED"), - HATE("HATE"), - POLR("POLR"), - PUBH("PUBH"), - GAME("GAME"), - PROV("PROV"), - HACK("HACK"), - MILX("MILX"), - DATE("DATE"), - ANON("ANON"), - ALDR("ALDR"), - GMB("GMB"), - XED("XED"), - REL("REL"), - GRP("GRP"), - GOVT("GOVT"), - ECON("ECON"), - LGBT("LGBT"), - FILE("FILE"), - HOST("HOST"), - HUMR("HUMR"), - NEWS("NEWS"), - ENV("ENV"), - CULTR("CULTR"), - CTRL("CTRL"), - IGO("IGO"), - - // Privacy - UPLOAD_RESULTS("upload_results"), - SEND_CRASH("send_crash"), - - // Proxy - PROXY_HOSTNAME("proxy_hostname"), - PROXY_PORT("proxy_port"), - - // Advanced - THEME_ENABLED("theme_enabled"), - LANGUAGE_SETTING("language_setting"), - DEBUG_LOGS("debugLogs"), - WARN_VPN_IN_USE("warn_vpn_in_use"), - STORAGE_SIZE("storage_size"), // purely decorative - - // MISC - DELETE_UPLOADED_JSONS("deleteUploadedJsons"), - IS_NOTIFICATION_DIALOG("isNotificationDialog"), - FIRST_RUN("first_run"), - - // Run Tests - TEST_SIGNAL("test_signal"), - RUN_HTTP_INVALID_REQUEST_LINE("run_http_invalid_request_line"), - TEST_FACEBOOK_MESSENGER("test_facebook_messenger"), - RUN_DASH("run_dash"), - WEB_CONNECTIVITY("web_connectivity"), - RUN_NDT("run_ndt"), - TEST_PSIPHON("test_psiphon"), - TEST_TOR("test_tor"), - PROXY_PROTOCOL("proxy_protocol"), - TEST_TELEGRAM("test_telegram"), - RUN_HTTP_HEADER_FIELD_MANIPULATION("run_http_header_field_manipulation"), - EXPERIMENTAL("experimental"), - TEST_WHATSAPP("test_whatsapp"), - - ROUTE("route"), -} 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 beaaf7e9..4b70e537 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt @@ -15,8 +15,9 @@ import org.ooni.engine.NetworkTypeFinder import org.ooni.engine.OonimkallBridge import org.ooni.engine.TaskEventMapper import org.ooni.probe.Database +import org.ooni.probe.data.models.PreferenceCategoryKey import org.ooni.probe.data.models.ResultModel -import org.ooni.probe.data.repositories.PreferenceCategoryKey +import org.ooni.probe.data.models.SettingsCategoryItem import org.ooni.probe.data.repositories.PreferenceRepository import org.ooni.probe.data.repositories.ResultRepository import org.ooni.probe.data.repositories.TestDescriptorRepository @@ -30,7 +31,6 @@ import org.ooni.probe.shared.PlatformInfo import org.ooni.probe.ui.dashboard.DashboardViewModel import org.ooni.probe.ui.result.ResultViewModel import org.ooni.probe.ui.results.ResultsViewModel -import org.ooni.probe.ui.settings.SettingsCategoryItem import org.ooni.probe.ui.settings.SettingsViewModel import org.ooni.probe.ui.settings.category.SettingsCategoryViewModel 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 523e6dd3..23413d3f 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 @@ -9,13 +9,13 @@ import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable +import org.ooni.probe.data.models.PreferenceCategoryKey import org.ooni.probe.data.models.ResultModel -import org.ooni.probe.data.repositories.PreferenceCategoryKey +import org.ooni.probe.data.models.SettingsCategoryItem import org.ooni.probe.di.Dependencies import org.ooni.probe.ui.dashboard.DashboardScreen import org.ooni.probe.ui.result.ResultScreen import org.ooni.probe.ui.results.ResultsScreen -import org.ooni.probe.ui.settings.SettingsCategoryItem import org.ooni.probe.ui.settings.SettingsScreen import org.ooni.probe.ui.settings.category.SettingsCategoryScreen diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Screen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Screen.kt index 911b4866..7bb2450a 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Screen.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Screen.kt @@ -2,8 +2,8 @@ package org.ooni.probe.ui.navigation import androidx.navigation.NavType import androidx.navigation.navArgument +import org.ooni.probe.data.models.PreferenceCategoryKey import org.ooni.probe.data.models.ResultModel -import org.ooni.probe.data.repositories.PreferenceCategoryKey sealed class Screen( val route: String, diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsScreen.kt index 0c81d3be..afa7580c 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsScreen.kt @@ -11,136 +11,13 @@ import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import ooniprobe.composeapp.generated.resources.CategoryCode_ALDR_Description -import ooniprobe.composeapp.generated.resources.CategoryCode_ALDR_Name -import ooniprobe.composeapp.generated.resources.CategoryCode_ANON_Description -import ooniprobe.composeapp.generated.resources.CategoryCode_ANON_Name -import ooniprobe.composeapp.generated.resources.CategoryCode_COMM_Description -import ooniprobe.composeapp.generated.resources.CategoryCode_COMM_Name -import ooniprobe.composeapp.generated.resources.CategoryCode_COMT_Description -import ooniprobe.composeapp.generated.resources.CategoryCode_COMT_Name -import ooniprobe.composeapp.generated.resources.CategoryCode_CTRL_Description -import ooniprobe.composeapp.generated.resources.CategoryCode_CTRL_Name -import ooniprobe.composeapp.generated.resources.CategoryCode_CULTR_Description -import ooniprobe.composeapp.generated.resources.CategoryCode_CULTR_Name -import ooniprobe.composeapp.generated.resources.CategoryCode_DATE_Description -import ooniprobe.composeapp.generated.resources.CategoryCode_DATE_Name -import ooniprobe.composeapp.generated.resources.CategoryCode_ECON_Description -import ooniprobe.composeapp.generated.resources.CategoryCode_ECON_Name -import ooniprobe.composeapp.generated.resources.CategoryCode_ENV_Description -import ooniprobe.composeapp.generated.resources.CategoryCode_ENV_Name -import ooniprobe.composeapp.generated.resources.CategoryCode_FILE_Description -import ooniprobe.composeapp.generated.resources.CategoryCode_FILE_Name -import ooniprobe.composeapp.generated.resources.CategoryCode_GAME_Description -import ooniprobe.composeapp.generated.resources.CategoryCode_GAME_Name -import ooniprobe.composeapp.generated.resources.CategoryCode_GMB_Description -import ooniprobe.composeapp.generated.resources.CategoryCode_GMB_Name -import ooniprobe.composeapp.generated.resources.CategoryCode_GOVT_Description -import ooniprobe.composeapp.generated.resources.CategoryCode_GOVT_Name -import ooniprobe.composeapp.generated.resources.CategoryCode_GRP_Description -import ooniprobe.composeapp.generated.resources.CategoryCode_GRP_Name -import ooniprobe.composeapp.generated.resources.CategoryCode_HACK_Description -import ooniprobe.composeapp.generated.resources.CategoryCode_HACK_Name -import ooniprobe.composeapp.generated.resources.CategoryCode_HATE_Description -import ooniprobe.composeapp.generated.resources.CategoryCode_HATE_Name -import ooniprobe.composeapp.generated.resources.CategoryCode_HOST_Description -import ooniprobe.composeapp.generated.resources.CategoryCode_HOST_Name -import ooniprobe.composeapp.generated.resources.CategoryCode_HUMR_Description -import ooniprobe.composeapp.generated.resources.CategoryCode_HUMR_Name -import ooniprobe.composeapp.generated.resources.CategoryCode_IGO_Description -import ooniprobe.composeapp.generated.resources.CategoryCode_IGO_Name -import ooniprobe.composeapp.generated.resources.CategoryCode_LGBT_Description -import ooniprobe.composeapp.generated.resources.CategoryCode_LGBT_Name -import ooniprobe.composeapp.generated.resources.CategoryCode_MILX_Description -import ooniprobe.composeapp.generated.resources.CategoryCode_MILX_Name -import ooniprobe.composeapp.generated.resources.CategoryCode_MMED_Description -import ooniprobe.composeapp.generated.resources.CategoryCode_MMED_Name -import ooniprobe.composeapp.generated.resources.CategoryCode_NEWS_Description -import ooniprobe.composeapp.generated.resources.CategoryCode_NEWS_Name -import ooniprobe.composeapp.generated.resources.CategoryCode_POLR_Description -import ooniprobe.composeapp.generated.resources.CategoryCode_POLR_Name -import ooniprobe.composeapp.generated.resources.CategoryCode_PORN_Description -import ooniprobe.composeapp.generated.resources.CategoryCode_PORN_Name -import ooniprobe.composeapp.generated.resources.CategoryCode_PROV_Description -import ooniprobe.composeapp.generated.resources.CategoryCode_PROV_Name -import ooniprobe.composeapp.generated.resources.CategoryCode_PUBH_Description -import ooniprobe.composeapp.generated.resources.CategoryCode_PUBH_Name -import ooniprobe.composeapp.generated.resources.CategoryCode_REL_Description -import ooniprobe.composeapp.generated.resources.CategoryCode_REL_Name -import ooniprobe.composeapp.generated.resources.CategoryCode_SRCH_Description -import ooniprobe.composeapp.generated.resources.CategoryCode_SRCH_Name -import ooniprobe.composeapp.generated.resources.CategoryCode_XED_Description -import ooniprobe.composeapp.generated.resources.CategoryCode_XED_Name -import ooniprobe.composeapp.generated.resources.Modal_EnableNotifications_Paragraph import ooniprobe.composeapp.generated.resources.Res -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_LanguageSettings_Title -import ooniprobe.composeapp.generated.resources.Settings_Advanced_RecentLogs -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_Footer -import ooniprobe.composeapp.generated.resources.Settings_AutomatedTesting_RunAutomatically_WiFiOnly -import ooniprobe.composeapp.generated.resources.Settings_Notifications_Enabled -import ooniprobe.composeapp.generated.resources.Settings_Notifications_Label -import ooniprobe.composeapp.generated.resources.Settings_Privacy_Label -import ooniprobe.composeapp.generated.resources.Settings_Privacy_SendCrashReports -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_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_MaxRuntime -import ooniprobe.composeapp.generated.resources.Settings_Websites_MaxRuntimeEnabled -import ooniprobe.composeapp.generated.resources.advanced -import ooniprobe.composeapp.generated.resources.category_aldr -import ooniprobe.composeapp.generated.resources.category_anon -import ooniprobe.composeapp.generated.resources.category_comm -import ooniprobe.composeapp.generated.resources.category_comt -import ooniprobe.composeapp.generated.resources.category_ctrl -import ooniprobe.composeapp.generated.resources.category_cultr -import ooniprobe.composeapp.generated.resources.category_date -import ooniprobe.composeapp.generated.resources.category_econ -import ooniprobe.composeapp.generated.resources.category_env -import ooniprobe.composeapp.generated.resources.category_file -import ooniprobe.composeapp.generated.resources.category_game -import ooniprobe.composeapp.generated.resources.category_gmb -import ooniprobe.composeapp.generated.resources.category_govt -import ooniprobe.composeapp.generated.resources.category_grp -import ooniprobe.composeapp.generated.resources.category_hack -import ooniprobe.composeapp.generated.resources.category_hate -import ooniprobe.composeapp.generated.resources.category_host -import ooniprobe.composeapp.generated.resources.category_humr -import ooniprobe.composeapp.generated.resources.category_igo -import ooniprobe.composeapp.generated.resources.category_lgbt -import ooniprobe.composeapp.generated.resources.category_milx -import ooniprobe.composeapp.generated.resources.category_mmed -import ooniprobe.composeapp.generated.resources.category_news -import ooniprobe.composeapp.generated.resources.category_polr -import ooniprobe.composeapp.generated.resources.category_porn -import ooniprobe.composeapp.generated.resources.category_prov -import ooniprobe.composeapp.generated.resources.category_pubh -import ooniprobe.composeapp.generated.resources.category_rel -import ooniprobe.composeapp.generated.resources.category_srch -import ooniprobe.composeapp.generated.resources.category_xed -import ooniprobe.composeapp.generated.resources.ic_settings -import ooniprobe.composeapp.generated.resources.notifications -import ooniprobe.composeapp.generated.resources.outline_info -import ooniprobe.composeapp.generated.resources.privacy -import ooniprobe.composeapp.generated.resources.proxy -import ooniprobe.composeapp.generated.resources.send_email import ooniprobe.composeapp.generated.resources.settings import org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource -import org.ooni.probe.data.repositories.PreferenceCategoryKey -import org.ooni.probe.data.repositories.SettingsKey -import org.ooni.probe.ui.settings.category.SettingsDescription +import org.ooni.probe.data.models.SettingsCategoryItem @Composable fun SettingsScreen(onNavigateToSettingsCategory: (SettingsViewModel.Event) -> Unit) { @@ -186,419 +63,3 @@ fun SettingsItemView( modifier = modifier, ) } - -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, -) - -data class SettingsItem( - override val icon: DrawableResource? = null, - override val title: StringResource, - override val type: PreferenceItemType, - override val key: SettingsKey, - val children: List? = emptyList(), - override val supportingContent: - @Composable() - (() -> Unit)? = null, -) : PreferenceItem(title = title, icon = icon, supportingContent = supportingContent, type = type, key = key) - -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, - ) { - fun routeToSettingsCategory() = SettingsViewModel.Event.SettingsCategoryClick(route) - - companion object { - private val seeRecentLogsCategory = - SettingsCategoryItem( - title = Res.string.Settings_Advanced_RecentLogs, - route = PreferenceCategoryKey.SEE_RECENT_LOGS, - ) - private val webCategory = - SettingsCategoryItem( - title = Res.string.Settings_Websites_Categories_Label, - route = PreferenceCategoryKey.WEBSITES_CATEGORIES, - supportingContent = { - // TODO(norbel): add enabled categories - Text(stringResource(Res.string.Settings_Websites_Categories_Description)) - }, - settings = - listOf( - SettingsItem( - icon = Res.drawable.category_anon, - title = Res.string.CategoryCode_ANON_Name, - supportingContent = { - Text(stringResource(Res.string.CategoryCode_ANON_Description)) - }, - key = SettingsKey.ANON, - type = PreferenceItemType.SWITCH, - ), - SettingsItem( - icon = Res.drawable.category_comt, - title = Res.string.CategoryCode_COMT_Name, - supportingContent = { - Text(stringResource(Res.string.CategoryCode_COMT_Description)) - }, - key = SettingsKey.COMT, - type = PreferenceItemType.SWITCH, - ), - SettingsItem( - icon = Res.drawable.category_ctrl, - title = Res.string.CategoryCode_CTRL_Name, - supportingContent = { - Text(stringResource(Res.string.CategoryCode_CTRL_Description)) - }, - key = SettingsKey.CTRL, - type = PreferenceItemType.SWITCH, - ), - SettingsItem( - icon = Res.drawable.category_cultr, - title = Res.string.CategoryCode_CULTR_Name, - supportingContent = { - Text(stringResource(Res.string.CategoryCode_CULTR_Description)) - }, - key = SettingsKey.CULTR, - type = PreferenceItemType.SWITCH, - ), - SettingsItem( - icon = Res.drawable.category_aldr, - title = Res.string.CategoryCode_ALDR_Name, - supportingContent = { - Text(stringResource(Res.string.CategoryCode_ALDR_Description)) - }, - key = SettingsKey.ALDR, - type = PreferenceItemType.SWITCH, - ), - SettingsItem( - icon = Res.drawable.category_comm, - title = Res.string.CategoryCode_COMM_Name, - supportingContent = { Text(stringResource(Res.string.CategoryCode_COMM_Description)) }, - key = SettingsKey.COMM, - type = PreferenceItemType.SWITCH, - ), - SettingsItem( - icon = Res.drawable.category_econ, - title = Res.string.CategoryCode_ECON_Name, - supportingContent = { Text(stringResource(Res.string.CategoryCode_ECON_Description)) }, - key = SettingsKey.ECON, - type = PreferenceItemType.SWITCH, - ), - SettingsItem( - icon = Res.drawable.category_env, - title = Res.string.CategoryCode_ENV_Name, - supportingContent = { Text(stringResource(Res.string.CategoryCode_ENV_Description)) }, - key = SettingsKey.ENV, - type = PreferenceItemType.SWITCH, - ), - SettingsItem( - icon = Res.drawable.category_file, - title = Res.string.CategoryCode_FILE_Name, - supportingContent = { Text(stringResource(Res.string.CategoryCode_FILE_Description)) }, - key = SettingsKey.FILE, - type = PreferenceItemType.SWITCH, - ), - SettingsItem( - icon = Res.drawable.category_gmb, - title = Res.string.CategoryCode_GMB_Name, - supportingContent = { Text(stringResource(Res.string.CategoryCode_GMB_Description)) }, - key = SettingsKey.GMB, - type = PreferenceItemType.SWITCH, - ), - SettingsItem( - icon = Res.drawable.category_game, - title = Res.string.CategoryCode_GAME_Name, - supportingContent = { Text(stringResource(Res.string.CategoryCode_GAME_Description)) }, - key = SettingsKey.GAME, - type = PreferenceItemType.SWITCH, - ), - SettingsItem( - icon = Res.drawable.category_govt, - title = Res.string.CategoryCode_GOVT_Name, - supportingContent = { Text(stringResource(Res.string.CategoryCode_GOVT_Description)) }, - key = SettingsKey.GOVT, - type = PreferenceItemType.SWITCH, - ), - SettingsItem( - icon = Res.drawable.category_hack, - title = Res.string.CategoryCode_HACK_Name, - supportingContent = { Text(stringResource(Res.string.CategoryCode_HACK_Description)) }, - key = SettingsKey.HACK, - type = PreferenceItemType.SWITCH, - ), - SettingsItem( - icon = Res.drawable.category_hate, - title = Res.string.CategoryCode_HATE_Name, - supportingContent = { Text(stringResource(Res.string.CategoryCode_HATE_Description)) }, - key = SettingsKey.HATE, - type = PreferenceItemType.SWITCH, - ), - SettingsItem( - icon = Res.drawable.category_host, - title = Res.string.CategoryCode_HOST_Name, - supportingContent = { Text(stringResource(Res.string.CategoryCode_HOST_Description)) }, - key = SettingsKey.HOST, - type = PreferenceItemType.SWITCH, - ), - SettingsItem( - icon = Res.drawable.category_humr, - title = Res.string.CategoryCode_HUMR_Name, - supportingContent = { Text(stringResource(Res.string.CategoryCode_HUMR_Description)) }, - key = SettingsKey.HUMR, - type = PreferenceItemType.SWITCH, - ), - SettingsItem( - icon = Res.drawable.category_igo, - title = Res.string.CategoryCode_IGO_Name, - supportingContent = { Text(stringResource(Res.string.CategoryCode_IGO_Description)) }, - key = SettingsKey.IGO, - type = PreferenceItemType.SWITCH, - ), - SettingsItem( - icon = Res.drawable.category_lgbt, - title = Res.string.CategoryCode_LGBT_Name, - supportingContent = { Text(stringResource(Res.string.CategoryCode_LGBT_Description)) }, - key = SettingsKey.LGBT, - type = PreferenceItemType.SWITCH, - ), - SettingsItem( - icon = Res.drawable.category_mmed, - title = Res.string.CategoryCode_MMED_Name, - supportingContent = { Text(stringResource(Res.string.CategoryCode_MMED_Description)) }, - key = SettingsKey.MMED, - type = PreferenceItemType.SWITCH, - ), - SettingsItem( - icon = Res.drawable.category_news, - title = Res.string.CategoryCode_NEWS_Name, - supportingContent = { Text(stringResource(Res.string.CategoryCode_NEWS_Description)) }, - key = SettingsKey.NEWS, - type = PreferenceItemType.SWITCH, - ), - SettingsItem( - icon = Res.drawable.category_date, - title = Res.string.CategoryCode_DATE_Name, - supportingContent = { Text(stringResource(Res.string.CategoryCode_DATE_Description)) }, - key = SettingsKey.DATE, - type = PreferenceItemType.SWITCH, - ), - SettingsItem( - icon = Res.drawable.category_polr, - title = Res.string.CategoryCode_POLR_Name, - supportingContent = { Text(stringResource(Res.string.CategoryCode_POLR_Description)) }, - key = SettingsKey.POLR, - type = PreferenceItemType.SWITCH, - ), - SettingsItem( - icon = Res.drawable.category_porn, - title = Res.string.CategoryCode_PORN_Name, - supportingContent = { Text(stringResource(Res.string.CategoryCode_PORN_Description)) }, - key = SettingsKey.PORN, - type = PreferenceItemType.SWITCH, - ), - SettingsItem( - icon = Res.drawable.category_prov, - title = Res.string.CategoryCode_PROV_Name, - supportingContent = { Text(stringResource(Res.string.CategoryCode_PROV_Description)) }, - key = SettingsKey.PROV, - type = PreferenceItemType.SWITCH, - ), - SettingsItem( - icon = Res.drawable.category_pubh, - title = Res.string.CategoryCode_PUBH_Name, - supportingContent = { Text(stringResource(Res.string.CategoryCode_PUBH_Description)) }, - key = SettingsKey.PUBH, - type = PreferenceItemType.SWITCH, - ), - SettingsItem( - icon = Res.drawable.category_rel, - title = Res.string.CategoryCode_REL_Name, - supportingContent = { Text(stringResource(Res.string.CategoryCode_REL_Description)) }, - key = SettingsKey.REL, - type = PreferenceItemType.SWITCH, - ), - SettingsItem( - icon = Res.drawable.category_srch, - title = Res.string.CategoryCode_SRCH_Name, - supportingContent = { Text(stringResource(Res.string.CategoryCode_SRCH_Description)) }, - key = SettingsKey.SRCH, - type = PreferenceItemType.SWITCH, - ), - SettingsItem( - icon = Res.drawable.category_xed, - title = Res.string.CategoryCode_XED_Name, - supportingContent = { Text(stringResource(Res.string.CategoryCode_XED_Description)) }, - key = SettingsKey.XED, - type = PreferenceItemType.SWITCH, - ), - SettingsItem( - icon = Res.drawable.category_grp, - title = Res.string.CategoryCode_GRP_Name, - supportingContent = { Text(stringResource(Res.string.CategoryCode_GRP_Description)) }, - key = SettingsKey.GRP, - type = PreferenceItemType.SWITCH, - ), - SettingsItem( - icon = Res.drawable.category_milx, - title = Res.string.CategoryCode_MILX_Name, - supportingContent = { Text(stringResource(Res.string.CategoryCode_MILX_Description)) }, - key = SettingsKey.MILX, - type = PreferenceItemType.SWITCH, - ), - ), - ) - - fun getSettingsItems() = - listOf( - SettingsCategoryItem( - icon = Res.drawable.notifications, - title = Res.string.Settings_Notifications_Label, - route = PreferenceCategoryKey.NOTIFICATIONS, - settings = - listOf( - SettingsItem( - title = Res.string.Settings_Notifications_Enabled, - key = SettingsKey.NOTIFICATIONS_ENABLED, - type = PreferenceItemType.SWITCH, - ), - ), - footerContent = { - SettingsDescription( - Res.string.Modal_EnableNotifications_Paragraph, - ) - }, - ), - SettingsCategoryItem( - icon = Res.drawable.ic_settings, - title = Res.string.Settings_TestOptions_Label, - route = PreferenceCategoryKey.TEST_OPTIONS, - settings = - listOf( - SettingsItem( - title = Res.string.Settings_AutomatedTesting_RunAutomatically, - key = SettingsKey.AUTOMATED_TESTING_ENABLED, - type = PreferenceItemType.SWITCH, - ), - SettingsItem( - title = Res.string.Settings_AutomatedTesting_RunAutomatically_WiFiOnly, - key = SettingsKey.AUTOMATED_TESTING_WIFIONLY, - type = PreferenceItemType.SWITCH, - ), - SettingsItem( - title = Res.string.Settings_AutomatedTesting_RunAutomatically_ChargingOnly, - key = SettingsKey.AUTOMATED_TESTING_CHARGING, - type = PreferenceItemType.SWITCH, - ), - webCategory, - 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.TEXT, - ), - ), - footerContent = { - SettingsDescription( - Res.string.Settings_AutomatedTesting_RunAutomatically_Footer, - ) - }, - ), - SettingsCategoryItem( - icon = Res.drawable.privacy, - title = Res.string.Settings_Privacy_Label, - route = PreferenceCategoryKey.PRIVACY, - settings = - listOf( - SettingsItem( - title = Res.string.Settings_Sharing_UploadResults, - key = SettingsKey.UPLOAD_RESULTS, - type = PreferenceItemType.SWITCH, - ), - SettingsItem( - title = Res.string.Settings_Privacy_SendCrashReports, - key = SettingsKey.SEND_CRASH, - type = PreferenceItemType.SWITCH, - ), - ), - ), - SettingsCategoryItem( - icon = Res.drawable.proxy, - title = Res.string.Settings_Proxy_Label, - route = PreferenceCategoryKey.PROXY, - ), - SettingsCategoryItem( - icon = Res.drawable.advanced, - title = Res.string.Settings_Advanced_Label, - route = PreferenceCategoryKey.ADVANCED, - settings = - listOf( - SettingsItem( - title = Res.string.Settings_Advanced_LanguageSettings_Title, - key = SettingsKey.LANGUAGE_SETTING, - type = PreferenceItemType.SELECT, - ), - seeRecentLogsCategory, - SettingsItem( - title = Res.string.Settings_Advanced_DebugLogs, - key = SettingsKey.DEBUG_LOGS, - type = PreferenceItemType.SWITCH, - ), - SettingsItem( - title = Res.string.Settings_Storage_Label, - key = SettingsKey.STORAGE_SIZE, - type = PreferenceItemType.BUTTON, - ), - SettingsItem( - title = Res.string.Settings_WarmVPNInUse_Label, - key = SettingsKey.WARN_VPN_IN_USE, - type = PreferenceItemType.SWITCH, - ), - ), - ), - SettingsCategoryItem( - icon = Res.drawable.send_email, - title = Res.string.Settings_SendEmail_Label, - route = PreferenceCategoryKey.SEND_EMAIL, - ), - SettingsCategoryItem( - icon = Res.drawable.outline_info, - title = Res.string.Settings_About_Label, - route = PreferenceCategoryKey.ABOUT_OONI, - ), - ) - - fun getSettingsItem(route: PreferenceCategoryKey) = - (getSettingsItems() + listOf(webCategory, seeRecentLogsCategory)).first { - it.route == route - } - } -} - -enum class PreferenceItemType { - SWITCH, - TEXT, - BUTTON, - SELECT, - ROUTE, -} diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsViewModel.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsViewModel.kt index 7549e115..0be5832c 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsViewModel.kt @@ -6,7 +6,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import org.ooni.probe.data.repositories.PreferenceCategoryKey +import org.ooni.probe.data.models.PreferenceCategoryKey open class SettingsViewModel( goToSettingsForCategory: (PreferenceCategoryKey) -> Unit, 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 5c65e578..38ae604d 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 @@ -29,10 +29,10 @@ import ooniprobe.composeapp.generated.resources.settings import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource -import org.ooni.probe.data.repositories.PreferenceCategoryKey -import org.ooni.probe.data.repositories.SettingsKey -import org.ooni.probe.ui.settings.PreferenceItemType -import org.ooni.probe.ui.settings.SettingsCategoryItem +import org.ooni.probe.data.models.PreferenceCategoryKey +import org.ooni.probe.data.models.PreferenceItemType +import org.ooni.probe.data.models.SettingsCategoryItem +import org.ooni.probe.data.models.SettingsKey @Composable fun SettingsCategoryScreen( diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryViewModel.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryViewModel.kt index e068da9b..c58c23fc 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryViewModel.kt @@ -1,6 +1,5 @@ package org.ooni.probe.ui.settings.category -import androidx.datastore.preferences.core.Preferences import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableSharedFlow @@ -10,10 +9,10 @@ import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update -import org.ooni.probe.data.repositories.PreferenceCategoryKey +import org.ooni.probe.data.models.PreferenceCategoryKey +import org.ooni.probe.data.models.SettingsCategoryItem +import org.ooni.probe.data.models.SettingsKey import org.ooni.probe.data.repositories.PreferenceRepository -import org.ooni.probe.data.repositories.SettingsKey -import org.ooni.probe.ui.settings.SettingsCategoryItem class SettingsCategoryViewModel( preferenceManager: PreferenceRepository, @@ -37,17 +36,10 @@ class SettingsCategoryViewModel( .onEach { goToSettingsForCategory(it.category) }.launchIn(viewModelScope) events.filterIsInstance().onEach { - (preferenceManager.preferenceKeyFromSettingsKey(it.key) as? Preferences.Key)?.let { key -> - preferenceManager.setValueByKey( - key, - it.value, - ) - } - _state.update { state -> - state.copy( - preference = state.preference?.plus(it.key to it.value), - ) - } + preferenceManager.setValueByKey( + key = it.key, + value = it.value, + ) }.launchIn(viewModelScope) events.filterIsInstance().onEach { onBack() }.launchIn(viewModelScope) diff --git a/composeApp/src/commonTest/kotlin/org/ooni/probe/data/repositories/PreferenceRepositoryTest.kt b/composeApp/src/commonTest/kotlin/org/ooni/probe/data/repositories/PreferenceRepositoryTest.kt index cd2d4457..d87370f1 100644 --- a/composeApp/src/commonTest/kotlin/org/ooni/probe/data/repositories/PreferenceRepositoryTest.kt +++ b/composeApp/src/commonTest/kotlin/org/ooni/probe/data/repositories/PreferenceRepositoryTest.kt @@ -1,8 +1,8 @@ package org.ooni.probe.data.repositories -import androidx.datastore.preferences.core.Preferences import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest +import org.ooni.probe.data.models.SettingsKey import org.ooni.testing.createPreferenceDataStore import kotlin.test.AfterTest import kotlin.test.BeforeTest @@ -27,12 +27,8 @@ class PreferenceRepositoryTest { @Test fun testAllSettings() = runTest { - val key: Preferences.Key = - preferenceRepository.preferenceKeyFromSettingsKey( - SettingsKey.LANGUAGE_SETTING, - ) as Preferences.Key val value = "value" - preferenceRepository.setValueByKey(key, value) + preferenceRepository.setValueByKey(SettingsKey.LANGUAGE_SETTING, value) val setting: Map = preferenceRepository.allSettings(listOf(SettingsKey.LANGUAGE_SETTING)).first() assertEquals(value, setting.values.first()) } @@ -67,47 +63,42 @@ class PreferenceRepositoryTest { @Test fun testGetValueByKey() = runTest { - val key = preferenceRepository.preferenceKeyFromSettingsKey(SettingsKey.LANGUAGE_SETTING) as Preferences.Key val value = "value" - preferenceRepository.setValueByKey(key, value) - assertEquals(value, preferenceRepository.getValueByKey(key = key).first()) + preferenceRepository.setValueByKey(SettingsKey.LANGUAGE_SETTING, value) + assertEquals(value, preferenceRepository.getValueByKey(key = SettingsKey.LANGUAGE_SETTING).first()) } @Test fun testSetValueByKey() = runTest { - val key = preferenceRepository.preferenceKeyFromSettingsKey(SettingsKey.LANGUAGE_SETTING) as Preferences.Key val value = "value" - preferenceRepository.setValueByKey(key, value) - assertEquals(value, preferenceRepository.getValueByKey(key).first()) + preferenceRepository.setValueByKey(SettingsKey.LANGUAGE_SETTING, value) + assertEquals(value, preferenceRepository.getValueByKey(SettingsKey.LANGUAGE_SETTING).first()) } @Test fun testClear() = runTest { - val key = preferenceRepository.preferenceKeyFromSettingsKey(SettingsKey.LANGUAGE_SETTING) as Preferences.Key val value = "value" - preferenceRepository.setValueByKey(key, value) + preferenceRepository.setValueByKey(SettingsKey.LANGUAGE_SETTING, value) preferenceRepository.clear() - assertNull(preferenceRepository.getValueByKey(key).first()) + assertNull(preferenceRepository.getValueByKey(SettingsKey.LANGUAGE_SETTING).first()) } @Test fun testRemove() = runTest { - val key = preferenceRepository.preferenceKeyFromSettingsKey(SettingsKey.LANGUAGE_SETTING) as Preferences.Key val value = "value" - preferenceRepository.setValueByKey(key, value) - preferenceRepository.remove(key) - assertNull(preferenceRepository.getValueByKey(key).first()) + preferenceRepository.setValueByKey(SettingsKey.LANGUAGE_SETTING, value) + preferenceRepository.remove(SettingsKey.LANGUAGE_SETTING) + assertNull(preferenceRepository.getValueByKey(SettingsKey.LANGUAGE_SETTING).first()) } @Test fun testContains() = runTest { - val key = preferenceRepository.preferenceKeyFromSettingsKey(SettingsKey.LANGUAGE_SETTING) as Preferences.Key val value = "value" - preferenceRepository.setValueByKey(key, value) - assertEquals(true, preferenceRepository.contains(key)) + preferenceRepository.setValueByKey(SettingsKey.LANGUAGE_SETTING, value) + assertEquals(true, preferenceRepository.contains(SettingsKey.LANGUAGE_SETTING)) } } diff --git a/composeApp/src/iosTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt b/composeApp/src/iosTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt index 393f4a9a..3dc472fd 100644 --- a/composeApp/src/iosTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt +++ b/composeApp/src/iosTest/kotlin/org/ooni/testing/CreatePreferenceDataStore.kt @@ -8,7 +8,6 @@ import platform.Foundation.NSFileManager import platform.Foundation.NSURL import platform.Foundation.NSUserDomainMask -@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) internal actual fun createPreferenceDataStore(): DataStore { return Dependencies.getDataStore( producePath = {