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"