From 94292816ce785ee6cead82265e264e1649235d4f Mon Sep 17 00:00:00 2001 From: Andrew Gunnerson Date: Fri, 7 Aug 2026 00:48:20 -0400 Subject: [PATCH] Make persistent notification details optional If the new detailed notifications option is turned off, the folder and device states will no longer be shown in the persistent notification. This avoids frequent refreshes of the notification for folks who are not interested in the additional details. Fixes: #219 Signed-off-by: Andrew Gunnerson --- .../com/chiller3/basicsync/Notifications.kt | 2 +- .../com/chiller3/basicsync/Preferences.kt | 15 ++-- .../basicsync/settings/SettingsScreen.kt | 71 +++++++++++++------ .../basicsync/syncthing/SyncthingService.kt | 56 ++++++++++----- app/src/main/res/values/strings.xml | 6 ++ 5 files changed, 106 insertions(+), 44 deletions(-) diff --git a/app/src/main/java/com/chiller3/basicsync/Notifications.kt b/app/src/main/java/com/chiller3/basicsync/Notifications.kt index f889bbb6..ba8b981a 100644 --- a/app/src/main/java/com/chiller3/basicsync/Notifications.kt +++ b/app/src/main/java/com/chiller3/basicsync/Notifications.kt @@ -118,7 +118,7 @@ class Notifications(private val context: Context) { setOngoing(true) setOnlyAlertOnce(true) - if (runState.showFolderStates) { + if (state.showDetails && runState.showFolderStates) { setContentText(buildString { append(context.resources.getQuantityString( R.plurals.device_state_connected, diff --git a/app/src/main/java/com/chiller3/basicsync/Preferences.kt b/app/src/main/java/com/chiller3/basicsync/Preferences.kt index cc4c44cd..b214efa7 100644 --- a/app/src/main/java/com/chiller3/basicsync/Preferences.kt +++ b/app/src/main/java/com/chiller3/basicsync/Preferences.kt @@ -20,9 +20,10 @@ class Preferences(context: Context) { const val PREF_RESPECT_BATTERY_SAVER = "respect_battery_saver" const val PREF_RESPECT_AUTO_SYNC_DATA = "respect_auto_sync_data" const val PREF_KEEP_ALIVE = "keep_alive" + const val PREF_SHOW_DETAILS = "show_details" + const val PREF_SHOW_EXIT = "show_exit" const val PREF_REMOTE_CONTROL = "remote_control" const val PREF_ALLOW_AUTO_MODE = "allow_auto_mode" - const val PREF_SHOW_EXIT = "show_exit" const val PREF_START_ON_BOOT = "start_on_boot" const val PREF_REQUIRE_UNMETERED_NETWORK = "require_unmetered_network" const val PREF_NETWORK_ALLOW_WIFI = "network_allow_wifi" @@ -74,6 +75,14 @@ class Preferences(context: Context) { get() = prefs.getBoolean(PREF_KEEP_ALIVE, true) set(enabled) = prefs.edit { putBoolean(PREF_KEEP_ALIVE, enabled) } + var showDetails: Boolean + get() = prefs.getBoolean(PREF_SHOW_DETAILS, true) + set(enabled) = prefs.edit { putBoolean(PREF_SHOW_DETAILS, enabled) } + + var showExit: Boolean + get() = prefs.getBoolean(PREF_SHOW_EXIT, false) + set(enabled) = prefs.edit { putBoolean(PREF_SHOW_EXIT, enabled) } + var remoteControl: Boolean get() = prefs.getBoolean(PREF_REMOTE_CONTROL, false) set(enabled) = prefs.edit { putBoolean(PREF_REMOTE_CONTROL, enabled) } @@ -82,10 +91,6 @@ class Preferences(context: Context) { get() = prefs.getBoolean(PREF_ALLOW_AUTO_MODE, true) set(enabled) = prefs.edit { putBoolean(PREF_ALLOW_AUTO_MODE, enabled) } - var showExit: Boolean - get() = prefs.getBoolean(PREF_SHOW_EXIT, false) - set(enabled) = prefs.edit { putBoolean(PREF_SHOW_EXIT, enabled) } - var startOnBoot: Boolean get() = prefs.getBoolean(PREF_START_ON_BOOT, true) set(enabled) = prefs.edit { putBoolean(PREF_START_ON_BOOT, enabled) } diff --git a/app/src/main/java/com/chiller3/basicsync/settings/SettingsScreen.kt b/app/src/main/java/com/chiller3/basicsync/settings/SettingsScreen.kt index 048c5ba0..3dbb0648 100644 --- a/app/src/main/java/com/chiller3/basicsync/settings/SettingsScreen.kt +++ b/app/src/main/java/com/chiller3/basicsync/settings/SettingsScreen.kt @@ -108,9 +108,10 @@ fun SettingsScreen( val respectBatterySaver = remember(reloadPrefs) { prefs.respectBatterySaver } val respectAutoSyncData = remember(reloadPrefs) { prefs.respectAutoSyncData } val keepAlive = remember(reloadPrefs) { prefs.keepAlive } + val showDetails = remember(reloadPrefs) { prefs.showDetails } + val showExit = remember(reloadPrefs) { prefs.showExit } val remoteControl = remember(reloadPrefs) { prefs.remoteControl } val allowAutoMode = remember(reloadPrefs) { prefs.allowAutoMode } - val showExit = remember(reloadPrefs) { prefs.showExit } val startOnBoot = remember(reloadPrefs) { prefs.startOnBoot } val isDebugMode = remember(reloadPrefs) { prefs.isDebugMode } @@ -330,9 +331,10 @@ fun SettingsScreen( respectBatterySaver = respectBatterySaver, respectAutoSyncData = respectAutoSyncData, keepAlive = keepAlive, + showDetails = showDetails, + showExit = showExit, remoteControl = remoteControl, allowAutoMode = allowAutoMode, - showExit = showExit, startOnBoot = startOnBoot, isDebugMode = isDebugMode, onInhibitBatteryOptGrant = { @@ -440,6 +442,14 @@ fun SettingsScreen( prefs.keepAlive = enabled reloadPrefs++ }, + onShowDetailsChange = { enabled -> + prefs.showDetails = enabled + reloadPrefs++ + }, + onShowExitChange = { enabled -> + prefs.showExit = enabled + reloadPrefs++ + }, onRemoteControlChange = { enabled -> prefs.remoteControl = enabled reloadPrefs++ @@ -456,10 +466,6 @@ fun SettingsScreen( SyncthingService.start(context, action) }, - onShowExitChange = { enabled -> - prefs.showExit = enabled - reloadPrefs++ - }, onStartOnBootChange = { enabled -> prefs.startOnBoot = enabled reloadPrefs++ @@ -566,9 +572,10 @@ private fun SettingsContent( respectBatterySaver: Boolean, respectAutoSyncData: Boolean, keepAlive: Boolean, + showDetails: Boolean, + showExit: Boolean, remoteControl: Boolean, allowAutoMode: Boolean, - showExit: Boolean, startOnBoot: Boolean, isDebugMode: Boolean, onInhibitBatteryOptGrant: () -> Unit, @@ -590,9 +597,10 @@ private fun SettingsContent( onRespectAutoSyncDataChange: (Boolean) -> Unit, onSyncScheduleSettingsOpen: () -> Unit, onKeepAliveChange: (Boolean) -> Unit, + onShowDetailsChange: (Boolean) -> Unit, + onShowExitChange: (Boolean) -> Unit, onRemoteControlChange: (Boolean) -> Unit, onAllowAutoModeChange: (Boolean) -> Unit, - onShowExitChange: (Boolean) -> Unit, onStartOnBootChange: (Boolean) -> Unit, onDebugModeChange: (Boolean) -> Unit, onSourceRepoOpen: () -> Unit, @@ -844,6 +852,35 @@ private fun SettingsContent( ) } + item(key = "notifications") { + PreferenceCategory( + title = { Text(text = stringResource(R.string.pref_header_notifications)) }, + modifier = Modifier.animateItem(), + ) + } + + item(key = "show_details") { + SwitchPreference( + checked = showDetails, + onCheckedChange = onShowDetailsChange, + shapes = BetterSegmentedShapes.top(), + title = { Text(text = stringResource(R.string.pref_show_details_name)) }, + summary = { Text(text = stringResource(R.string.pref_show_details_desc)) }, + modifier = Modifier.animateItem(), + ) + } + + item(key = "show_exit") { + SwitchPreference( + checked = showExit, + onCheckedChange = onShowExitChange, + shapes = BetterSegmentedShapes.bottom(), + title = { Text(text = stringResource(R.string.pref_show_exit_name)) }, + summary = { Text(text = stringResource(R.string.pref_show_exit_desc)) }, + modifier = Modifier.animateItem(), + ) + } + item(key = "advanced") { PreferenceCategory( title = { Text(text = stringResource(R.string.pref_header_advanced)) }, @@ -873,17 +910,6 @@ private fun SettingsContent( ) } - item(key = "show_exit") { - SwitchPreference( - checked = showExit, - onCheckedChange = onShowExitChange, - shapes = BetterSegmentedShapes.middle(), - title = { Text(text = stringResource(R.string.pref_show_exit_name)) }, - summary = { Text(text = stringResource(R.string.pref_show_exit_desc)) }, - modifier = Modifier.animateItem(), - ) - } - item(key = "start_on_boot") { SwitchPreference( checked = startOnBoot, @@ -1016,6 +1042,7 @@ private fun PreviewSettingsScreen() { manualMode = false, allowAutoMode = true, preRunAction = null, + showDetails = true, showExit = false, folderStates = SyncthingService.FolderStates(), deviceStates = SyncthingService.DeviceStates(), @@ -1042,9 +1069,10 @@ private fun PreviewSettingsScreen() { respectBatterySaver = true, respectAutoSyncData = true, keepAlive = false, + showDetails = true, + showExit = false, remoteControl = false, allowAutoMode = true, - showExit = false, startOnBoot = true, isDebugMode = true, onInhibitBatteryOptGrant = {}, @@ -1066,9 +1094,10 @@ private fun PreviewSettingsScreen() { onRespectAutoSyncDataChange = {}, onSyncScheduleSettingsOpen = {}, onKeepAliveChange = {}, + onShowDetailsChange = {}, + onShowExitChange = {}, onRemoteControlChange = {}, onAllowAutoModeChange = {}, - onShowExitChange = {}, onStartOnBootChange = {}, onDebugModeChange = {}, onSourceRepoOpen = {}, diff --git a/app/src/main/java/com/chiller3/basicsync/syncthing/SyncthingService.kt b/app/src/main/java/com/chiller3/basicsync/syncthing/SyncthingService.kt index 7909d2be..a2f38404 100644 --- a/app/src/main/java/com/chiller3/basicsync/syncthing/SyncthingService.kt +++ b/app/src/main/java/com/chiller3/basicsync/syncthing/SyncthingService.kt @@ -52,6 +52,7 @@ class SyncthingService : Service(), SyncthingStatusReceiver, DeviceStateListener ) private val STATE_CHANGE_PREFS = arrayOf( Preferences.PREF_KEEP_ALIVE, + Preferences.PREF_SHOW_DETAILS, Preferences.PREF_SHOW_EXIT, ) @@ -174,10 +175,25 @@ class SyncthingService : Service(), SyncthingStatusReceiver, DeviceStateListener private val manualMode: Boolean, private val allowAutoMode: Boolean, private val preRunAction: PreRunAction?, + val showDetails: Boolean, private val showExit: Boolean, val folderStates: FolderStates, val deviceStates: DeviceStates, ) { + fun equivalent(prev: ServiceState?): Boolean = + prev != null + && keepAlive == prev.keepAlive + && blockedReasons == prev.blockedReasons + && isStarted == prev.isStarted + && isResumed == prev.isResumed + && manualMode == prev.manualMode + && allowAutoMode == prev.allowAutoMode + && preRunAction == prev.preRunAction + && showDetails == prev.showDetails + && showExit == prev.showExit + && (!showDetails || (folderStates == prev.folderStates + && deviceStates == prev.deviceStates)) + private val shouldResume: Boolean get() = blockedReasons.isEmpty() @@ -654,7 +670,7 @@ class SyncthingService : Service(), SyncthingStatusReceiver, DeviceStateListener return } - val notificationState = ServiceState( + val serviceState = ServiceState( keepAlive = prefs.keepAlive, blockedReasons = blockedReasons, isStarted = isStarted, @@ -662,43 +678,49 @@ class SyncthingService : Service(), SyncthingStatusReceiver, DeviceStateListener manualMode = prefs.isManualMode, allowAutoMode = prefs.allowAutoMode, preRunAction = currentPreRunAction, + showDetails = prefs.showDetails, showExit = prefs.showExit, folderStates = syncthingFolderStates, deviceStates = syncthingDeviceStates, ) - val wasChanged = notificationState != lastServiceState + val wasChanged = serviceState != lastServiceState if (wasChanged || forceShowNotification) { if (wasChanged) { - deviceStateTracker.updateBusyFolders(notificationState.folderStates) - deviceStateTracker.updateConnectedDevices(notificationState.deviceStates) + deviceStateTracker.updateBusyFolders(serviceState.folderStates) + deviceStateTracker.updateConnectedDevices(serviceState.deviceStates) val guiInfo = guiInfo - allListeners { it.onRunStateChanged(notificationState, guiInfo) } + allListeners { it.onRunStateChanged(serviceState, guiInfo) } } - val (id, notification) = notifications.createPersistentNotification(notificationState) val useLocation = deviceStateTracker.canUseLocation() - var type = 0 + val locationChanged = useLocation != lastUseLocation + val notificationChanged = !serviceState.equivalent(lastServiceState) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && useLocation) { - type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION - } + if (locationChanged || notificationChanged || forceShowNotification) { + val (id, notification) = notifications.createPersistentNotification(serviceState) + var type = 0 - ServiceCompat.startForeground(this, id, notification, type) - notifications.cancelOppositePersistentNotification(id) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && useLocation) { + type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION + } + + ServiceCompat.startForeground(this, id, notification, type) + notifications.cancelOppositePersistentNotification(id) + } - if (lastUseLocation != useLocation) { + if (locationChanged) { deviceStateTracker.refreshNetworkState() lastUseLocation = useLocation } - lastServiceState = notificationState + lastServiceState = serviceState } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8c7157e2..764bb35f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -10,6 +10,8 @@ Configuration Run conditions + + Notifications Advanced @@ -100,6 +102,10 @@ Show Exit button Show an Exit button in the persistent notification. The app will automatically start again after a reboot or when receiving a remote control command. + + Detailed notifications + + Show the status of shared folders and connected devices in the persistent notification. Start on boot