diff --git a/app/src/main/java/com/chiller3/basicsync/Notifications.kt b/app/src/main/java/com/chiller3/basicsync/Notifications.kt index 4952bdc..77cdb39 100644 --- a/app/src/main/java/com/chiller3/basicsync/Notifications.kt +++ b/app/src/main/java/com/chiller3/basicsync/Notifications.kt @@ -109,12 +109,14 @@ class Notifications(private val context: Context) { if (runState.showFolderStates) { setContentText(buildString { append(context.resources.getQuantityString( - R.plurals.connected_devices, - state.connectedDevices, - state.connectedDevices, + R.plurals.device_state_connected, + state.deviceStates.connected, + state.deviceStates.connected, )) for ((resId, count) in arrayOf( + R.plurals.device_state_syncing to state.deviceStates.syncing, + R.plurals.device_state_pending to state.deviceStates.pending, 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, 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 8e7ecef..ae7d57b 100644 --- a/app/src/main/java/com/chiller3/basicsync/settings/SettingsScreen.kt +++ b/app/src/main/java/com/chiller3/basicsync/settings/SettingsScreen.kt @@ -926,7 +926,7 @@ private fun PreviewSettingsScreen() { preRunAction = null, showExit = false, folderStates = SyncthingService.FolderStates(), - connectedDevices = 0, + deviceStates = SyncthingService.DeviceStates(), ) 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 3c0def7..4df36fb 100644 --- a/app/src/main/java/com/chiller3/basicsync/syncthing/DeviceState.kt +++ b/app/src/main/java/com/chiller3/basicsync/syncthing/DeviceState.kt @@ -535,7 +535,9 @@ class DeviceStateTracker(private val context: Context) : } } - fun updateConnectedDevices(count: Int) { + fun updateConnectedDevices(deviceStates: SyncthingService.DeviceStates) { + val count = deviceStates.connected + handler.post { if (state.connectedDevices != count) { val connectedBefore = state.connectedOnce 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 5e2a7eb..80d4dac 100644 --- a/app/src/main/java/com/chiller3/basicsync/syncthing/SyncthingService.kt +++ b/app/src/main/java/com/chiller3/basicsync/syncthing/SyncthingService.kt @@ -89,7 +89,7 @@ class SyncthingService : Service(), SyncthingStatusReceiver, DeviceStateListener private val preRunAction: PreRunAction?, private val showExit: Boolean, val folderStates: FolderStates, - val connectedDevices: Int, + val deviceStates: DeviceStates, ) { private val shouldResume: Boolean get() = blockedReasons.isEmpty() @@ -202,6 +202,18 @@ class SyncthingService : Service(), SyncthingStatusReceiver, DeviceStateListener ) } + data class DeviceStates( + val connected: Int, + val syncing: Int, + val pending: Int, + ) { + constructor() : this( + connected = 0, + syncing = 0, + pending = 0, + ) + } + private lateinit var prefs: Preferences private lateinit var notifications: Notifications private val runnerThread = Thread(::runner) @@ -266,7 +278,7 @@ class SyncthingService : Service(), SyncthingStatusReceiver, DeviceStateListener @GuardedBy("stateLock") private var syncthingFolderStates = FolderStates() @GuardedBy("stateLock") - private var syncthingConnectedDevices = 0 + private var syncthingDeviceStates = DeviceStates() private val isResumed: Boolean @GuardedBy("stateLock") @@ -471,7 +483,7 @@ class SyncthingService : Service(), SyncthingStatusReceiver, DeviceStateListener preRunAction = currentPreRunAction, showExit = prefs.showExit, folderStates = syncthingFolderStates, - connectedDevices = syncthingConnectedDevices, + deviceStates = syncthingDeviceStates, ) val wasChanged = notificationState != lastServiceState @@ -479,7 +491,7 @@ class SyncthingService : Service(), SyncthingStatusReceiver, DeviceStateListener if (wasChanged || forceShowNotification) { if (wasChanged) { deviceStateTracker.updateBusyFolders(notificationState.folderStates) - deviceStateTracker.updateConnectedDevices(notificationState.connectedDevices) + deviceStateTracker.updateConnectedDevices(notificationState.deviceStates) val guiInfo = guiInfo @@ -627,7 +639,7 @@ class SyncthingService : Service(), SyncthingStatusReceiver, DeviceStateListener syncthingConflicts = emptyList() syncthingAlerts = 0 syncthingFolderStates = FolderStates() - syncthingConnectedDevices = 0 + syncthingDeviceStates = DeviceStates() syncthingApp = null stateChanged() @@ -680,9 +692,13 @@ class SyncthingService : Service(), SyncthingStatusReceiver, DeviceStateListener } @WorkerThread - override fun onConnectedDevicesUpdated(count: Int) { + override fun onDeviceStatesUpdated(connected: Int, syncing: Int, pending: Int) { synchronized(stateLock) { - syncthingConnectedDevices = count + syncthingDeviceStates = DeviceStates( + connected = connected, + syncing = syncing, + pending = pending, + ) stateChanged() } } diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index e434737..4751fb1 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -213,7 +213,7 @@ • %d folderów uruchamianych • %d folderów uruchamianych - + • %d podłączone urządzenie • %d podłączone urządzenia • %d podłączonych urządzeń diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 8a426c7..e6d7723 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -189,7 +189,7 @@ • %d klasör başlatılıyor • %d klasör başlatılıyor - + • %d cihaz bağlandı • %d cihaz bağlandı diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 85f14ec..a901f0e 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -177,7 +177,7 @@ • %d 个文件夹正在启动 - + • %d 台设备已连接 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 682fc1d..071221d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -337,10 +337,20 @@ • %d folders starting - + • %d device connected • %d devices connected + + + • %d device syncing + • %d devices syncing + + + + • %d device pending sync + • %d devices pending sync + Auto mode diff --git a/stbridge/stbridge.go b/stbridge/stbridge.go index 3230fd1..115d2ad 100644 --- a/stbridge/stbridge.go +++ b/stbridge/stbridge.go @@ -306,7 +306,11 @@ type SyncthingStatusReceiver interface { starting int32, ) - OnConnectedDevicesUpdated(count int32) + OnDeviceStatesUpdated( + connected int32, + syncing int32, + pending int32, + ) } // db.DB is internal, so it's unnameable and we can't create a function that @@ -436,11 +440,28 @@ func dispatchFolderStates( ) } -func dispatchConnectedDevices( - devicesConnected map[string]struct{}, +type deviceStates struct { + connected map[string]struct{} + dirty map[string]map[string]struct{} +} + +func dispatchDeviceStates( + deviceStates *deviceStates, receiver SyncthingStatusReceiver, ) { - receiver.OnConnectedDevicesUpdated(int32(len(devicesConnected))) + connected := int32(len(deviceStates.connected)) + syncing := int32(0) + pending := int32(0) + + for deviceID, _ := range deviceStates.dirty { + if _, ok := deviceStates.connected[deviceID]; ok { + syncing += 1 + } else { + pending += 1 + } + } + + receiver.OnDeviceStatesUpdated(connected, syncing, pending) } func eventLoop( @@ -463,12 +484,16 @@ func eventLoop( events.StateChanged | events.DeviceConnected | events.DeviceDisconnected | + events.FolderCompletion | events.ConfigSaved, ) defer sub.Unsubscribe() - devicesConnected := map[string]struct{}{} folderStates := map[string]string{} + deviceStates := deviceStates{ + connected: map[string]struct{}{}, + dirty: map[string]map[string]struct{}{}, + } for { select { @@ -580,17 +605,46 @@ func eventLoop( data := evt.Data.(map[string]string) deviceID := data["id"] - devicesConnected[deviceID] = struct{}{} + deviceStates.connected[deviceID] = struct{}{} - dispatchConnectedDevices(devicesConnected, receiver) + dispatchDeviceStates(&deviceStates, receiver) case events.DeviceDisconnected: data := evt.Data.(map[string]string) deviceID := data["id"] - delete(devicesConnected, deviceID) + delete(deviceStates.connected, deviceID) + + dispatchDeviceStates(&deviceStates, receiver) + + case events.FolderCompletion: + data := evt.Data.(map[string]interface{}) + folderID := data["folder"].(string) + deviceID := data["device"].(string) + + needBytes := data["needBytes"].(int64) + needItems := data["needItems"].(int) + needDeletes := data["needDeletes"].(int) + + syncing := needBytes != 0 || needItems != 0 || needDeletes != 0 - dispatchConnectedDevices(devicesConnected, receiver) + if syncing { + if _, ok := deviceStates.dirty[deviceID]; !ok { + deviceStates.dirty[deviceID] = map[string]struct{}{} + } + if _, ok := deviceStates.dirty[deviceID][folderID]; !ok { + deviceStates.dirty[deviceID][folderID] = struct{}{} + } + } else { + if folders, ok := deviceStates.dirty[deviceID]; ok { + delete(folders, folderID) + if len(folders) == 0 { + delete(deviceStates.dirty, deviceID) + } + } + } + + dispatchDeviceStates(&deviceStates, receiver) // When a folder is deleted, we need to manually clear out the // corresponding conflicts because we will not receive deletion @@ -605,23 +659,44 @@ func eventLoop( conflictsInfo.folderPaths[folder.ID], _ = fs.ExpandTilde(folder.Path) } - for key := range conflictsInfo.byFolder { - if _, ok := conflictsInfo.folderPaths[key]; !ok { - delete(conflictsInfo.byFolder, key) + for folderID := range conflictsInfo.byFolder { + if _, ok := conflictsInfo.folderPaths[folderID]; !ok { + delete(conflictsInfo.byFolder, folderID) } } - for key := range folderStates { - if _, ok := conflictsInfo.folderPaths[key]; !ok { - delete(folderStates, key) + dispatchConflicts(conflictsInfo, receiver) + + for folderID := range folderStates { + if _, ok := conflictsInfo.folderPaths[folderID]; !ok { + delete(folderStates, folderID) } } - dispatchConflicts(conflictsInfo, 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. + + devices := map[string]struct{}{} + + for _, device := range cfg.Devices { + devices[device.DeviceID.String()] = struct{}{} + } + + // We don't need to clean up deviceStates.connected because + // we'll always receive a disconnection event when connections + // are closed during deletion. + + for deviceID, folders := range deviceStates.dirty { + for folderID := range folders { + if _, ok := conflictsInfo.folderPaths[folderID]; !ok { + delete(folders, folderID) + } + } + if _, ok := devices[deviceID]; !ok || len(folders) == 0 { + delete(deviceStates.dirty, deviceID) + } + } + + dispatchDeviceStates(&deviceStates, receiver) // A config.Wrapper is needed to determine if a restart is // required. config.Configuration does not contain enough info.