From 80264a431d00356063d9d0e40acebf22b06b2bb0 Mon Sep 17 00:00:00 2001 From: Andrew Gunnerson Date: Fri, 5 Jun 2026 22:35:57 -0400 Subject: [PATCH] Show folder and device states in notification Currently, the connected device count is always shown. For the folder states, they are grouped into idle, scanning, syncing, cleaning, errored, and starting. A count is shown for each if it is non-zero. Issue: #150 Signed-off-by: Andrew Gunnerson --- .../com/chiller3/basicsync/Notifications.kt | 25 +++++- .../basicsync/settings/SettingsScreen.kt | 2 + .../basicsync/syncthing/DeviceState.kt | 5 +- .../basicsync/syncthing/SyncthingService.kt | 63 ++++++++++++-- app/src/main/res/values/strings.xml | 36 ++++++++ stbridge/stbridge.go | 85 ++++++++++++++----- 6 files changed, 188 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/com/chiller3/basicsync/Notifications.kt b/app/src/main/java/com/chiller3/basicsync/Notifications.kt index 70b54e9..4952bdc 100644 --- a/app/src/main/java/com/chiller3/basicsync/Notifications.kt +++ b/app/src/main/java/com/chiller3/basicsync/Notifications.kt @@ -106,7 +106,30 @@ class Notifications(private val context: Context) { setOngoing(true) setOnlyAlertOnce(true) - if (runState.showBlockedReasons) { + if (runState.showFolderStates) { + setContentText(buildString { + append(context.resources.getQuantityString( + R.plurals.connected_devices, + state.connectedDevices, + state.connectedDevices, + )) + + for ((resId, count) in arrayOf( + R.plurals.folder_state_idle to state.folderStates.idle, + R.plurals.folder_state_scanning to state.folderStates.scanning, + R.plurals.folder_state_syncing to state.folderStates.syncing, + R.plurals.folder_state_cleaning to state.folderStates.cleaning, + R.plurals.folder_state_errored to state.folderStates.errored, + R.plurals.folder_state_starting to state.folderStates.starting, + )) { + if (count > 0) { + append('\n') + append(context.resources.getQuantityString(resId, count, count)) + } + } + }) + style = Notification.BigTextStyle() + } else if (runState.showBlockedReasons) { setContentText(buildString { for ((i, reason) in state.blockedReasons.withIndex()) { if (i > 0) { 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 d31e6b8..3b94b20 100644 --- a/app/src/main/java/com/chiller3/basicsync/settings/SettingsScreen.kt +++ b/app/src/main/java/com/chiller3/basicsync/settings/SettingsScreen.kt @@ -927,6 +927,8 @@ private fun PreviewSettingsScreen() { allowAutoMode = true, preRunAction = null, showExit = false, + folderStates = SyncthingService.FolderStates(), + connectedDevices = 0, ) AppTheme { 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 93a8c32..3c0def7 100644 --- a/app/src/main/java/com/chiller3/basicsync/syncthing/DeviceState.kt +++ b/app/src/main/java/com/chiller3/basicsync/syncthing/DeviceState.kt @@ -519,7 +519,10 @@ class DeviceStateTracker(private val context: Context) : } } - fun updateBusyFolders(count: Int) { + fun updateBusyFolders(folderStates: SyncthingService.FolderStates) { + // We only want mutating operations to interrupt the idle timer. + val count = folderStates.syncing + folderStates.cleaning + folderStates.starting + handler.post { if (state.busyFolders != count) { alarmManager.cancel(scheduleIdlePendingIntent) 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 affd9b1..5e2a7eb 100644 --- a/app/src/main/java/com/chiller3/basicsync/syncthing/SyncthingService.kt +++ b/app/src/main/java/com/chiller3/basicsync/syncthing/SyncthingService.kt @@ -69,6 +69,9 @@ class SyncthingService : Service(), SyncthingStatusReceiver, DeviceStateListener IMPORTING, EXPORTING; + val showFolderStates: Boolean + get() = this == RUNNING + val showBlockedReasons: Boolean get() = this == NOT_RUNNING || this == PAUSED @@ -85,6 +88,8 @@ class SyncthingService : Service(), SyncthingStatusReceiver, DeviceStateListener private val allowAutoMode: Boolean, private val preRunAction: PreRunAction?, private val showExit: Boolean, + val folderStates: FolderStates, + val connectedDevices: Int, ) { private val shouldResume: Boolean get() = blockedReasons.isEmpty() @@ -179,6 +184,24 @@ class SyncthingService : Service(), SyncthingStatusReceiver, DeviceStateListener } } + data class FolderStates( + val idle: Int, + val scanning: Int, + val syncing: Int, + val cleaning: Int, + val errored: Int, + val starting: Int, + ) { + constructor() : this( + idle = 0, + scanning = 0, + syncing = 0, + cleaning = 0, + errored = 0, + starting = 0, + ) + } + private lateinit var prefs: Preferences private lateinit var notifications: Notifications private val runnerThread = Thread(::runner) @@ -240,6 +263,10 @@ class SyncthingService : Service(), SyncthingStatusReceiver, DeviceStateListener notifications.sendOrClearAlertsNotification(count) } } + @GuardedBy("stateLock") + private var syncthingFolderStates = FolderStates() + @GuardedBy("stateLock") + private var syncthingConnectedDevices = 0 private val isResumed: Boolean @GuardedBy("stateLock") @@ -443,12 +470,17 @@ class SyncthingService : Service(), SyncthingStatusReceiver, DeviceStateListener allowAutoMode = prefs.allowAutoMode, preRunAction = currentPreRunAction, showExit = prefs.showExit, + folderStates = syncthingFolderStates, + connectedDevices = syncthingConnectedDevices, ) val wasChanged = notificationState != lastServiceState if (wasChanged || forceShowNotification) { if (wasChanged) { + deviceStateTracker.updateBusyFolders(notificationState.folderStates) + deviceStateTracker.updateConnectedDevices(notificationState.connectedDevices) + val guiInfo = guiInfo allListeners { it.onRunStateChanged(notificationState, guiInfo) } @@ -592,11 +624,10 @@ class SyncthingService : Service(), SyncthingStatusReceiver, DeviceStateListener Log.i(TAG, "Syncthing successfully stopped") synchronized(stateLock) { - deviceStateTracker.updateBusyFolders(0) - deviceStateTracker.updateConnectedDevices(0) - syncthingConflicts = emptyList() syncthingAlerts = 0 + syncthingFolderStates = FolderStates() + syncthingConnectedDevices = 0 syncthingApp = null stateChanged() @@ -627,13 +658,33 @@ class SyncthingService : Service(), SyncthingStatusReceiver, DeviceStateListener } @WorkerThread - override fun onBusyFoldersUpdated(count: Int) { - deviceStateTracker.updateBusyFolders(count) + override fun onFolderStatesUpdated( + idle: Int, + scanning: Int, + syncing: Int, + cleaning: Int, + errored: Int, + starting: Int, + ) { + synchronized(stateLock) { + syncthingFolderStates = FolderStates( + idle = idle, + scanning = scanning, + syncing = syncing, + cleaning = cleaning, + errored = errored, + starting = starting, + ) + stateChanged() + } } @WorkerThread override fun onConnectedDevicesUpdated(count: Int) { - deviceStateTracker.updateConnectedDevices(count) + synchronized(stateLock) { + syncthingConnectedDevices = count + stateChanged() + } } data class GuiInfo( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 429d538..682fc1d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -306,6 +306,42 @@ • Outside of time schedule + + + • %d folder up to date + • %d folders up to date + + + + • %d folder scanning + • %d folders scanning + + + + • %d folder syncing + • %d folders syncing + + + + • %d folder cleaning old files + • %d folders cleaning old files + + + + • %d folder with errors + • %d folders with errors + + + + • %d folder starting + • %d folders starting + + + + • %d device connected + • %d devices connected + + Auto mode diff --git a/stbridge/stbridge.go b/stbridge/stbridge.go index 54de77f..3230fd1 100644 --- a/stbridge/stbridge.go +++ b/stbridge/stbridge.go @@ -297,7 +297,14 @@ type SyncthingStatusReceiver interface { // Can be sent before OnSyncthingStarted, but not after OnSyncthingStopped. OnAlertsUpdated(count int32) - OnBusyFoldersUpdated(count int32) + OnFolderStatesUpdated( + idle int32, + scanning int32, + syncing int32, + cleaning int32, + errored int32, + starting int32, + ) OnConnectedDevicesUpdated(count int32) } @@ -365,30 +372,68 @@ func dispatchAlerts( receiver.OnAlertsUpdated(int32(count)) } -// The scanning states are intentionally excluded because we only want mutating -// operations to interrupt the idle timer. -var busyEvents = []string{ - model.FolderSyncWaiting.String(), - model.FolderSyncPreparing.String(), - model.FolderSyncing.String(), - model.FolderCleaning.String(), - model.FolderCleanWaiting.String(), - model.FolderStarting.String(), -} +var ( + idleEvents = []string{ + model.FolderIdle.String(), + } + scanEvents = []string{ + model.FolderScanning.String(), + model.FolderScanWaiting.String(), + } + syncEvents = []string{ + model.FolderSyncWaiting.String(), + model.FolderSyncPreparing.String(), + model.FolderSyncing.String(), + } + cleanEvents = []string{ + model.FolderCleaning.String(), + model.FolderCleanWaiting.String(), + } + errorEvents = []string{ + model.FolderError.String(), + } + startEvents = []string{ + model.FolderStarting.String(), + } +) -func dispatchBusyFolders( +func dispatchFolderStates( folderStates map[string]string, receiver SyncthingStatusReceiver, ) { - busyCount := int32(0) - - for _, state := range folderStates { - if slices.Contains(busyEvents, state) { - busyCount += 1 + idle := int32(0) + scanning := int32(0) + syncing := int32(0) + cleaning := int32(0) + errored := int32(0) + starting := int32(0) + + for folderID, state := range folderStates { + if slices.Contains(idleEvents, state) { + idle += 1 + } else if slices.Contains(scanEvents, state) { + scanning += 1 + } else if slices.Contains(syncEvents, state) { + syncing += 1 + } else if slices.Contains(cleanEvents, state) { + cleaning += 1 + } else if slices.Contains(errorEvents, state) { + errored += 1 + } else if slices.Contains(startEvents, state) { + starting += 1 + } else { + log.Printf("Unknown folder state: %v: %v", folderID, state) } } - receiver.OnBusyFoldersUpdated(busyCount) + receiver.OnFolderStatesUpdated( + idle, + scanning, + syncing, + cleaning, + errored, + starting, + ) } func dispatchConnectedDevices( @@ -529,7 +574,7 @@ func eventLoop( folderStates[folder] = state - dispatchBusyFolders(folderStates, receiver) + dispatchFolderStates(folderStates, receiver) case events.DeviceConnected: data := evt.Data.(map[string]string) @@ -573,7 +618,7 @@ func eventLoop( } dispatchConflicts(conflictsInfo, receiver) - dispatchBusyFolders(folderStates, receiver) + dispatchFolderStates(folderStates, receiver) // Unlike folders, we don't need to remove deleted devices from // devicesConnected. We'll always receive a disconnection event // when connections are closed during deletion.