diff --git a/app/src/main/java/com/aurora/store/compose/navigation/Destination.kt b/app/src/main/java/com/aurora/store/compose/navigation/Destination.kt index 3f824027e..da381c775 100644 --- a/app/src/main/java/com/aurora/store/compose/navigation/Destination.kt +++ b/app/src/main/java/com/aurora/store/compose/navigation/Destination.kt @@ -45,4 +45,5 @@ sealed class Destination { data object Dispenser : Destination() data object UIPreference : Destination() data object UpdatesPreference : Destination() + data object SourceFilters : Destination() } diff --git a/app/src/main/java/com/aurora/store/compose/navigation/NavDisplay.kt b/app/src/main/java/com/aurora/store/compose/navigation/NavDisplay.kt index 773f09c03..0846dc0cf 100644 --- a/app/src/main/java/com/aurora/store/compose/navigation/NavDisplay.kt +++ b/app/src/main/java/com/aurora/store/compose/navigation/NavDisplay.kt @@ -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 { SettingsScreen(onNavigateTo = ::navigate) } entry { NetworkPreferenceScreen(onNavigateTo = ::navigate) } entry { UIPreferenceScreen() } - entry { UpdatesPreferenceScreen() } + entry { UpdatesPreferenceScreen(onNavigateTo = ::navigate) } + entry { SourceFiltersScreen() } } ) } diff --git a/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt b/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt index c651ab0d3..f8b0b0870 100644 --- a/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt +++ b/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt @@ -92,6 +92,9 @@ sealed class Screen : NavKey, Parcelable { @Serializable data object UpdatesPreference : Screen() + @Serializable + data object SourceFilters : Screen() + @Serializable data object Splash : Screen() diff --git a/app/src/main/java/com/aurora/store/compose/ui/preferences/updates/SourceFiltersScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/preferences/updates/SourceFiltersScreen.kt new file mode 100644 index 000000000..9c9ac0458 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/preferences/updates/SourceFiltersScreen.kt @@ -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, + 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() +} 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 index 10462a9df..f457b5cf0 100644 --- 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 @@ -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 diff --git a/app/src/main/java/com/aurora/store/data/work/UpdateWorker.kt b/app/src/main/java/com/aurora/store/data/work/UpdateWorker.kt index f0bbd8e97..f32a14527 100644 --- a/app/src/main/java/com/aurora/store/data/work/UpdateWorker.kt +++ b/app/src/main/java/com/aurora/store/data/work/UpdateWorker.kt @@ -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 } diff --git a/app/src/main/java/com/aurora/store/util/PackageUtil.kt b/app/src/main/java/com/aurora/store/util/PackageUtil.kt index 1f800c942..75ad1b36c 100644 --- a/app/src/main/java/com/aurora/store/util/PackageUtil.kt +++ b/app/src/main/java/com/aurora/store/util/PackageUtil.kt @@ -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) { diff --git a/app/src/main/java/com/aurora/store/util/Preferences.kt b/app/src/main/java/com/aurora/store/util/Preferences.kt index 73a9ec17f..dc56d21ca 100644 --- a/app/src/main/java/com/aurora/store/util/Preferences.kt +++ b/app/src/main/java/com/aurora/store/util/Preferences.kt @@ -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" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 525df6792..e240d90e9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -226,10 +226,20 @@ "For you pages" "Display For you pages on home screen" "Similar and related apps" - Show updates that may fail - Display updates for incompatible or disabled apps that may fail to install - Filter apps from other sources - Don\'t check for updates for apps installed from sources outside Aurora Store + "Show updates that may fail" + "Display updates for incompatible or disabled apps that may fail to install." + "Filter apps from other sources." + "Do not check for updates for apps installed from sources outside Aurora Store" + "Filter apps from other sources" + "Skip every app not installed by Aurora Store" + "No installers excluded" + "%1$d installer(s) excluded" + "Filter all other installers" + "Specific installers" + "No installers added yet" + "Add installer" + "Enter package name" + "Remove installer" "Download failed" "App not purchased" "App not supported"