Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions app/src/main/java/com/chiller3/basicsync/Notifications.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2022-2025 Andrew Gunnerson
* SPDX-FileCopyrightText: 2022-2026 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/

Expand Down Expand Up @@ -62,7 +62,8 @@ class Notifications(private val context: Context) {
}

fun createPersistentNotification(state: SyncthingService.ServiceState): Notification {
val titleResId = when (state.runState) {
val runState = state.runState
val titleResId = when (runState) {
SyncthingService.RunState.RUNNING -> R.string.notification_persistent_running_title
SyncthingService.RunState.NOT_RUNNING -> R.string.notification_persistent_not_running_title
SyncthingService.RunState.PAUSED -> R.string.notification_persistent_paused_title
Expand All @@ -79,6 +80,18 @@ class Notifications(private val context: Context) {
setOngoing(true)
setOnlyAlertOnce(true)

if (runState.showBlockedReasons) {
setContentText(buildString {
for ((i, reason) in state.blockedReasons.withIndex()) {
if (i > 0) {
append('\n')
}
append(reason.toString(context))
}
})
style = Notification.BigTextStyle()
}

for (action in state.actions) {
val actionTextResId = when (action) {
SyncthingService.ACTION_AUTO_MODE -> R.string.notification_action_auto_mode
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2025 Andrew Gunnerson
* SPDX-FileCopyrightText: 2025-2026 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/

Expand All @@ -20,8 +20,8 @@ abstract class ServiceBaseViewModel(application: Application) : AndroidViewModel
ServiceConnection, SyncthingService.ServiceListener {
protected var binder: SyncthingService.ServiceBinder? = null

private val _runState = MutableStateFlow<SyncthingService.RunState?>(null)
val runState = _runState.asStateFlow()
private val _serviceState = MutableStateFlow<SyncthingService.ServiceState?>(null)
val serviceState = _serviceState.asStateFlow()

private val _guiInfo = MutableStateFlow<SyncthingService.GuiInfo?>(null)
val guiInfo = _guiInfo.asStateFlow()
Expand Down Expand Up @@ -59,10 +59,10 @@ abstract class ServiceBaseViewModel(application: Application) : AndroidViewModel
}

override fun onRunStateChanged(
state: SyncthingService.RunState,
state: SyncthingService.ServiceState,
guiInfo: SyncthingService.GuiInfo?,
) {
_runState.update { state }
_serviceState.update { state }
_guiInfo.update { guiInfo }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,32 +201,45 @@ class SettingsFragment : PreferenceBaseFragment(), FragmentResultListener,

lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
viewModel.runState.collect {
prefOpenWebUi.isEnabled = it != null && it.webUiAvailable
viewModel.serviceState.collect { state ->
val runState = state?.runState

serviceAvailable = it != null
prefOpenWebUi.isEnabled = runState != null && runState.webUiAvailable

serviceAvailable = runState != null
refreshImportExport()

prefServiceStatus.isChecked = it == SyncthingService.RunState.RUNNING
|| it == SyncthingService.RunState.STARTING
prefServiceStatus.summary = when (it) {
SyncthingService.RunState.RUNNING ->
getString(R.string.notification_persistent_running_title)
SyncthingService.RunState.NOT_RUNNING ->
getString(R.string.notification_persistent_not_running_title)
SyncthingService.RunState.PAUSED ->
getString(R.string.notification_persistent_paused_title)
SyncthingService.RunState.STARTING ->
getString(R.string.notification_persistent_starting_title)
SyncthingService.RunState.STOPPING ->
getString(R.string.notification_persistent_stopping_title)
SyncthingService.RunState.PAUSING ->
getString(R.string.notification_persistent_pausing_title)
SyncthingService.RunState.IMPORTING ->
getString(R.string.notification_persistent_importing_title)
SyncthingService.RunState.EXPORTING ->
getString(R.string.notification_persistent_exporting_title)
null -> null
prefServiceStatus.isChecked = runState == SyncthingService.RunState.RUNNING
|| runState == SyncthingService.RunState.STARTING
prefServiceStatus.summary = runState?.let {
buildString {
val statusText = when (runState) {
SyncthingService.RunState.RUNNING ->
getString(R.string.notification_persistent_running_title)
SyncthingService.RunState.NOT_RUNNING ->
getString(R.string.notification_persistent_not_running_title)
SyncthingService.RunState.PAUSED ->
getString(R.string.notification_persistent_paused_title)
SyncthingService.RunState.STARTING ->
getString(R.string.notification_persistent_starting_title)
SyncthingService.RunState.STOPPING ->
getString(R.string.notification_persistent_stopping_title)
SyncthingService.RunState.PAUSING ->
getString(R.string.notification_persistent_pausing_title)
SyncthingService.RunState.IMPORTING ->
getString(R.string.notification_persistent_importing_title)
SyncthingService.RunState.EXPORTING ->
getString(R.string.notification_persistent_exporting_title)
}
append(statusText)

if (runState.showBlockedReasons) {
for (reason in state.blockedReasons) {
append('\n')
append(reason.toString(context))
}
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2025 Andrew Gunnerson
* SPDX-FileCopyrightText: 2025-2026 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/

Expand Down Expand Up @@ -206,8 +206,8 @@ class WebUiActivity : AppCompatActivity() {

lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
viewModel.runState.collect {
if (it != null && !it.webUiAvailable) {
viewModel.serviceState.collect {
if (it?.runState?.webUiAvailable == false) {
finish()
}
}
Expand Down
99 changes: 69 additions & 30 deletions app/src/main/java/com/chiller3/basicsync/syncthing/DeviceState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import android.os.SystemClock
import android.util.Log
import com.chiller3.basicsync.Permissions
import com.chiller3.basicsync.Preferences
import com.chiller3.basicsync.R
import java.util.EnumSet
import kotlin.math.max
import kotlin.math.roundToInt

Expand All @@ -42,6 +44,36 @@ data class ProxyInfo(
val noProxy: String,
)

enum class BlockedReason {
MANUAL,
DISCONNECTED,
METERED_NETWORK,
BAD_NETWORK_TYPE,
BAD_WIFI_SSID,
ON_BATTERY,
LOW_BATTERY,
BATTERY_SAVER,
AUTO_SYNC_DATA,
TIME_SCHEDULE;

fun toString(context: Context): String {
val stringId = when (this) {
MANUAL -> R.string.blocked_reason_manual
DISCONNECTED -> R.string.blocked_reason_disconnected
METERED_NETWORK -> R.string.blocked_reason_metered_network
BAD_NETWORK_TYPE -> R.string.blocked_reason_bad_network_type
BAD_WIFI_SSID -> R.string.blocked_reason_bad_wifi_ssid
ON_BATTERY -> R.string.blocked_reason_on_battery
LOW_BATTERY -> R.string.blocked_reason_low_battery
BATTERY_SAVER -> R.string.blocked_reason_battery_saver
AUTO_SYNC_DATA -> R.string.blocked_reason_auto_sync_data
TIME_SCHEDULE -> R.string.blocked_reason_time_schedule
}

return context.getString(stringId)
}
}

data class DeviceState(
val isNetworkConnected: Boolean = false,
val isNetworkUnmetered: Boolean = false,
Expand Down Expand Up @@ -85,33 +117,35 @@ data class DeviceState(
allowed == ssid?.let(::normalizeSsid)
}

fun canRun(prefs: Preferences): Boolean {
fun blockedReasons(prefs: Preferences): EnumSet<BlockedReason> {
val reasons = EnumSet.noneOf(BlockedReason::class.java)

if (!isNetworkConnected) {
Log.d(TAG, "Blocked due to lack of network connectivity")
return false
}

if (prefs.requireUnmeteredNetwork && !isNetworkUnmetered) {
Log.d(TAG, "Blocked due to unmetered network requirement")
return false
}
reasons.add(BlockedReason.DISCONNECTED)
} else {
if (prefs.requireUnmeteredNetwork && !isNetworkUnmetered) {
Log.d(TAG, "Blocked due to unmetered network requirement")
reasons.add(BlockedReason.METERED_NETWORK)
}

val networkAllowed = when (networkType) {
NetworkType.WIFI -> prefs.networkAllowWifi
NetworkType.CELLULAR -> prefs.networkAllowCellular
NetworkType.ETHERNET -> prefs.networkAllowEthernet
NetworkType.OTHER -> prefs.networkAllowOther
}
if (!networkAllowed) {
Log.d(TAG, "Blocked due to disallowed network interface type: $networkType")
return false
}
val networkAllowed = when (networkType) {
NetworkType.WIFI -> prefs.networkAllowWifi
NetworkType.CELLULAR -> prefs.networkAllowCellular
NetworkType.ETHERNET -> prefs.networkAllowEthernet
NetworkType.OTHER -> prefs.networkAllowOther
}
if (!networkAllowed) {
Log.d(TAG, "Blocked due to disallowed network interface type: $networkType")
reasons.add(BlockedReason.BAD_NETWORK_TYPE)
}

val allowedWifiNetworks = prefs.allowedWifiNetworks
if (networkType == NetworkType.WIFI && allowedWifiNetworks.isNotEmpty()
&& allowedWifiNetworks.none { ssidMatches(it, wifiSsid) }) {
Log.d(TAG, "Blocked due to disallowed network: ssid=$wifiSsid")
return false
val allowedWifiNetworks = prefs.allowedWifiNetworks
if (networkType == NetworkType.WIFI && allowedWifiNetworks.isNotEmpty()
&& allowedWifiNetworks.none { ssidMatches(it, wifiSsid) }) {
Log.d(TAG, "Blocked due to disallowed network: ssid=$wifiSsid")
reasons.add(BlockedReason.BAD_WIFI_SSID)
}
}

if (!isPluggedIn) {
Expand All @@ -120,30 +154,35 @@ data class DeviceState(

if (!runOnBattery) {
Log.d(TAG, "Blocked due to battery power source")
return false
reasons.add(BlockedReason.ON_BATTERY)
} else if (batteryLevel < minBatteryLevel) {
Log.d(TAG, "Blocked due to low battery level: $batteryLevel < $minBatteryLevel")
return false
reasons.add(BlockedReason.LOW_BATTERY)
}
}

if (prefs.respectBatterySaver && isBatterySaver) {
Log.d(TAG, "Blocked due to battery saver mode")
return false
reasons.add(BlockedReason.BATTERY_SAVER)
}

if (prefs.respectAutoSyncData && !isAutoSyncData) {
Log.d(TAG, "Blocked due to auto-sync data status")
return false
reasons.add(BlockedReason.AUTO_SYNC_DATA)
}

if (!isInTimeWindow) {
Log.d(TAG, "Blocked due to execution time window")
return false
reasons.add(BlockedReason.TIME_SCHEDULE)
}

if (reasons.isEmpty()) {
Log.d(TAG, "Permitted to run")
} else {
Log.d(TAG, "Blocked reasons: $reasons")
}

Log.d(TAG, "Permitted to run")
return true
return reasons
}
}

Expand Down
Loading