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
4 changes: 3 additions & 1 deletion app/src/main/java/com/chiller3/basicsync/Notifications.kt
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,10 @@ class Notifications(private val context: Context) {
val titleResId = when (state.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
SyncthingService.RunState.STARTING -> R.string.notification_persistent_starting_title
SyncthingService.RunState.STOPPING -> R.string.notification_persistent_stopping_title
SyncthingService.RunState.PAUSING -> R.string.notification_persistent_pausing_title
SyncthingService.RunState.IMPORTING -> R.string.notification_persistent_importing_title
SyncthingService.RunState.EXPORTING -> R.string.notification_persistent_exporting_title
}
Expand Down Expand Up @@ -101,7 +103,7 @@ class Notifications(private val context: Context) {
).build())
}

val primaryIntent = if (state.runState == SyncthingService.RunState.RUNNING) {
val primaryIntent = if (state.runState.webUiAvailable) {
Intent(context, WebUiActivity::class.java)
} else {
Intent(context, SettingsActivity::class.java)
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/java/com/chiller3/basicsync/Preferences.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class Preferences(context: Context) {
// Main preferences.
const val PREF_REQUIRE_UNMETERED_NETWORK = "require_unmetered_network"
const val PREF_REQUIRE_SUFFICIENT_BATTERY = "require_sufficient_battery"
const val PREF_KEEP_ALIVE = "keep_alive"

// Main UI actions only.
const val PREF_INHIBIT_BATTERY_OPT = "inhibit_battery_opt"
Expand Down Expand Up @@ -57,6 +58,10 @@ class Preferences(context: Context) {
get() = prefs.getBoolean(PREF_REQUIRE_SUFFICIENT_BATTERY, true)
set(enabled) = prefs.edit { putBoolean(PREF_REQUIRE_SUFFICIENT_BATTERY, enabled) }

var keepAlive: Boolean
get() = prefs.getBoolean(PREF_KEEP_ALIVE, true)
set(enabled) = prefs.edit { putBoolean(PREF_KEEP_ALIVE, enabled) }

var isDebugMode: Boolean
get() = prefs.getBoolean(PREF_DEBUG_MODE, false)
set(enabled) = prefs.edit { putBoolean(PREF_DEBUG_MODE, enabled) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ class SettingsFragment : PreferenceBaseFragment(), Preference.OnPreferenceClickL
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
viewModel.runState.collect {
prefOpenWebUi.isEnabled = it == SyncthingService.RunState.RUNNING
prefOpenWebUi.isEnabled = it != null && it.webUiAvailable

prefImportConfiguration.isEnabled = it != null
prefExportConfiguration.isEnabled = it != null
Expand All @@ -195,10 +195,14 @@ class SettingsFragment : PreferenceBaseFragment(), Preference.OnPreferenceClickL
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 ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.net.toUri
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
Expand All @@ -37,12 +38,10 @@ import androidx.lifecycle.repeatOnLifecycle
import com.chiller3.basicsync.R
import com.chiller3.basicsync.databinding.WebUiActivityBinding
import com.chiller3.basicsync.dialog.FolderPickerDialogFragment
import com.chiller3.basicsync.syncthing.SyncthingService
import kotlinx.coroutines.launch
import java.io.ByteArrayInputStream
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import androidx.core.net.toUri

class WebUiActivity : AppCompatActivity() {
companion object {
Expand Down Expand Up @@ -208,10 +207,8 @@ class WebUiActivity : AppCompatActivity() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
viewModel.runState.collect {
when (it) {
SyncthingService.RunState.NOT_RUNNING,
SyncthingService.RunState.STOPPING -> finish()
else -> {}
if (it != null && !it.webUiAvailable) {
finish()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,22 @@ class SyncthingService : Service(), SyncthingStatusReceiver,
enum class RunState {
RUNNING,
NOT_RUNNING,
PAUSED,
STARTING,
STOPPING,
PAUSING,
IMPORTING,
EXPORTING,
EXPORTING;

val webUiAvailable: Boolean
get() = this == RUNNING || this == PAUSED || this == PAUSING
}

data class ServiceState(
val keepAlive: Boolean,
val shouldRun: Boolean,
val isRunning: Boolean,
val isStarted: Boolean,
val isActive: Boolean,
val manualMode: Boolean,
val preRunAction: PreRunAction?,
) {
Expand All @@ -95,17 +102,33 @@ class SyncthingService : Service(), SyncthingStatusReceiver,
is PreRunAction.Import -> RunState.IMPORTING
is PreRunAction.Export -> RunState.EXPORTING
}
} else if (isRunning) {
if (shouldRun) {
RunState.RUNNING
} else if (isStarted) {
if (isActive) {
if (shouldRun) {
RunState.RUNNING
} else if (keepAlive) {
RunState.PAUSING
} else {
RunState.STOPPING
}
} else {
RunState.STOPPING
if (shouldRun) {
RunState.STARTING
} else if (keepAlive) {
RunState.PAUSED
} else {
RunState.STOPPING
}
}
} else {
if (shouldRun) {
RunState.STARTING
if (isActive) {
throw IllegalArgumentException("Service active, but not running?")
} else {
RunState.NOT_RUNNING
if (shouldRun) {
RunState.STARTING
} else {
RunState.NOT_RUNNING
}
}
}

Expand Down Expand Up @@ -191,6 +214,10 @@ class SyncthingService : Service(), SyncthingStatusReceiver,
autoShouldRun
}

private val shouldStart: Boolean
@GuardedBy("stateLock")
get() = prefs.keepAlive || shouldRun

@GuardedBy("stateLock")
private val preRunActions = mutableListOf<PreRunAction>()

Expand All @@ -200,10 +227,18 @@ class SyncthingService : Service(), SyncthingStatusReceiver,
@GuardedBy("stateLock")
private var syncthingApp: SyncthingApp? = null

private val isRunning: Boolean
private val isStarted: Boolean
@GuardedBy("stateLock")
get() = syncthingApp != null

private val isActive: Boolean
@GuardedBy("stateLock")
get() = if (prefs.keepAlive) {
syncthingApp?.isConnectAllowed ?: false
} else {
isStarted
}

private val guiInfo: GuiInfo?
@GuardedBy("stateLock")
get() = syncthingApp?.let {
Expand Down Expand Up @@ -362,7 +397,8 @@ class SyncthingService : Service(), SyncthingStatusReceiver,
Preferences.PREF_MANUAL_MODE,
Preferences.PREF_MANUAL_SHOULD_RUN,
Preferences.PREF_REQUIRE_UNMETERED_NETWORK,
Preferences.PREF_REQUIRE_SUFFICIENT_BATTERY -> stateChanged()
Preferences.PREF_REQUIRE_SUFFICIENT_BATTERY,
Preferences.PREF_KEEP_ALIVE -> stateChanged()
Preferences.PREF_DEBUG_MODE -> setLogLevel()
}
}
Expand All @@ -376,9 +412,13 @@ class SyncthingService : Service(), SyncthingStatusReceiver,

private fun stateChanged() {
synchronized(stateLock) {
handleStateChangeLocked()

val notificationState = ServiceState(
keepAlive = prefs.keepAlive,
shouldRun = shouldRun,
isRunning = isRunning,
isStarted = isStarted,
isActive = isActive,
manualMode = prefs.isManualMode,
preRunAction = currentPreRunAction,
)
Expand Down Expand Up @@ -408,19 +448,20 @@ class SyncthingService : Service(), SyncthingStatusReceiver,

lastServiceState = notificationState
}

triggerRunnerLoopLocked()
}
}

@GuardedBy("stateLock")
private fun triggerRunnerLoopLocked() {
private fun handleStateChangeLocked() {
val app = syncthingApp

if (runningProxyInfo != deviceProxyInfo
|| preRunActions.isNotEmpty()
|| isRunning != shouldRun) {
if (app != null) {
val needFullRestart = runningProxyInfo != deviceProxyInfo || preRunActions.isNotEmpty()

if (needFullRestart || isStarted != shouldStart || isActive != shouldRun) {
if (!needFullRestart && app != null && prefs.keepAlive) {
Log.d(TAG, "Keep alive enabled; changing connect allowed to $shouldRun")
app.isConnectAllowed = shouldRun
} else if (app != null) {
Log.d(TAG, "Syncthing is running; stopping service")
app.stopAsync()
} else {
Expand All @@ -436,7 +477,8 @@ class SyncthingService : Service(), SyncthingStatusReceiver,
var proxyInfo: ProxyInfo

synchronized(stateLock) {
while (preRunActions.isEmpty() && !shouldRun) {
while (preRunActions.isEmpty() && !shouldStart) {
Log.d(TAG, "Nothing to do; sleeping")
stateLock.wait()
}

Expand Down Expand Up @@ -492,6 +534,8 @@ class SyncthingService : Service(), SyncthingStatusReceiver,
notifications.sendFailureNotification(e)

// For now, just switch to manual mode so that we're not stuck in a restart loop.
// Since Syncthing is not running, this won't result in handleStateChangeLocked()
// just toggling isConnectAllowed.
prefs.manualShouldRun = false
prefs.isManualMode = true

Expand Down Expand Up @@ -585,7 +629,7 @@ class SyncthingService : Service(), SyncthingStatusReceiver,
Log.d(TAG, "Scheduling configuration import: $uri")

preRunActions.add(PreRunAction.Import(uri))
triggerRunnerLoopLocked()
handleStateChangeLocked()
}
}

Expand All @@ -594,7 +638,7 @@ class SyncthingService : Service(), SyncthingStatusReceiver,
Log.d(TAG, "Scheduling configuration export: $uri")

preRunActions.add(PreRunAction.Export(uri))
triggerRunnerLoopLocked()
handleStateChangeLocked()
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
<string name="pref_require_unmetered_network_desc">Only run if the device is connected to an unmetered network (eg. Wi-Fi).</string>
<string name="pref_require_sufficient_battery_name">Require sufficient battery level</string>
<string name="pref_require_sufficient_battery_desc">Only run if the battery level is not low or if the device is charging.</string>
<string name="pref_keep_alive_name">Keep Syncthing alive</string>
<string name="pref_keep_alive_desc">When Syncthing should not run, pause it instead of shutting it down. The avoids a potential performance hit when scanning large folders on startup.</string>
<string name="pref_version_name">Version</string>
<string name="pref_save_logs_name">Save logs</string>
<string name="pref_save_logs_desc">Save logcat logs to a file.</string>
Expand Down Expand Up @@ -65,8 +67,10 @@
<string name="notification_channel_failure_desc">Alerts for errors when starting Syncthing</string>
<string name="notification_persistent_running_title">Syncthing is running</string>
<string name="notification_persistent_not_running_title">Syncthing is not running</string>
<string name="notification_persistent_paused_title">Syncthing is paused</string>
<string name="notification_persistent_starting_title">Syncthing is starting</string>
<string name="notification_persistent_stopping_title">Syncthing is stopping</string>
<string name="notification_persistent_pausing_title">Syncthing is pausing</string>
<string name="notification_persistent_importing_title">Importing configuration</string>
<string name="notification_persistent_exporting_title">Exporting configuration</string>
<string name="notification_failure_run_title">Failed to run Syncthing</string>
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/res/xml/preferences_root.xml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@
app:title="@string/pref_require_sufficient_battery_name"
app:summary="@string/pref_require_sufficient_battery_desc"
app:iconSpaceReserved="false" />

<SwitchPreferenceCompat
app:key="keep_alive"
app:defaultValue="true"
app:title="@string/pref_keep_alive_name"
app:summary="@string/pref_keep_alive_desc"
app:iconSpaceReserved="false" />
</PreferenceCategory>

<PreferenceCategory
Expand Down
15 changes: 15 additions & 0 deletions stbridge/stbridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,21 @@ func (app *SyncthingApp) StopAsync() {
go app.app.Stop(svcutil.ExitSuccess)
}

func (app *SyncthingApp) IsConnectAllowed() bool {
return app.cfg.Options().ConnectAllowed
}

func (app *SyncthingApp) SetConnectAllowed(allowed bool) error {
_, err := app.cfg.Modify(func(cfg *config.Configuration) {
cfg.Options.ConnectAllowed = allowed
})
if err != nil {
return fmt.Errorf("failed to set connect allowed state: %v: %w", allowed, err)
}

return nil
}

func (app *SyncthingApp) GuiAddress() string {
return app.cfg.GUI().URL()
}
Expand Down