diff --git a/app/src/main/java/com/chiller3/basicsync/Notifications.kt b/app/src/main/java/com/chiller3/basicsync/Notifications.kt index 76d1cfc..0062af1 100644 --- a/app/src/main/java/com/chiller3/basicsync/Notifications.kt +++ b/app/src/main/java/com/chiller3/basicsync/Notifications.kt @@ -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 } @@ -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) diff --git a/app/src/main/java/com/chiller3/basicsync/Preferences.kt b/app/src/main/java/com/chiller3/basicsync/Preferences.kt index 4f1ac36..99b63ed 100644 --- a/app/src/main/java/com/chiller3/basicsync/Preferences.kt +++ b/app/src/main/java/com/chiller3/basicsync/Preferences.kt @@ -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" @@ -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) } diff --git a/app/src/main/java/com/chiller3/basicsync/settings/SettingsFragment.kt b/app/src/main/java/com/chiller3/basicsync/settings/SettingsFragment.kt index d4834fe..4e35c38 100644 --- a/app/src/main/java/com/chiller3/basicsync/settings/SettingsFragment.kt +++ b/app/src/main/java/com/chiller3/basicsync/settings/SettingsFragment.kt @@ -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 @@ -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 -> diff --git a/app/src/main/java/com/chiller3/basicsync/settings/WebUiActivity.kt b/app/src/main/java/com/chiller3/basicsync/settings/WebUiActivity.kt index ce1a8df..481fe18 100644 --- a/app/src/main/java/com/chiller3/basicsync/settings/WebUiActivity.kt +++ b/app/src/main/java/com/chiller3/basicsync/settings/WebUiActivity.kt @@ -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 @@ -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 { @@ -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() } } } 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 6b3d76e..6f0ee2d 100644 --- a/app/src/main/java/com/chiller3/basicsync/syncthing/SyncthingService.kt +++ b/app/src/main/java/com/chiller3/basicsync/syncthing/SyncthingService.kt @@ -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?, ) { @@ -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 + } } } @@ -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() @@ -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 { @@ -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() } } @@ -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, ) @@ -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 { @@ -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() } @@ -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 @@ -585,7 +629,7 @@ class SyncthingService : Service(), SyncthingStatusReceiver, Log.d(TAG, "Scheduling configuration import: $uri") preRunActions.add(PreRunAction.Import(uri)) - triggerRunnerLoopLocked() + handleStateChangeLocked() } } @@ -594,7 +638,7 @@ class SyncthingService : Service(), SyncthingStatusReceiver, Log.d(TAG, "Scheduling configuration export: $uri") preRunActions.add(PreRunAction.Export(uri)) - triggerRunnerLoopLocked() + handleStateChangeLocked() } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c33a698..51f7262 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -33,6 +33,8 @@ Only run if the device is connected to an unmetered network (eg. Wi-Fi). Require sufficient battery level Only run if the battery level is not low or if the device is charging. + Keep Syncthing alive + When Syncthing should not run, pause it instead of shutting it down. The avoids a potential performance hit when scanning large folders on startup. Version Save logs Save logcat logs to a file. @@ -65,8 +67,10 @@ Alerts for errors when starting Syncthing Syncthing is running Syncthing is not running + Syncthing is paused Syncthing is starting Syncthing is stopping + Syncthing is pausing Importing configuration Exporting configuration Failed to run Syncthing diff --git a/app/src/main/res/xml/preferences_root.xml b/app/src/main/res/xml/preferences_root.xml index 1609c8f..491e86d 100644 --- a/app/src/main/res/xml/preferences_root.xml +++ b/app/src/main/res/xml/preferences_root.xml @@ -95,6 +95,13 @@ app:title="@string/pref_require_sufficient_battery_name" app:summary="@string/pref_require_sufficient_battery_desc" app:iconSpaceReserved="false" /> + +