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.
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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<Screen.Settings> { SettingsScreen(onNavigateTo = ::navigate) }
|
||||
entry<Screen.NetworkPreference> { NetworkPreferenceScreen(onNavigateTo = ::navigate) }
|
||||
entry<Screen.UIPreference> { UIPreferenceScreen() }
|
||||
entry<Screen.NotificationPreference> { NotificationPreferenceScreen() }
|
||||
entry<Screen.UpdatesPreference> { UpdatesPreferenceScreen(onNavigateTo = ::navigate) }
|
||||
entry<Screen.SourceFilters> { SourceFiltersScreen() }
|
||||
entry<Screen.SecurityPreference> { SecurityPreferenceScreen() }
|
||||
|
||||
@@ -89,6 +89,9 @@ sealed class Screen : NavKey, Parcelable {
|
||||
@Serializable
|
||||
data object UIPreference : Screen()
|
||||
|
||||
@Serializable
|
||||
data object NotificationPreference : Screen()
|
||||
|
||||
@Serializable
|
||||
data object UpdatesPreference : Screen()
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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) },
|
||||
|
||||
@@ -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<NotificationManager>()!!
|
||||
|
||||
// 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<Long>()
|
||||
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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -298,6 +298,13 @@
|
||||
<string name="title_no_network">"No network"</string>
|
||||
<string name="title_purchase_history">"Purchase history"</string>
|
||||
<string name="title_settings">"Settings"</string>
|
||||
<string name="title_notifications">"Notifications"</string>
|
||||
<string name="pref_notification_progress">Show download progress</string>
|
||||
<string name="pref_notification_progress_desc">Show detailed progress while downloading and updating apps. When off, a single quiet notification keeps downloads running in the background.</string>
|
||||
<string name="pref_notification_categories">Categories</string>
|
||||
<string name="pref_notification_categories_desc">Tune sound and importance for each kind of notification</string>
|
||||
<string name="pref_notification_disabled">Notifications are turned off</string>
|
||||
<string name="pref_notification_disabled_desc">Tap to enable notifications for Aurora Store</string>
|
||||
<string name="title_spoof_manager">"Spoof manager"</string>
|
||||
<string name="title_search_suggestion">"Search suggestion"</string>
|
||||
<string name="title_search_results">"Search results"</string>
|
||||
|
||||
Reference in New Issue
Block a user