updates: granular installer filters + advanced-section move

- New SourceFiltersScreen with All toggle + add/remove of specific
  installer package names. Backed by PREFERENCE_FILTER_INSTALLERS
  (Set<String>); PREFERENCE_FILTER_AURORA_ONLY preserved as the All
  switch for backward compat.
- UpdateWorker reads installer via getInstallSourceInfo (R+) / falls
  back to deprecated getInstallerPackageName below.
- UpdatesPreferenceScreen: "Filter F-Droid apps" moved to Advanced;
  "Filter apps from other sources" becomes a navigation row to the
  new sub-screen with a state summary.
This commit is contained in:
Rahul Patel
2026-05-19 11:49:51 +05:30
parent 106779737c
commit 8f0bf55714
9 changed files with 318 additions and 41 deletions

View File

@@ -45,4 +45,5 @@ sealed class Destination {
data object Dispenser : Destination()
data object UIPreference : Destination()
data object UpdatesPreference : Destination()
data object SourceFilters : Destination()
}

View File

@@ -53,6 +53,7 @@ import com.aurora.store.compose.ui.preferences.UIPreferenceScreen
import com.aurora.store.compose.ui.preferences.installation.InstallationPreferenceScreen
import com.aurora.store.compose.ui.preferences.installation.InstallerScreen
import com.aurora.store.compose.ui.preferences.network.NetworkPreferenceScreen
import com.aurora.store.compose.ui.preferences.updates.SourceFiltersScreen
import com.aurora.store.compose.ui.preferences.updates.UpdatesPreferenceScreen
import com.aurora.store.compose.ui.search.SearchScreen
import com.aurora.store.compose.ui.splash.SplashScreen
@@ -147,6 +148,7 @@ fun NavDisplay(startDestination: NavKey) {
Destination.Dispenser -> backstack.add(Screen.Dispenser)
Destination.UIPreference -> backstack.add(Screen.UIPreference)
Destination.UpdatesPreference -> backstack.add(Screen.UpdatesPreference)
Destination.SourceFilters -> backstack.add(Screen.SourceFilters)
}
}
@@ -262,7 +264,8 @@ fun NavDisplay(startDestination: NavKey) {
entry<Screen.Settings> { SettingsScreen(onNavigateTo = ::navigate) }
entry<Screen.NetworkPreference> { NetworkPreferenceScreen(onNavigateTo = ::navigate) }
entry<Screen.UIPreference> { UIPreferenceScreen() }
entry<Screen.UpdatesPreference> { UpdatesPreferenceScreen() }
entry<Screen.UpdatesPreference> { UpdatesPreferenceScreen(onNavigateTo = ::navigate) }
entry<Screen.SourceFilters> { SourceFiltersScreen() }
}
)
}

View File

@@ -92,6 +92,9 @@ sealed class Screen : NavKey, Parcelable {
@Serializable
data object UpdatesPreference : Screen()
@Serializable
data object SourceFilters : Screen()
@Serializable
data object Splash : Screen()

View File

@@ -0,0 +1,210 @@
/*
* SPDX-FileCopyrightText: 2026 Aurora OSS
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.ui.preferences.updates
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
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.DisposableEffect
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.painterResource
import androidx.compose.ui.res.stringResource
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.preview.ThemePreviewProvider
import com.aurora.store.util.Preferences
import com.aurora.store.util.Preferences.PREFERENCE_FILTER_AURORA_ONLY
import com.aurora.store.util.Preferences.PREFERENCE_FILTER_INSTALLERS
import com.aurora.store.util.save
import com.aurora.store.viewmodel.all.UpdatesViewModel
@Composable
fun SourceFiltersScreen(viewModel: UpdatesViewModel = hiltViewModel()) {
ScreenContent(onCheckUpdatesNow = { viewModel.updateHelper.checkUpdatesNow() })
}
@Composable
private fun ScreenContent(onCheckUpdatesNow: () -> Unit = {}) {
val context = LocalContext.current
var auroraOnly by remember {
mutableStateOf(Preferences.getBoolean(context, PREFERENCE_FILTER_AURORA_ONLY))
}
var installers by remember {
mutableStateOf(Preferences.getStringSet(context, PREFERENCE_FILTER_INSTALLERS))
}
var showAddDialog by remember { mutableStateOf(false) }
// Trigger a fresh check on the way out so removed/added installers take effect
// without waiting for the next periodic run.
DisposableEffect(Unit) {
onDispose { onCheckUpdatesNow() }
}
if (showAddDialog) {
AddInstallerDialog(
existing = installers,
onAdd = { name ->
installers = installers + name
context.save(PREFERENCE_FILTER_INSTALLERS, installers)
showAddDialog = false
},
onDismiss = { showAddDialog = false }
)
}
Scaffold(
topBar = { TopAppBar(title = stringResource(R.string.pref_source_filters_title)) }
) { paddingValues ->
LazyColumn(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
) {
item {
ListItem(
modifier = Modifier.clickable {
auroraOnly = !auroraOnly
context.save(PREFERENCE_FILTER_AURORA_ONLY, auroraOnly)
},
headlineContent = { Text(stringResource(R.string.source_filters_all)) },
supportingContent = {
Text(stringResource(R.string.pref_source_filters_desc_all))
},
trailingContent = {
Switch(
checked = auroraOnly,
onCheckedChange = { checked ->
auroraOnly = checked
context.save(PREFERENCE_FILTER_AURORA_ONLY, checked)
}
)
}
)
}
item { HorizontalDivider() }
item {
ListItem(
headlineContent = {
Text(stringResource(R.string.source_filters_installers_header))
}
)
}
if (installers.isEmpty()) {
item {
ListItem(
headlineContent = {
Text(
text = stringResource(R.string.source_filters_installers_empty),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
)
}
} else {
items(items = installers.toList(), key = { it }) { name ->
ListItem(
headlineContent = { Text(name) },
trailingContent = {
IconButton(
onClick = {
installers = installers - name
context.save(PREFERENCE_FILTER_INSTALLERS, installers)
}
) {
Icon(
painter = painterResource(R.drawable.ic_delete_forever),
contentDescription = stringResource(
R.string.source_filters_remove
)
)
}
}
)
}
}
item {
ListItem(
modifier = Modifier.clickable { showAddDialog = true },
leadingContent = {
Icon(
painter = painterResource(R.drawable.ic_add),
contentDescription = null
)
},
headlineContent = { Text(stringResource(R.string.source_filters_add)) }
)
}
}
}
}
@Composable
private fun AddInstallerDialog(
existing: Set<String>,
onAdd: (String) -> Unit,
onDismiss: () -> Unit
) {
var value by remember { mutableStateOf("") }
val trimmed = value.trim()
val canAdd = trimmed.isNotEmpty() && trimmed !in existing
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(stringResource(R.string.source_filters_add)) },
text = {
Column {
OutlinedTextField(
value = value,
label = { Text(stringResource(R.string.source_filters_add_dialog_hint)) },
onValueChange = { value = it },
singleLine = true
)
}
},
confirmButton = {
TextButton(onClick = { onAdd(trimmed) }, enabled = canAdd) {
Text(stringResource(android.R.string.ok))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.action_cancel))
}
}
)
}
@PreviewWrapper(ThemePreviewProvider::class)
@Preview
@Composable
private fun SourceFiltersScreenPreview() {
ScreenContent()
}

View File

@@ -44,10 +44,14 @@ 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 androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
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.navigation.Destination
import com.aurora.store.compose.preview.ThemePreviewProvider
import com.aurora.store.compose.ui.preferences.network.SingleChoiceDialog
import com.aurora.store.data.model.UpdateMode
@@ -57,6 +61,7 @@ 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_FILTER_INSTALLERS
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
@@ -65,12 +70,16 @@ import com.aurora.store.viewmodel.all.UpdatesViewModel
import kotlin.math.abs
@Composable
fun UpdatesPreferenceScreen(viewModel: UpdatesViewModel = hiltViewModel()) {
fun UpdatesPreferenceScreen(
viewModel: UpdatesViewModel = hiltViewModel(),
onNavigateTo: (Destination) -> Unit = {}
) {
ScreenContent(
onCancelAutomatedCheck = { viewModel.updateHelper.cancelAutomatedCheck() },
onScheduleAutomatedCheck = { viewModel.updateHelper.scheduleAutomatedCheck() },
onUpdateAutomatedCheck = { viewModel.updateHelper.updateAutomatedCheck() },
onCheckUpdatesNow = { viewModel.updateHelper.checkUpdatesNow() }
onCheckUpdatesNow = { viewModel.updateHelper.checkUpdatesNow() },
onNavigateTo = onNavigateTo
)
}
@@ -79,7 +88,8 @@ private fun ScreenContent(
onCancelAutomatedCheck: () -> Unit = {},
onScheduleAutomatedCheck: () -> Unit = {},
onUpdateAutomatedCheck: () -> Unit = {},
onCheckUpdatesNow: () -> Unit = {}
onCheckUpdatesNow: () -> Unit = {},
onNavigateTo: (Destination) -> Unit = {}
) {
val context = LocalContext.current
val autoEntries = stringArrayResource(R.array.pref_updates_auto)
@@ -96,12 +106,26 @@ private fun ScreenContent(
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))
}
// Re-read source-filter prefs after returning from SourceFiltersScreen.
val lifecycleOwner = LocalLifecycleOwner.current
var refreshKey by remember { mutableIntStateOf(0) }
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) refreshKey++
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
}
val filterAuroraOnly = remember(refreshKey) {
Preferences.getBoolean(context, PREFERENCE_FILTER_AURORA_ONLY)
}
val installerCount = remember(refreshKey) {
Preferences.getStringSet(context, PREFERENCE_FILTER_INSTALLERS).size
}
var showAutoDialog by remember { mutableStateOf(false) }
var showFrequencyDialog by remember { mutableStateOf(false) }
var showRestrictionsDialog by remember { mutableStateOf(false) }
@@ -248,6 +272,23 @@ private fun ScreenContent(
Text(stringResource(R.string.pref_updates_app_source))
})
}
item {
ListItem(
modifier = Modifier.clickable {
onNavigateTo(Destination.SourceFilters)
},
headlineContent = {
Text(stringResource(R.string.pref_source_filters_title))
},
supportingContent = {
Text(sourceFiltersSummary(filterAuroraOnly, installerCount))
}
)
}
item { HorizontalDivider() }
item {
ListItem(headlineContent = { Text(stringResource(R.string.pref_common_advanced)) })
}
item {
ListItem(
modifier = Modifier.clickable {
@@ -271,33 +312,6 @@ private fun ScreenContent(
}
)
}
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 {
@@ -458,6 +472,13 @@ private fun selectedFrequencyIndex(storedHours: Int): Int {
return UPDATE_INTERVAL_HOURS.indices.minBy { abs(UPDATE_INTERVAL_HOURS[it] - storedHours) }
}
@Composable
private fun sourceFiltersSummary(allEnabled: Boolean, count: Int): String = when {
allEnabled -> stringResource(R.string.pref_source_filters_desc_all)
count == 0 -> stringResource(R.string.pref_source_filters_desc_none)
else -> stringResource(R.string.pref_source_filters_desc_count, count)
}
@PreviewWrapper(ThemePreviewProvider::class)
@Preview
@Composable

View File

@@ -166,12 +166,23 @@ class UpdateWorker @AssistedInject constructor(
.filter { if (!isExtendedUpdateEnabled) it.applicationInfo!!.enabled else true }
// Filter out packages based on user's preferences
val installerFilters = Preferences.getStringSet(
context,
Preferences.PREFERENCE_FILTER_INSTALLERS
)
val filteredPackages = if (isAuroraOnlyFilterEnabled) {
packages.filter { CertUtil.isAuroraStoreApp(context, it.packageName) }
} else {
packages.filterNot {
if (isFDroidFilterEnabled) {
CertUtil.isFDroidApp(context, it.packageName)
packages.filterNot { pkg ->
if (isFDroidFilterEnabled && CertUtil.isFDroidApp(context, pkg.packageName)) {
return@filterNot true
}
if (installerFilters.isNotEmpty()) {
val installer = PackageUtil.getInstallerPackageName(
context,
pkg.packageName
)
installer != null && installer in installerFilters
} else {
false
}

View File

@@ -42,6 +42,7 @@ import com.aurora.Constants.PACKAGE_NAME_PLAY_STORE
import com.aurora.extensions.isHuawei
import com.aurora.extensions.isOAndAbove
import com.aurora.extensions.isPAndAbove
import com.aurora.extensions.isRAndAbove
import com.aurora.extensions.isTAndAbove
import com.aurora.extensions.isVAndAbove
import com.aurora.extensions.isValidApp
@@ -270,6 +271,22 @@ object PackageUtil {
}
}
/**
* Returns the installer package name that installed [packageName], or null if unknown / not
* installed. Uses [PackageManager.getInstallSourceInfo] on API 30+; falls back to the
* pre-30 deprecated API below that.
*/
@Suppress("DEPRECATION")
fun getInstallerPackageName(context: Context, packageName: String): String? = try {
if (isRAndAbove) {
context.packageManager.getInstallSourceInfo(packageName).installingPackageName
} else {
context.packageManager.getInstallerPackageName(packageName)
}
} catch (_: PackageManager.NameNotFoundException) {
null
}
@Throws(Exception::class)
fun getPackageInfo(context: Context, packageName: String, flags: Int = 0): PackageInfo =
if (isTAndAbove) {

View File

@@ -39,6 +39,7 @@ object Preferences {
const val PREFERENCE_FILTER_FDROID = "PREFERENCE_FILTER_FDROID"
const val PREFERENCE_FILTER_AURORA_ONLY = "PREFERENCE_FILTER_AURORA_ONLY"
const val PREFERENCE_FILTER_INSTALLERS = "PREFERENCE_FILTER_INSTALLERS"
const val PREFERENCE_AUTO_DELETE = "PREFERENCE_AUTO_DELETE"