compose: migrate preferences screens

Settings, UI, Installation, Network and Updates preference panes are
now native Compose. Replaces the old PreferenceFragment XML hierarchy
with explicit ListItem / Switch / dialog rows.
This commit is contained in:
Rahul Patel
2026-05-19 12:12:01 +05:30
parent 9a1191ecf8
commit 9e0a4ffee7
6 changed files with 1232 additions and 5 deletions

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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<DevicePolicyManager>()
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()
}

View File

@@ -35,7 +35,7 @@ import com.aurora.store.data.model.InstallerInfo
import com.aurora.store.viewmodel.preferences.InstallerViewModel import com.aurora.store.viewmodel.preferences.InstallerViewModel
@Composable @Composable
fun InstallerScreen(onNavigateUp: () -> Unit, viewModel: InstallerViewModel = hiltViewModel()) { fun InstallerScreen(viewModel: InstallerViewModel = hiltViewModel()) {
val currentInstallerId by viewModel.currentInstaller.collectAsStateWithLifecycle() val currentInstallerId by viewModel.currentInstaller.collectAsStateWithLifecycle()
val snackBarHostState = remember { SnackbarHostState() } val snackBarHostState = remember { SnackbarHostState() }
@@ -46,7 +46,6 @@ fun InstallerScreen(onNavigateUp: () -> Unit, viewModel: InstallerViewModel = hi
} }
ScreenContent( ScreenContent(
onNavigateUp = onNavigateUp,
snackBarHostState = snackBarHostState, snackBarHostState = snackBarHostState,
currentInstaller = Installer.entries[currentInstallerId], currentInstaller = Installer.entries[currentInstallerId],
availableInstallers = AppInstaller.getAvailableInstallersInfo(LocalContext.current), availableInstallers = AppInstaller.getAvailableInstallersInfo(LocalContext.current),
@@ -56,7 +55,6 @@ fun InstallerScreen(onNavigateUp: () -> Unit, viewModel: InstallerViewModel = hi
@Composable @Composable
private fun ScreenContent( private fun ScreenContent(
onNavigateUp: () -> Unit = {},
snackBarHostState: SnackbarHostState = SnackbarHostState(), snackBarHostState: SnackbarHostState = SnackbarHostState(),
currentInstaller: Installer = Installer.SESSION, currentInstaller: Installer = Installer.SESSION,
availableInstallers: List<InstallerInfo> = emptyList(), availableInstallers: List<InstallerInfo> = emptyList(),
@@ -70,8 +68,7 @@ private fun ScreenContent(
}, },
topBar = { topBar = {
TopAppBar( TopAppBar(
title = stringResource(R.string.pref_install_mode_title), title = stringResource(R.string.pref_install_mode_title)
onNavigateUp = onNavigateUp
) )
} }
) { paddingValues -> ) { paddingValues ->

View File

@@ -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<String>,
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)
}

View File

@@ -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()
}