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.