From 94292816ce785ee6cead82265e264e1649235d4f Mon Sep 17 00:00:00 2001 From: Andrew Gunnerson Date: Fri, 7 Aug 2026 00:48:20 -0400 Subject: [PATCH 1/2] 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 From 8fdf483b4ee2413a8436a204d63a8fce4c59035f Mon Sep 17 00:00:00 2001 From: Andrew Gunnerson Date: Fri, 7 Aug 2026 17:15:28 -0400 Subject: [PATCH 2/2] SyncthingService: Store location usage state in ServiceState Signed-off-by: Andrew Gunnerson --- .../basicsync/settings/SettingsScreen.kt | 1 + .../chiller3/basicsync/syncthing/DeviceState.kt | 1 + .../basicsync/syncthing/SyncthingService.kt | 16 ++++++---------- 3 files changed, 8 insertions(+), 10 deletions(-) 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 3dbb0648..0023c315 100644 --- a/app/src/main/java/com/chiller3/basicsync/settings/SettingsScreen.kt +++ b/app/src/main/java/com/chiller3/basicsync/settings/SettingsScreen.kt @@ -1042,6 +1042,7 @@ private fun PreviewSettingsScreen() { manualMode = false, allowAutoMode = true, preRunAction = null, + useLocation = false, showDetails = true, showExit = false, folderStates = SyncthingService.FolderStates(), diff --git a/app/src/main/java/com/chiller3/basicsync/syncthing/DeviceState.kt b/app/src/main/java/com/chiller3/basicsync/syncthing/DeviceState.kt index 315513a7..8394a243 100644 --- a/app/src/main/java/com/chiller3/basicsync/syncthing/DeviceState.kt +++ b/app/src/main/java/com/chiller3/basicsync/syncthing/DeviceState.kt @@ -679,6 +679,7 @@ class DeviceStateTracker(private val context: Context) : // switch to the location-compatible network callback. However, note that even if we used // the location-compatible network callback all the time, Android would still not send us a // new event when the permissions are granted. + Log.d(TAG, "Reregistering network callback") unregisterNetworkCallback() registerNetworkCallback() } 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 a2f38404..90e41192 100644 --- a/app/src/main/java/com/chiller3/basicsync/syncthing/SyncthingService.kt +++ b/app/src/main/java/com/chiller3/basicsync/syncthing/SyncthingService.kt @@ -175,6 +175,7 @@ class SyncthingService : Service(), SyncthingStatusReceiver, DeviceStateListener private val manualMode: Boolean, private val allowAutoMode: Boolean, private val preRunAction: PreRunAction?, + val useLocation: Boolean, val showDetails: Boolean, private val showExit: Boolean, val folderStates: FolderStates, @@ -189,6 +190,7 @@ class SyncthingService : Service(), SyncthingStatusReceiver, DeviceStateListener && manualMode == prev.manualMode && allowAutoMode == prev.allowAutoMode && preRunAction == prev.preRunAction + && useLocation == prev.useLocation && showDetails == prev.showDetails && showExit == prev.showExit && (!showDetails || (folderStates == prev.folderStates @@ -405,8 +407,6 @@ class SyncthingService : Service(), SyncthingStatusReceiver, DeviceStateListener @GuardedBy("stateLock") private var lastServiceState: ServiceState? = null - @GuardedBy("stateLock") - private var lastUseLocation: Boolean = false private lateinit var deviceStateTracker: DeviceStateTracker @GuardedBy("stateLock") @@ -678,6 +678,7 @@ class SyncthingService : Service(), SyncthingStatusReceiver, DeviceStateListener manualMode = prefs.isManualMode, allowAutoMode = prefs.allowAutoMode, preRunAction = currentPreRunAction, + useLocation = deviceStateTracker.canUseLocation(), showDetails = prefs.showDetails, showExit = prefs.showExit, folderStates = syncthingFolderStates, @@ -696,18 +697,14 @@ class SyncthingService : Service(), SyncthingStatusReceiver, DeviceStateListener allListeners { it.onRunStateChanged(serviceState, guiInfo) } } - val useLocation = deviceStateTracker.canUseLocation() - val locationChanged = useLocation != lastUseLocation - val notificationChanged = !serviceState.equivalent(lastServiceState) - - if (locationChanged || notificationChanged || forceShowNotification) { + if (!serviceState.equivalent(lastServiceState) || forceShowNotification) { val (id, notification) = notifications.createPersistentNotification(serviceState) var type = 0 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) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && serviceState.useLocation) { type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION } @@ -715,9 +712,8 @@ class SyncthingService : Service(), SyncthingStatusReceiver, DeviceStateListener notifications.cancelOppositePersistentNotification(id) } - if (locationChanged) { + if (serviceState.useLocation != lastServiceState?.useLocation) { deviceStateTracker.refreshNetworkState() - lastUseLocation = useLocation } lastServiceState = serviceState