From a66098343800161f8806cacd2e7b83fe8378aa8b Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Sat, 30 May 2026 18:17:52 +0530 Subject: [PATCH] Add a Notifications settings screen with a progress toggle There was no in-app control over notifications. Add a Notifications preference screen under Settings offering: - "Show download progress": when off, downloads/updates no longer post per-tick progress; a single quiet foreground notification keeps the work running in the background (honouring the mandatory FGS rule). - Deep links into each notification category's system settings (O+) so users can tune sound and importance per channel. - A shortcut to enable notifications when they're turned off at the system level, so the screen degrades gracefully. The download worker reads the progress preference live, keeping its foreground notification minimal and skipping progress refreshes when the user has opted out. POST_NOTIFICATIONS is already handled by the onboarding permission flow, so no duplicate prompt is added here. --- .../store/compose/navigation/Destination.kt | 1 + .../store/compose/navigation/NavDisplay.kt | 3 + .../aurora/store/compose/navigation/Screen.kt | 3 + .../NotificationPreferenceScreen.kt | 174 ++++++++++++++++++ .../compose/ui/preferences/SettingsScreen.kt | 14 ++ .../aurora/store/data/work/DownloadWorker.kt | 14 +- .../java/com/aurora/store/util/Preferences.kt | 2 + app/src/main/res/values/strings.xml | 7 + 8 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/aurora/store/compose/ui/preferences/NotificationPreferenceScreen.kt 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 7a3d48a53..4c81f2368 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 @@ -44,6 +44,7 @@ sealed class Destination { data object NetworkPreference : Destination() data object Dispenser : Destination() data object UIPreference : Destination() + data object NotificationPreference : Destination() data object UpdatesPreference : Destination() data object SourceFilters : Destination() data object SecurityPreference : 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 b2223f827..857461033 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 @@ -48,6 +48,7 @@ import com.aurora.store.compose.ui.favourite.FavouriteScreen import com.aurora.store.compose.ui.installed.InstalledScreen import com.aurora.store.compose.ui.main.MainScreen import com.aurora.store.compose.ui.onboarding.OnboardingScreen +import com.aurora.store.compose.ui.preferences.NotificationPreferenceScreen import com.aurora.store.compose.ui.preferences.SettingsScreen import com.aurora.store.compose.ui.preferences.UIPreferenceScreen import com.aurora.store.compose.ui.preferences.installation.InstallationPreferenceScreen @@ -161,6 +162,7 @@ fun NavDisplay(startDestination: NavKey) { Destination.NetworkPreference -> backstack.add(Screen.NetworkPreference) Destination.Dispenser -> backstack.add(Screen.Dispenser) Destination.UIPreference -> backstack.add(Screen.UIPreference) + Destination.NotificationPreference -> backstack.add(Screen.NotificationPreference) Destination.UpdatesPreference -> backstack.add(Screen.UpdatesPreference) Destination.SourceFilters -> backstack.add(Screen.SourceFilters) Destination.SecurityPreference -> backstack.add(Screen.SecurityPreference) @@ -284,6 +286,7 @@ fun NavDisplay(startDestination: NavKey) { entry { SettingsScreen(onNavigateTo = ::navigate) } entry { NetworkPreferenceScreen(onNavigateTo = ::navigate) } entry { UIPreferenceScreen() } + entry { NotificationPreferenceScreen() } entry { UpdatesPreferenceScreen(onNavigateTo = ::navigate) } entry { SourceFiltersScreen() } entry { SecurityPreferenceScreen() } 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 87e14c11f..8cebacce6 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 @@ -89,6 +89,9 @@ sealed class Screen : NavKey, Parcelable { @Serializable data object UIPreference : Screen() + @Serializable + data object NotificationPreference : Screen() + @Serializable data object UpdatesPreference : Screen() diff --git a/app/src/main/java/com/aurora/store/compose/ui/preferences/NotificationPreferenceScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/preferences/NotificationPreferenceScreen.kt new file mode 100644 index 000000000..f4889cffc --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/preferences/NotificationPreferenceScreen.kt @@ -0,0 +1,174 @@ +/* + * SPDX-FileCopyrightText: 2026 Aurora OSS + * 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.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.net.toUri +import androidx.lifecycle.compose.LifecycleResumeEffect +import com.aurora.Constants +import com.aurora.extensions.areNotificationsEnabled +import com.aurora.extensions.isOAndAbove +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_NOTIFICATION_PROGRESS +import com.aurora.store.util.save + +@Composable +fun NotificationPreferenceScreen() { + ScreenContent() +} + +@Composable +private fun ScreenContent() { + val context = LocalContext.current + + var notificationsEnabled by remember { mutableStateOf(context.areNotificationsEnabled()) } + var showProgress by remember { + mutableStateOf(Preferences.getBoolean(context, PREFERENCE_NOTIFICATION_PROGRESS, true)) + } + + // Re-read the system notification state when returning from settings. + LifecycleResumeEffect(Unit) { + notificationsEnabled = context.areNotificationsEnabled() + onPauseOrDispose {} + } + + // The channels users most likely want to fine-tune (sound, importance, on/off), paired + // with their display-name strings, deep-linked into the system per-channel settings. + val channels = listOf( + Constants.NOTIFICATION_CHANNEL_DOWNLOADS to R.string.notification_channel_downloads, + Constants.NOTIFICATION_CHANNEL_INSTALL to R.string.notification_channel_install, + Constants.NOTIFICATION_CHANNEL_UPDATES to R.string.notification_channel_updates, + Constants.NOTIFICATION_CHANNEL_ALERTS to R.string.notification_channel_alerts, + Constants.NOTIFICATION_CHANNEL_EXPORT to R.string.notification_channel_export + ) + + Scaffold( + topBar = { + TopAppBar( + title = stringResource(R.string.title_notifications) + ) + } + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize() + ) { + if (!notificationsEnabled) { + item { + ListItem( + modifier = Modifier.clickable { openAppNotificationSettings(context) }, + headlineContent = { + Text(stringResource(R.string.pref_notification_disabled)) + }, + supportingContent = { + Text(stringResource(R.string.pref_notification_disabled_desc)) + } + ) + } + item { HorizontalDivider() } + } + + item { + ListItem( + modifier = Modifier.clickable { + showProgress = !showProgress + context.save(PREFERENCE_NOTIFICATION_PROGRESS, showProgress) + }, + headlineContent = { + Text(stringResource(R.string.pref_notification_progress)) + }, + supportingContent = { + Text(stringResource(R.string.pref_notification_progress_desc)) + }, + trailingContent = { + Switch( + checked = showProgress, + onCheckedChange = { checked -> + showProgress = checked + context.save(PREFERENCE_NOTIFICATION_PROGRESS, checked) + } + ) + } + ) + } + + // Per-channel system settings are only meaningful on Android O+ where channels + // exist; on older versions the app-level toggle above is the only control. + if (isOAndAbove) { + item { HorizontalDivider() } + item { + ListItem( + headlineContent = { + Text(stringResource(R.string.pref_notification_categories)) + }, + supportingContent = { + Text(stringResource(R.string.pref_notification_categories_desc)) + } + ) + } + channels.forEach { (channelId, nameRes) -> + item { + ListItem( + modifier = Modifier.clickable { + openChannelSettings(context, channelId) + }, + headlineContent = { Text(stringResource(nameRes)) } + ) + } + } + } + } + } +} + +private fun openAppNotificationSettings(context: android.content.Context) { + val intent = if (isOAndAbove) { + Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) + .putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) + } else { + Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + .setData("package:${context.packageName}".toUri()) + } + context.startActivity(intent) +} + +private fun openChannelSettings(context: android.content.Context, channelId: String) { + val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS) + .putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) + .putExtra(Settings.EXTRA_CHANNEL_ID, channelId) + context.startActivity(intent) +} + +@PreviewWrapper(ThemePreviewProvider::class) +@Preview +@Composable +private fun NotificationPreferenceScreenPreview() { + ScreenContent() +} 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 index cde261b01..03dae286c 100644 --- 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 @@ -88,6 +88,20 @@ private fun ScreenContent(onNavigateTo: (Destination) -> Unit = {}) { headlineContent = { Text(stringResource(R.string.pref_ui_title)) } ) } + item { + ListItem( + modifier = Modifier.clickable { + onNavigateTo(Destination.NotificationPreference) + }, + leadingContent = { + Icon( + painter = painterResource(R.drawable.ic_notification_outlined), + contentDescription = null + ) + }, + headlineContent = { Text(stringResource(R.string.title_notifications)) } + ) + } item { ListItem( modifier = Modifier.clickable { onNavigateTo(Destination.NetworkPreference) }, diff --git a/app/src/main/java/com/aurora/store/data/work/DownloadWorker.kt b/app/src/main/java/com/aurora/store/data/work/DownloadWorker.kt index b8b81e0a4..b1a3934f8 100644 --- a/app/src/main/java/com/aurora/store/data/work/DownloadWorker.kt +++ b/app/src/main/java/com/aurora/store/data/work/DownloadWorker.kt @@ -46,6 +46,8 @@ import com.aurora.store.util.CertUtil import com.aurora.store.util.NotificationUtil import com.aurora.store.util.PackageUtil import com.aurora.store.util.PathUtil +import com.aurora.store.util.Preferences +import com.aurora.store.util.Preferences.PREFERENCE_NOTIFICATION_PROGRESS import dagger.assisted.Assisted import dagger.assisted.AssistedInject import java.io.File @@ -93,6 +95,12 @@ class DownloadWorker @AssistedInject constructor( private val notificationManager = context.getSystemService()!! + // When the user opts out of progress notifications, the mandatory foreground notification + // is kept minimal and per-tick progress refreshes are skipped. Read live so toggling the + // setting takes effect on the next download. + private val showProgress: Boolean + get() = Preferences.getBoolean(context, PREFERENCE_NOTIFICATION_PROGRESS, true) + private var icon: Bitmap? = null private var totalBytes by Delegates.notNull() private var totalProgress = 0 @@ -514,7 +522,7 @@ class DownloadWorker @AssistedInject constructor( } override suspend fun getForegroundInfo(): ForegroundInfo { - val notification = if (this::download.isInitialized) { + val notification = if (this::download.isInitialized && showProgress) { NotificationUtil.getDownloadNotification(context, download, icon) } else { NotificationUtil.getDownloadNotification(context) @@ -575,6 +583,10 @@ class DownloadWorker @AssistedInject constructor( else -> {} } + // Skip detailed progress refreshes when the user has hidden progress; the minimal + // foreground notification posted via getForegroundInfo keeps the download alive. + if (isProgress && !showProgress) return + val notification = NotificationUtil.getDownloadNotification( context, download, 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 9c7b621d0..e01de6560 100644 --- a/app/src/main/java/com/aurora/store/util/Preferences.kt +++ b/app/src/main/java/com/aurora/store/util/Preferences.kt @@ -43,6 +43,8 @@ object Preferences { const val PREFERENCE_AUTO_DELETE = "PREFERENCE_AUTO_DELETE" + const val PREFERENCE_NOTIFICATION_PROGRESS = "PREFERENCE_NOTIFICATION_PROGRESS" + const val PREFERENCE_INSTALLATION_DEVICE_OWNER = "PREFERENCE_INSTALLATION_DEVICE_OWNER" const val PREFERENCE_PROXY_URL = "PREFERENCE_PROXY_URL" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index de0922dfe..0c38aaf51 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -298,6 +298,13 @@ "No network" "Purchase history" "Settings" + "Notifications" + Show download progress + Show detailed progress while downloading and updating apps. When off, a single quiet notification keeps downloads running in the background. + Categories + Tune sound and importance for each kind of notification + Notifications are turned off + Tap to enable notifications for Aurora Store "Spoof manager" "Search suggestion" "Search results"