diff --git a/app/src/main/java/com/aurora/store/compose/ui/preferences/SettingsScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/preferences/SettingsScreen.kt new file mode 100644 index 000000000..a623e2a77 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/preferences/SettingsScreen.kt @@ -0,0 +1,124 @@ +/* + * SPDX-FileCopyrightText: 2025 The Calyx Institute + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.ui.preferences + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewWrapper +import com.aurora.store.R +import com.aurora.store.compose.composable.TopAppBar +import com.aurora.store.compose.navigation.Destination +import com.aurora.store.compose.preview.ThemePreviewProvider +import com.aurora.store.data.model.PermissionType + +@Composable +fun SettingsScreen(onNavigateTo: (Destination) -> Unit) { + ScreenContent(onNavigateTo = onNavigateTo) +} + +@Composable +private fun ScreenContent(onNavigateTo: (Destination) -> Unit = {}) { + Scaffold( + topBar = { + TopAppBar( + title = stringResource(R.string.title_settings) + ) + } + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize() + ) { + item { + ListItem( + modifier = Modifier.clickable { + onNavigateTo( + Destination.PermissionRationale(PermissionType.entries.toSet()) + ) + }, + leadingContent = { + Icon( + painter = painterResource(R.drawable.ic_list_check), + contentDescription = null + ) + }, + headlineContent = { + Text(stringResource(R.string.onboarding_title_permissions)) + } + ) + } + item { + ListItem( + modifier = Modifier.clickable { + onNavigateTo(Destination.InstallationPreference) + }, + leadingContent = { + Icon( + painter = painterResource(R.drawable.ic_installation), + contentDescription = null + ) + }, + headlineContent = { Text(stringResource(R.string.title_installation)) } + ) + } + item { + ListItem( + modifier = Modifier.clickable { onNavigateTo(Destination.UIPreference) }, + leadingContent = { + Icon( + painter = painterResource(R.drawable.ic_ui), + contentDescription = null + ) + }, + headlineContent = { Text(stringResource(R.string.pref_ui_title)) } + ) + } + item { + ListItem( + modifier = Modifier.clickable { onNavigateTo(Destination.NetworkPreference) }, + leadingContent = { + Icon( + painter = painterResource(R.drawable.ic_network), + contentDescription = null + ) + }, + headlineContent = { Text(stringResource(R.string.pref_network_title)) } + ) + } + item { + ListItem( + modifier = Modifier.clickable { onNavigateTo(Destination.UpdatesPreference) }, + leadingContent = { + Icon( + painter = painterResource(R.drawable.ic_updates), + contentDescription = null + ) + }, + headlineContent = { Text(stringResource(R.string.title_updates)) } + ) + } + } + } +} + +@PreviewWrapper(ThemePreviewProvider::class) +@Preview +@Composable +private fun SettingsScreenPreview() { + ScreenContent() +} diff --git a/app/src/main/java/com/aurora/store/compose/ui/preferences/UIPreferenceScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/preferences/UIPreferenceScreen.kt new file mode 100644 index 000000000..872400775 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/preferences/UIPreferenceScreen.kt @@ -0,0 +1,178 @@ +/* + * SPDX-FileCopyrightText: 2025 The Calyx Institute + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.ui.preferences + +import android.content.Intent +import android.provider.Settings +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.ListItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLocale +import androidx.compose.ui.res.stringArrayResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewWrapper +import androidx.core.net.toUri +import com.aurora.extensions.isTAndAbove +import com.aurora.extensions.setAppTheme +import com.aurora.store.R +import com.aurora.store.compose.composable.TopAppBar +import com.aurora.store.compose.preview.ThemePreviewProvider +import com.aurora.store.compose.ui.preferences.network.SingleChoiceDialog +import com.aurora.store.util.Preferences +import com.aurora.store.util.Preferences.PREFERENCE_DEFAULT_SELECTED_TAB +import com.aurora.store.util.Preferences.PREFERENCE_FOR_YOU +import com.aurora.store.util.Preferences.PREFERENCE_THEME_STYLE +import com.aurora.store.util.save + +@Composable +fun UIPreferenceScreen() { + ScreenContent() +} + +@Composable +private fun ScreenContent() { + val context = LocalContext.current + + val themeEntries = stringArrayResource(R.array.pref_theme_style) + var themeStyle by remember { + mutableIntStateOf(Preferences.getInteger(context, PREFERENCE_THEME_STYLE)) + } + val tabEntries = stringArrayResource(R.array.pref_default_tab) + var selectedTab by remember { + mutableIntStateOf(Preferences.getInteger(context, PREFERENCE_DEFAULT_SELECTED_TAB)) + } + var forYou by remember { + mutableStateOf(Preferences.getBoolean(context, PREFERENCE_FOR_YOU, true)) + } + var showThemeDialog by remember { mutableStateOf(false) } + var showTabDialog by remember { mutableStateOf(false) } + + if (showThemeDialog) { + SingleChoiceDialog( + title = stringResource(R.string.pref_ui_theme), + options = themeEntries.toList(), + selected = themeStyle, + onSelect = { index -> + themeStyle = index + context.save(PREFERENCE_THEME_STYLE, index) + setAppTheme(index) + showThemeDialog = false + }, + onDismiss = { showThemeDialog = false } + ) + } + + if (showTabDialog) { + SingleChoiceDialog( + title = stringResource(R.string.pref_ui_layout_tab), + options = tabEntries.toList(), + selected = selectedTab, + onSelect = { index -> + selectedTab = index + context.save(PREFERENCE_DEFAULT_SELECTED_TAB, index) + showTabDialog = false + }, + onDismiss = { showTabDialog = false } + ) + } + + Scaffold( + topBar = { + TopAppBar( + title = stringResource(R.string.pref_ui_title) + ) + } + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize() + ) { + if (isTAndAbove) { + item { + ListItem( + modifier = Modifier.clickable { + context.startActivity( + Intent(Settings.ACTION_APP_LOCALE_SETTINGS).apply { + data = ("package:" + context.packageName).toUri() + } + ) + }, + headlineContent = { Text(stringResource(R.string.app_language)) }, + supportingContent = { Text(LocalLocale.current.platformLocale.displayName) } + ) + } + } + item { HorizontalDivider() } + item { + ListItem(headlineContent = { Text(stringResource(R.string.pref_ui_theme)) }) + } + item { + ListItem( + modifier = Modifier.clickable { showThemeDialog = true }, + headlineContent = { Text(stringResource(R.string.pref_ui_theme)) }, + supportingContent = { Text(themeEntries.getOrElse(themeStyle) { "" }) } + ) + } + item { HorizontalDivider() } + item { + ListItem(headlineContent = { Text(stringResource(R.string.pref_ui_layout)) }) + } + item { + ListItem( + modifier = Modifier.clickable { showTabDialog = true }, + headlineContent = { Text(stringResource(R.string.pref_ui_layout_tab)) }, + supportingContent = { Text(tabEntries.getOrElse(selectedTab) { "" }) } + ) + } + item { HorizontalDivider() } + item { + ListItem(headlineContent = { Text(stringResource(R.string.pref_common_extra)) }) + } + item { + ListItem( + modifier = Modifier.clickable { + forYou = !forYou + context.save(PREFERENCE_FOR_YOU, forYou) + }, + headlineContent = { Text(stringResource(R.string.pref_ui_no_for_you)) }, + supportingContent = { Text(stringResource(R.string.pref_ui_no_for_you_desc)) }, + trailingContent = { + Switch( + checked = forYou, + onCheckedChange = { checked -> + forYou = checked + context.save(PREFERENCE_FOR_YOU, checked) + } + ) + } + ) + } + } + } +} + +@PreviewWrapper(ThemePreviewProvider::class) +@Preview +@Composable +private fun UIPreferenceScreenPreview() { + ScreenContent() +} diff --git a/app/src/main/java/com/aurora/store/compose/ui/preferences/installation/InstallationPreferenceScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/preferences/installation/InstallationPreferenceScreen.kt new file mode 100644 index 000000000..1023f45b1 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/preferences/installation/InstallationPreferenceScreen.kt @@ -0,0 +1,156 @@ +/* + * SPDX-FileCopyrightText: 2025 The Calyx Institute + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.ui.preferences.installation + +import android.app.admin.DevicePolicyManager +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.ListItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewWrapper +import androidx.core.content.getSystemService +import com.aurora.store.R +import com.aurora.store.compose.composable.TopAppBar +import com.aurora.store.compose.navigation.Destination +import com.aurora.store.compose.preview.ThemePreviewProvider +import com.aurora.store.util.Preferences +import com.aurora.store.util.Preferences.PREFERENCE_AUTO_DELETE +import com.aurora.store.util.save + +@Composable +fun InstallationPreferenceScreen(onNavigateTo: (Destination) -> Unit) { + val context = LocalContext.current + val devicePolicyManager = context.getSystemService() + val isDeviceOwner = devicePolicyManager?.isDeviceOwnerApp(context.packageName) ?: false + + ScreenContent( + onNavigateTo = onNavigateTo, + isDeviceOwner = isDeviceOwner, + onClearDeviceOwner = { + @Suppress("DEPRECATION") + devicePolicyManager?.clearDeviceOwnerApp(context.packageName) + } + ) +} + +@Composable +private fun ScreenContent( + onNavigateTo: (Destination) -> Unit = {}, + isDeviceOwner: Boolean = false, + onClearDeviceOwner: () -> Unit = {} +) { + val context = LocalContext.current + var autoDelete by remember { + mutableStateOf(Preferences.getBoolean(context, PREFERENCE_AUTO_DELETE, true)) + } + var showClearOwnerDialog by remember { mutableStateOf(false) } + + if (showClearOwnerDialog) { + AlertDialog( + onDismissRequest = { showClearOwnerDialog = false }, + title = { Text(stringResource(R.string.pref_clear_device_owner_title)) }, + text = { Text(stringResource(R.string.pref_clear_device_owner_desc)) }, + confirmButton = { + TextButton(onClick = { + showClearOwnerDialog = false + onClearDeviceOwner() + }) { + Text(stringResource(android.R.string.ok)) + } + }, + dismissButton = { + TextButton(onClick = { showClearOwnerDialog = false }) { + Text(stringResource(android.R.string.cancel)) + } + } + ) + } + + Scaffold( + topBar = { + TopAppBar( + title = stringResource(R.string.title_installation) + ) + } + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize() + ) { + item { + ListItem( + modifier = Modifier.clickable { onNavigateTo(Destination.Installer) }, + headlineContent = { Text(stringResource(R.string.pref_install_mode_title)) }, + supportingContent = { Text(stringResource(R.string.pref_install_mode_summary)) } + ) + } + item { HorizontalDivider() } + item { + ListItem( + headlineContent = { Text(stringResource(R.string.pref_common_extra)) } + ) + } + item { + ListItem( + modifier = Modifier.clickable { + autoDelete = !autoDelete + context.save(PREFERENCE_AUTO_DELETE, autoDelete) + }, + headlineContent = { Text(stringResource(R.string.pref_install_delete_title)) }, + supportingContent = { + Text(stringResource(R.string.pref_install_delete_summary)) + }, + trailingContent = { + Switch( + checked = autoDelete, + onCheckedChange = { checked -> + autoDelete = checked + context.save(PREFERENCE_AUTO_DELETE, checked) + } + ) + } + ) + } + if (isDeviceOwner) { + item { + ListItem( + modifier = Modifier.clickable { showClearOwnerDialog = true }, + headlineContent = { + Text(stringResource(R.string.pref_clear_device_owner_title)) + }, + supportingContent = { + Text(stringResource(R.string.pref_clear_device_owner_summary)) + } + ) + } + } + } + } +} + +@PreviewWrapper(ThemePreviewProvider::class) +@Preview +@Composable +private fun InstallationPreferenceScreenPreview() { + ScreenContent() +} diff --git a/app/src/main/java/com/aurora/store/compose/ui/preferences/installation/InstallerScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/preferences/installation/InstallerScreen.kt index 2292fd4db..ef0cb9b62 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/preferences/installation/InstallerScreen.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/preferences/installation/InstallerScreen.kt @@ -35,7 +35,7 @@ import com.aurora.store.data.model.InstallerInfo import com.aurora.store.viewmodel.preferences.InstallerViewModel @Composable -fun InstallerScreen(onNavigateUp: () -> Unit, viewModel: InstallerViewModel = hiltViewModel()) { +fun InstallerScreen(viewModel: InstallerViewModel = hiltViewModel()) { val currentInstallerId by viewModel.currentInstaller.collectAsStateWithLifecycle() val snackBarHostState = remember { SnackbarHostState() } @@ -46,7 +46,6 @@ fun InstallerScreen(onNavigateUp: () -> Unit, viewModel: InstallerViewModel = hi } ScreenContent( - onNavigateUp = onNavigateUp, snackBarHostState = snackBarHostState, currentInstaller = Installer.entries[currentInstallerId], availableInstallers = AppInstaller.getAvailableInstallersInfo(LocalContext.current), @@ -56,7 +55,6 @@ fun InstallerScreen(onNavigateUp: () -> Unit, viewModel: InstallerViewModel = hi @Composable private fun ScreenContent( - onNavigateUp: () -> Unit = {}, snackBarHostState: SnackbarHostState = SnackbarHostState(), currentInstaller: Installer = Installer.SESSION, availableInstallers: List = emptyList(), @@ -70,8 +68,7 @@ private fun ScreenContent( }, topBar = { TopAppBar( - title = stringResource(R.string.pref_install_mode_title), - onNavigateUp = onNavigateUp + title = stringResource(R.string.pref_install_mode_title) ) } ) { paddingValues -> diff --git a/app/src/main/java/com/aurora/store/compose/ui/preferences/network/NetworkPreferenceScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/preferences/network/NetworkPreferenceScreen.kt new file mode 100644 index 000000000..cd0f0653d --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/preferences/network/NetworkPreferenceScreen.kt @@ -0,0 +1,316 @@ +/* + * SPDX-FileCopyrightText: 2025 The Calyx Institute + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.ui.preferences.network + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.ListItem +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.stringArrayResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewWrapper +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import com.aurora.store.R +import com.aurora.store.compose.composable.TopAppBar +import com.aurora.store.compose.navigation.Destination +import com.aurora.store.compose.preview.ThemePreviewProvider +import com.aurora.store.compose.ui.commons.ForceRestartDialog +import com.aurora.store.util.CommonUtil +import com.aurora.store.util.PackageUtil +import com.aurora.store.util.Preferences +import com.aurora.store.util.Preferences.PREFERENCE_MICROG_AUTH +import com.aurora.store.util.Preferences.PREFERENCE_PROXY_INFO +import com.aurora.store.util.Preferences.PREFERENCE_PROXY_URL +import com.aurora.store.util.Preferences.PREFERENCE_VENDING_VERSION +import com.aurora.store.util.remove +import com.aurora.store.util.save +import com.aurora.store.viewmodel.preferences.ProxyURLViewModel +import com.jakewharton.processphoenix.ProcessPhoenix + +@Composable +fun NetworkPreferenceScreen( + onNavigateTo: (Destination) -> Unit, + viewModel: ProxyURLViewModel = hiltViewModel() +) { + val context = LocalContext.current + val hasMicroG = PackageUtil.hasSupportedMicroGVariant(context) + + ScreenContent( + onNavigateTo = onNavigateTo, + hasMicroG = hasMicroG, + onSaveProxyUrl = { url -> + val proxyInfo = CommonUtil.parseProxyUrl(url) + if (proxyInfo != null) { + context.save(PREFERENCE_PROXY_URL, url) + context.save(PREFERENCE_PROXY_INFO, viewModel.json.encodeToString(proxyInfo)) + true + } else { + false + } + }, + onDeleteProxyUrl = { + Preferences.remove(context, PREFERENCE_PROXY_URL) + Preferences.remove(context, PREFERENCE_PROXY_INFO) + } + ) +} + +@Composable +private fun ScreenContent( + onNavigateTo: (Destination) -> Unit = {}, + hasMicroG: Boolean = false, + onSaveProxyUrl: (String) -> Boolean = { false }, + onDeleteProxyUrl: () -> Unit = {} +) { + val context = LocalContext.current + + val vendingEntries = stringArrayResource(R.array.pref_vending_version) + var vendingVersion by remember { + mutableIntStateOf(Preferences.getInteger(context, PREFERENCE_VENDING_VERSION)) + } + var microGAuth by remember { + mutableStateOf(Preferences.getBoolean(context, PREFERENCE_MICROG_AUTH, true)) + } + var showProxyDialog by remember { mutableStateOf(false) } + var showVendingDialog by remember { mutableStateOf(false) } + var showForceRestartDialog by remember { mutableStateOf(false) } + + if (showForceRestartDialog) { + ForceRestartDialog( + onConfirm = { ProcessPhoenix.triggerRebirth(context) }, + onDismiss = { showForceRestartDialog = false } + ) + } + + if (showProxyDialog) { + ProxyURLDialog( + currentUrl = Preferences.getString(context, PREFERENCE_PROXY_URL), + onSave = { url -> + showProxyDialog = false + if (onSaveProxyUrl(url)) { + showForceRestartDialog = true + } + }, + onDelete = { + showProxyDialog = false + onDeleteProxyUrl() + showForceRestartDialog = true + }, + onDismiss = { showProxyDialog = false } + ) + } + + if (showVendingDialog) { + SingleChoiceDialog( + title = stringResource(R.string.pref_vending_version_title), + options = vendingEntries.toList(), + selected = vendingVersion, + onSelect = { index -> + vendingVersion = index + context.save(PREFERENCE_VENDING_VERSION, index) + showVendingDialog = false + }, + onDismiss = { showVendingDialog = false } + ) + } + + Scaffold( + topBar = { + TopAppBar( + title = stringResource(R.string.pref_network_title) + ) + } + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize() + ) { + item { + ListItem( + modifier = Modifier.clickable { onNavigateTo(Destination.Dispenser) }, + headlineContent = { Text(stringResource(R.string.pref_dispenser_title)) }, + supportingContent = { Text(stringResource(R.string.pref_dispenser_summary)) } + ) + } + item { + ListItem( + modifier = Modifier.clickable { showProxyDialog = true }, + headlineContent = { Text(stringResource(R.string.pref_network_proxy_url)) }, + supportingContent = { Text(stringResource(R.string.pref_network_proxy_desc)) } + ) + } + item { HorizontalDivider() } + item { + ListItem(headlineContent = { Text(stringResource(R.string.pref_common_extra)) }) + } + if (hasMicroG) { + item { + ListItem( + modifier = Modifier.clickable { + microGAuth = !microGAuth + context.save(PREFERENCE_MICROG_AUTH, microGAuth) + }, + headlineContent = { + Text(stringResource(R.string.pref_network_microg_login_title)) + }, + supportingContent = { + Text(stringResource(R.string.pref_network_microg_login_desc)) + }, + trailingContent = { + Switch( + checked = microGAuth, + onCheckedChange = { checked -> + microGAuth = checked + context.save(PREFERENCE_MICROG_AUTH, checked) + } + ) + } + ) + } + } + item { + ListItem( + modifier = Modifier.clickable { showVendingDialog = true }, + headlineContent = { Text(stringResource(R.string.pref_vending_version_title)) }, + supportingContent = { Text(vendingEntries.getOrElse(vendingVersion) { "" }) } + ) + } + } + } +} + +@Composable +private fun ProxyURLDialog( + currentUrl: String, + onSave: (String) -> Unit, + onDelete: () -> Unit, + onDismiss: () -> Unit +) { + var url by remember { mutableStateOf(currentUrl) } + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.pref_network_proxy_url)) }, + text = { + Column { + Text( + text = stringResource(R.string.pref_network_proxy_url_message), + modifier = Modifier.padding(bottom = dimensionResource(R.dimen.padding_small)) + ) + OutlinedTextField( + value = url, + onValueChange = { url = it }, + label = { Text(stringResource(R.string.pref_network_proxy_url_hint)) }, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri), + modifier = Modifier.fillMaxWidth() + ) + } + }, + confirmButton = { + TextButton( + enabled = url.isNotBlank(), + onClick = { onSave(url.trim()) } + ) { + Text(stringResource(R.string.set)) + } + }, + dismissButton = { + Row { + if (currentUrl.isNotBlank()) { + TextButton(onClick = onDelete) { + Text(stringResource(R.string.disable)) + } + } + Spacer(Modifier.width(dimensionResource(R.dimen.margin_small))) + TextButton(onClick = onDismiss) { + Text(stringResource(android.R.string.cancel)) + } + } + } + ) +} + +@Composable +internal fun SingleChoiceDialog( + title: String, + options: List, + selected: Int, + onSelect: (Int) -> Unit, + onDismiss: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(title) }, + text = { + Column(modifier = Modifier.selectableGroup()) { + options.forEachIndexed { index, option -> + Row( + modifier = Modifier + .fillMaxWidth() + .selectable( + selected = index == selected, + onClick = { onSelect(index) }, + role = Role.RadioButton + ) + .padding(vertical = dimensionResource(R.dimen.padding_small)), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = index == selected, + onClick = null + ) + Spacer(Modifier.width(dimensionResource(R.dimen.margin_small))) + Text(option) + } + } + } + }, + confirmButton = {}, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(android.R.string.cancel)) + } + } + ) +} + +@PreviewWrapper(ThemePreviewProvider::class) +@Preview +@Composable +private fun NetworkPreferenceScreenPreview() { + ScreenContent(hasMicroG = true) +} diff --git a/app/src/main/java/com/aurora/store/compose/ui/preferences/updates/UpdatesPreferenceScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/preferences/updates/UpdatesPreferenceScreen.kt new file mode 100644 index 000000000..7d76ec05c --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/preferences/updates/UpdatesPreferenceScreen.kt @@ -0,0 +1,456 @@ +/* + * SPDX-FileCopyrightText: 2025 The Calyx Institute + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.ui.preferences.updates + +import android.Manifest +import android.content.Intent +import android.provider.Settings +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Checkbox +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.ListItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Slider +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.stringArrayResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewWrapper +import androidx.core.net.toUri +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import com.aurora.extensions.isIgnoringBatteryOptimizations +import com.aurora.extensions.isTAndAbove +import com.aurora.store.R +import com.aurora.store.compose.composable.TopAppBar +import com.aurora.store.compose.preview.ThemePreviewProvider +import com.aurora.store.compose.ui.preferences.network.SingleChoiceDialog +import com.aurora.store.data.model.UpdateMode +import com.aurora.store.util.Preferences +import com.aurora.store.util.Preferences.PREFERENCES_UPDATES_RESTRICTIONS_BATTERY +import com.aurora.store.util.Preferences.PREFERENCES_UPDATES_RESTRICTIONS_IDLE +import com.aurora.store.util.Preferences.PREFERENCES_UPDATES_RESTRICTIONS_METERED +import com.aurora.store.util.Preferences.PREFERENCE_FILTER_AURORA_ONLY +import com.aurora.store.util.Preferences.PREFERENCE_FILTER_FDROID +import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_AUTO +import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_CHECK_INTERVAL +import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_EXTENDED +import com.aurora.store.util.save +import com.aurora.store.viewmodel.all.UpdatesViewModel +import kotlin.math.roundToInt + +@Composable +fun UpdatesPreferenceScreen(viewModel: UpdatesViewModel = hiltViewModel()) { + ScreenContent( + onCancelAutomatedCheck = { viewModel.updateHelper.cancelAutomatedCheck() }, + onScheduleAutomatedCheck = { viewModel.updateHelper.scheduleAutomatedCheck() }, + onUpdateAutomatedCheck = { viewModel.updateHelper.updateAutomatedCheck() }, + onCheckUpdatesNow = { viewModel.updateHelper.checkUpdatesNow() } + ) +} + +@Composable +private fun ScreenContent( + onCancelAutomatedCheck: () -> Unit = {}, + onScheduleAutomatedCheck: () -> Unit = {}, + onUpdateAutomatedCheck: () -> Unit = {}, + onCheckUpdatesNow: () -> Unit = {} +) { + val context = LocalContext.current + val autoEntries = stringArrayResource(R.array.pref_updates_auto) + + var autoMode by remember { + mutableIntStateOf(Preferences.getInteger(context, PREFERENCE_UPDATES_AUTO, 2)) + } + val autoEnabled = autoMode != UpdateMode.DISABLED.ordinal + + var checkInterval by remember { + mutableFloatStateOf( + Preferences.getInteger(context, PREFERENCE_UPDATES_CHECK_INTERVAL, 3).toFloat() + ) + } + var filterFDroid by remember { + mutableStateOf(Preferences.getBoolean(context, PREFERENCE_FILTER_FDROID, true)) + } + var filterAuroraOnly by remember { + mutableStateOf(Preferences.getBoolean(context, PREFERENCE_FILTER_AURORA_ONLY)) + } + var updatesExtended by remember { + mutableStateOf(Preferences.getBoolean(context, PREFERENCE_UPDATES_EXTENDED)) + } + var showAutoDialog by remember { mutableStateOf(false) } + var showRestrictionsDialog by remember { mutableStateOf(false) } + + val notifPermissionLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.RequestPermission() + ) { granted -> + if (granted) { + context.save(PREFERENCE_UPDATES_AUTO, UpdateMode.CHECK_AND_NOTIFY.ordinal) + autoMode = UpdateMode.CHECK_AND_NOTIFY.ordinal + onScheduleAutomatedCheck() + } else { + autoMode = Preferences.getInteger(context, PREFERENCE_UPDATES_AUTO, 2) + } + } + + val batteryOptLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { + if (context.isIgnoringBatteryOptimizations()) { + context.save(PREFERENCE_UPDATES_AUTO, UpdateMode.CHECK_AND_INSTALL.ordinal) + autoMode = UpdateMode.CHECK_AND_INSTALL.ordinal + onScheduleAutomatedCheck() + } else { + autoMode = Preferences.getInteger(context, PREFERENCE_UPDATES_AUTO, 2) + } + } + + if (showAutoDialog) { + SingleChoiceDialog( + title = stringResource(R.string.pref_updates_auto), + options = autoEntries.toList(), + selected = autoMode, + onSelect = { index -> + showAutoDialog = false + when (UpdateMode.entries[index]) { + UpdateMode.DISABLED -> { + context.save(PREFERENCE_UPDATES_AUTO, 0) + autoMode = 0 + onCancelAutomatedCheck() + } + UpdateMode.CHECK_AND_NOTIFY -> { + if (!isTAndAbove || + context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == + android.content.pm.PackageManager.PERMISSION_GRANTED + ) { + context.save(PREFERENCE_UPDATES_AUTO, 1) + autoMode = 1 + onScheduleAutomatedCheck() + } else { + notifPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } + } + UpdateMode.CHECK_AND_INSTALL -> { + if (context.isIgnoringBatteryOptimizations()) { + context.save(PREFERENCE_UPDATES_AUTO, 2) + autoMode = 2 + onScheduleAutomatedCheck() + } else { + batteryOptLauncher.launch( + Intent( + Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, + "package:${context.packageName}".toUri() + ) + ) + } + } + else -> {} + } + }, + onDismiss = { showAutoDialog = false } + ) + } + + if (showRestrictionsDialog) { + UpdatesRestrictionsDialog( + onUpdateAutomatedCheck = onUpdateAutomatedCheck, + onDismiss = { showRestrictionsDialog = false } + ) + } + + Scaffold( + topBar = { + TopAppBar( + title = stringResource(R.string.title_updates) + ) + } + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize() + ) { + item { + ListItem( + modifier = Modifier.clickable { showAutoDialog = true }, + headlineContent = { Text(stringResource(R.string.pref_updates_auto)) }, + supportingContent = { Text(autoEntries.getOrElse(autoMode) { "" }) } + ) + } + if (autoEnabled) { + item { + ListItem( + headlineContent = { + Text(stringResource(R.string.pref_updates_check_frequency)) + }, + supportingContent = { + Column { + Text(stringResource(R.string.pref_updates_check_frequency_desc)) + Spacer(Modifier.height(dimensionResource(R.dimen.margin_small))) + Row(verticalAlignment = Alignment.CenterVertically) { + Slider( + value = checkInterval, + onValueChange = { checkInterval = it }, + onValueChangeFinished = { + context.save( + PREFERENCE_UPDATES_CHECK_INTERVAL, + checkInterval.roundToInt() + ) + onUpdateAutomatedCheck() + }, + valueRange = 1f..24f, + steps = 22, + modifier = Modifier.weight(1f) + ) + Spacer(Modifier.width(dimensionResource(R.dimen.margin_small))) + Text(checkInterval.roundToInt().toString()) + } + } + } + ) + } + item { + ListItem( + modifier = Modifier.clickable { showRestrictionsDialog = true }, + headlineContent = { + Text(stringResource(R.string.pref_updates_restrictions_title)) + }, + supportingContent = { + Text(stringResource(R.string.pref_updates_restrictions_desc)) + } + ) + } + } + item { HorizontalDivider() } + item { + ListItem(headlineContent = { + Text(stringResource(R.string.pref_updates_app_source)) + }) + } + item { + ListItem( + modifier = Modifier.clickable { + filterFDroid = !filterFDroid + context.save(PREFERENCE_FILTER_FDROID, filterFDroid) + onCheckUpdatesNow() + }, + headlineContent = { Text(stringResource(R.string.pref_filter_fdroid_title)) }, + supportingContent = { + Text(stringResource(R.string.pref_filter_fdroid_summary)) + }, + trailingContent = { + Switch( + checked = filterFDroid, + onCheckedChange = { checked -> + filterFDroid = checked + context.save(PREFERENCE_FILTER_FDROID, checked) + onCheckUpdatesNow() + } + ) + } + ) + } + item { + ListItem( + modifier = Modifier.clickable { + filterAuroraOnly = !filterAuroraOnly + context.save(PREFERENCE_FILTER_AURORA_ONLY, filterAuroraOnly) + if (filterAuroraOnly) filterFDroid = false + onCheckUpdatesNow() + }, + headlineContent = { Text(stringResource(R.string.pref_aurora_only)) }, + supportingContent = { Text(stringResource(R.string.pref_aurora_only_desc)) }, + trailingContent = { + Switch( + checked = filterAuroraOnly, + onCheckedChange = { checked -> + filterAuroraOnly = checked + context.save(PREFERENCE_FILTER_AURORA_ONLY, checked) + if (checked) filterFDroid = false + onCheckUpdatesNow() + } + ) + } + ) + } + item { HorizontalDivider() } + item { + ListItem(headlineContent = { Text(stringResource(R.string.pref_common_advanced)) }) + } + item { + ListItem( + modifier = Modifier.clickable { + updatesExtended = !updatesExtended + context.save(PREFERENCE_UPDATES_EXTENDED, updatesExtended) + onCheckUpdatesNow() + }, + headlineContent = { Text(stringResource(R.string.pref_updates_incompatible)) }, + supportingContent = { + Text(stringResource(R.string.pref_updates_incompatible_desc)) + }, + trailingContent = { + Switch( + checked = updatesExtended, + onCheckedChange = { checked -> + updatesExtended = checked + context.save(PREFERENCE_UPDATES_EXTENDED, checked) + onCheckUpdatesNow() + } + ) + } + ) + } + } + } +} + +@Composable +private fun UpdatesRestrictionsDialog(onUpdateAutomatedCheck: () -> Unit, onDismiss: () -> Unit) { + val context = LocalContext.current + var metered by remember { + mutableStateOf( + Preferences.getBoolean(context, PREFERENCES_UPDATES_RESTRICTIONS_METERED, true) + ) + } + var idle by remember { + mutableStateOf(Preferences.getBoolean(context, PREFERENCES_UPDATES_RESTRICTIONS_IDLE, true)) + } + var battery by remember { + mutableStateOf( + Preferences.getBoolean(context, PREFERENCES_UPDATES_RESTRICTIONS_BATTERY, true) + ) + } + + DisposableEffect(Unit) { + onDispose { onUpdateAutomatedCheck() } + } + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.pref_updates_restrictions_title)) }, + text = { + Column { + Text(stringResource(R.string.pref_updates_restrictions_desc)) + Spacer(Modifier.height(dimensionResource(R.dimen.margin_small))) + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + metered = !metered + Preferences.putBoolean( + context, + PREFERENCES_UPDATES_RESTRICTIONS_METERED, + metered + ) + }, + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = metered, + onCheckedChange = { checked -> + metered = checked + Preferences.putBoolean( + context, + PREFERENCES_UPDATES_RESTRICTIONS_METERED, + checked + ) + } + ) + Text(stringResource(R.string.pref_updates_restrictions_metered)) + } + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + idle = !idle + Preferences.putBoolean( + context, + PREFERENCES_UPDATES_RESTRICTIONS_IDLE, + idle + ) + }, + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = idle, + onCheckedChange = { checked -> + idle = checked + Preferences.putBoolean( + context, + PREFERENCES_UPDATES_RESTRICTIONS_IDLE, + checked + ) + } + ) + Text(stringResource(R.string.pref_updates_restrictions_idle)) + } + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + battery = !battery + Preferences.putBoolean( + context, + PREFERENCES_UPDATES_RESTRICTIONS_BATTERY, + battery + ) + }, + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = battery, + onCheckedChange = { checked -> + battery = checked + Preferences.putBoolean( + context, + PREFERENCES_UPDATES_RESTRICTIONS_BATTERY, + checked + ) + } + ) + Text(stringResource(R.string.pref_updates_restrictions_battery)) + } + } + }, + confirmButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(android.R.string.ok)) + } + } + ) +} + +@PreviewWrapper(ThemePreviewProvider::class) +@Preview +@Composable +private fun UpdatesPreferenceScreenPreview() { + ScreenContent() +}