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
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 @@ -25,6 +25,7 @@ class Preferences(context: Context) {
const val PREF_REMOTE_CONTROL = "remote_control"
const val PREF_ALLOW_AUTO_MODE = "allow_auto_mode"
const val PREF_START_ON_BOOT = "start_on_boot"
const val PREF_PROXY_OVERRIDE = "proxy_override"
const val PREF_REQUIRE_UNMETERED_NETWORK = "require_unmetered_network"
const val PREF_NETWORK_ALLOW_WIFI = "network_allow_wifi"
const val PREF_NETWORK_ALLOW_CELLULAR = "network_allow_cellular"
Expand Down Expand Up @@ -97,6 +98,10 @@ class Preferences(context: Context) {
get() = prefs.getBoolean(PREF_START_ON_BOOT, true)
set(enabled) = prefs.edit { putBoolean(PREF_START_ON_BOOT, enabled) }

var proxyOverride: String?
get() = prefs.getString(PREF_PROXY_OVERRIDE, null)
set(url) = prefs.edit { putString(PREF_PROXY_OVERRIDE, url?.ifEmpty { null }) }

var requireUnmeteredNetwork: Boolean
get() = prefs.getBoolean(PREF_REQUIRE_UNMETERED_NETWORK, true)
set(enabled) = prefs.edit { putBoolean(PREF_REQUIRE_UNMETERED_NETWORK, enabled) }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* SPDX-FileCopyrightText: 2026 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/

package com.chiller3.basicsync.settings

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import androidx.core.net.toUri
import com.chiller3.basicsync.R

@Composable
fun ProxyOverrideDialog(
initialUrl: String?,
onSelect: (String?) -> Unit,
onDismiss: () -> Unit,
) {
val input = rememberTextFieldState(initialText = initialUrl ?: "")
val url = tryParseInput(input.text.toString())

AlertDialog(
title = { Text(text = stringResource(R.string.pref_proxy_override_name)) },
text = {
Column(modifier = Modifier.verticalScroll(state = rememberScrollState())) {
Text(text = stringResource(R.string.pref_proxy_override_desc))

OutlinedTextField(
state = input,
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
label = { Text(text = stringResource(R.string.dialog_proxy_override_hint)) },
placeholder = { Text(text = "{http,https,socks5}://1.2.3.4:5678") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri),
lineLimits = TextFieldLineLimits.SingleLine,
)
}
},
onDismissRequest = onDismiss,
confirmButton = {
TextButton(
onClick = { onSelect(url!!.ifEmpty { null }) },
enabled = url != null,
) {
Text(text = stringResource(android.R.string.ok))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(text = stringResource(android.R.string.cancel))
}
},
properties = DialogProperties(
dismissOnBackPress = false,
dismissOnClickOutside = false,
),
)
}

private fun tryParseInput(input: String): String? {
if (input.isEmpty()) {
return input
}

val uri = input.toUri()

if ((uri.scheme == "http" || uri.scheme == "https" || uri.scheme == "socks5")
&& !uri.authority.isNullOrEmpty()) {
return input
}

return null
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ fun SettingsScreen(
val remoteControl = remember(reloadPrefs) { prefs.remoteControl }
val allowAutoMode = remember(reloadPrefs) { prefs.allowAutoMode }
val startOnBoot = remember(reloadPrefs) { prefs.startOnBoot }
val proxyOverride = remember(reloadPrefs) { prefs.proxyOverride }
val isDebugMode = remember(reloadPrefs) { prefs.isDebugMode }

var reloadPerms by remember { mutableIntStateOf(0) }
Expand Down Expand Up @@ -336,6 +337,7 @@ fun SettingsScreen(
remoteControl = remoteControl,
allowAutoMode = allowAutoMode,
startOnBoot = startOnBoot,
proxyOverride = proxyOverride,
isDebugMode = isDebugMode,
onInhibitBatteryOptGrant = {
requestInhibitBatteryOpt.launch(Permissions.getInhibitBatteryOptIntent(context))
Expand Down Expand Up @@ -470,6 +472,10 @@ fun SettingsScreen(
prefs.startOnBoot = enabled
reloadPrefs++
},
onProxyOverrideChange = { url ->
prefs.proxyOverride = url
reloadPrefs++
},
onDebugModeChange = { enabled ->
prefs.isDebugMode = enabled
reloadPrefs++
Expand Down Expand Up @@ -577,6 +583,7 @@ private fun SettingsContent(
remoteControl: Boolean,
allowAutoMode: Boolean,
startOnBoot: Boolean,
proxyOverride: String?,
isDebugMode: Boolean,
onInhibitBatteryOptGrant: () -> Unit,
onNotificationsGrant: () -> Unit,
Expand All @@ -602,6 +609,7 @@ private fun SettingsContent(
onRemoteControlChange: (Boolean) -> Unit,
onAllowAutoModeChange: (Boolean) -> Unit,
onStartOnBootChange: (Boolean) -> Unit,
onProxyOverrideChange: (String?) -> Unit,
onDebugModeChange: (Boolean) -> Unit,
onSourceRepoOpen: () -> Unit,
onSaveLogs: () -> Unit,
Expand Down Expand Up @@ -668,6 +676,7 @@ private fun SettingsContent(
val runState = serviceState?.runState

var showMinBatteryLevelDialog by rememberSaveable { mutableStateOf(false) }
var showProxyOverrideDialog by rememberSaveable { mutableStateOf(false) }

PreferenceColumn(contentPadding = contentPadding) {
if (missingPermissions.isNotEmpty()) {
Expand Down Expand Up @@ -914,13 +923,23 @@ private fun SettingsContent(
SwitchPreference(
checked = startOnBoot,
onCheckedChange = onStartOnBootChange,
shapes = BetterSegmentedShapes.bottom(),
shapes = BetterSegmentedShapes.middle(),
title = { Text(text = stringResource(R.string.pref_start_on_boot_name)) },
summary = { Text(text = stringResource(R.string.pref_start_on_boot_desc)) },
modifier = Modifier.animateItem(),
)
}

item(key = "proxy_override") {
Preference(
onClick = { showProxyOverrideDialog = true },
shapes = BetterSegmentedShapes.bottom(),
title = { Text(text = stringResource(R.string.pref_proxy_override_name)) },
summary = { Text(text = stringResource(R.string.pref_proxy_override_desc)) },
modifier = Modifier.animateItem(),
)
}

item(key = "about") {
PreferenceCategory(
title = { Text(text = stringResource(R.string.pref_header_about)) },
Expand Down Expand Up @@ -971,6 +990,19 @@ private fun SettingsContent(
},
)
}

if (showProxyOverrideDialog) {
ProxyOverrideDialog(
initialUrl = proxyOverride,
onSelect = { url ->
onProxyOverrideChange(url)
showProxyOverrideDialog = false
},
onDismiss = {
showProxyOverrideDialog = false
},
)
}
}

@Composable
Expand Down Expand Up @@ -1074,6 +1106,7 @@ private fun PreviewSettingsScreen() {
remoteControl = false,
allowAutoMode = true,
startOnBoot = true,
proxyOverride = null,
isDebugMode = true,
onInhibitBatteryOptGrant = {},
onNotificationsGrant = {},
Expand All @@ -1099,6 +1132,7 @@ private fun PreviewSettingsScreen() {
onRemoteControlChange = {},
onAllowAutoModeChange = {},
onStartOnBootChange = {},
onProxyOverrideChange = {},
onDebugModeChange = {},
onSourceRepoOpen = {},
onSaveLogs = {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -440,8 +440,11 @@ class SyncthingService : Service(), SyncthingStatusReceiver, DeviceStateListener
private lateinit var deviceStateTracker: DeviceStateTracker
@GuardedBy("stateLock")
private var deviceState = DeviceState()

@GuardedBy("stateLock")
private var desiredProxyInfo = ProxyInfo(proxy = "", noProxy = "")
@GuardedBy("stateLock")
private var runningProxyInfo: ProxyInfo? = null
private var runningProxyInfo = desiredProxyInfo

@GuardedBy("stateLock")
private var blockedReasons = EnumSet.noneOf(BlockedReason::class.java)
Expand Down Expand Up @@ -632,6 +635,7 @@ class SyncthingService : Service(), SyncthingStatusReceiver, DeviceStateListener
Log.d(TAG, "Preference $key changed")

var recomputeBlockedReasons = false
var recomputeProxyInfo = false

// We have to switch foreground service and network callback types when location becomes
// needed or no longer needed.
Expand All @@ -640,19 +644,21 @@ class SyncthingService : Service(), SyncthingStatusReceiver, DeviceStateListener
when (key) {
in BLOCKED_REASONS_PREFS, in DeviceState.PREFS -> recomputeBlockedReasons = true
in STATE_CHANGE_PREFS -> {}
Preferences.PREF_PROXY_OVERRIDE -> recomputeProxyInfo = true
else -> return
}

stateChanged(
recomputeBlockedReasons = recomputeBlockedReasons,
recomputeProxyInfo = recomputeProxyInfo,
forceShowNotification = forceShowNotification,
)
}

override fun onDeviceStateChanged(state: DeviceState) {
synchronized(stateLock) {
deviceState = state
stateChanged(recomputeBlockedReasons = true)
stateChanged(recomputeBlockedReasons = true, recomputeProxyInfo = true)
}
}

Expand All @@ -678,6 +684,7 @@ class SyncthingService : Service(), SyncthingStatusReceiver, DeviceStateListener

private fun stateChanged(
recomputeBlockedReasons: Boolean = false,
recomputeProxyInfo: Boolean = false,
forceShowNotification: Boolean = false,
) {
synchronized(stateLock) {
Expand All @@ -702,6 +709,12 @@ class SyncthingService : Service(), SyncthingStatusReceiver, DeviceStateListener
}
}

if (recomputeProxyInfo) {
desiredProxyInfo = prefs.proxyOverride
?.let { ProxyInfo(proxy = it, noProxy = "") }
?: deviceState.proxyInfo
}

handleStateChangeLocked()

if (!shouldThreadRun) {
Expand Down Expand Up @@ -773,7 +786,7 @@ class SyncthingService : Service(), SyncthingStatusReceiver, DeviceStateListener
// The service needs to be restarted for proxy changes to take effect. The hack we do to set
// the proxy on the golang side can't be made thread-safe.
val needFullRestart = !shouldThreadRun
|| runningProxyInfo != deviceState.proxyInfo
|| runningProxyInfo != desiredProxyInfo
|| preRunActions.isNotEmpty()
|| recheckPermissions

Expand Down Expand Up @@ -811,8 +824,8 @@ class SyncthingService : Service(), SyncthingStatusReceiver, DeviceStateListener
actions.addAll(preRunActions)
preRunActions.clear()

runningProxyInfo = deviceState.proxyInfo
proxyInfo = deviceState.proxyInfo
runningProxyInfo = desiredProxyInfo
proxyInfo = desiredProxyInfo

// recheckPermissions is cleared in onCheckStoragePermissions().
recheckOnly = !shouldStart
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@
<string name="pref_start_on_boot_name">Start on boot</string>
<!-- Description for the preference to start the app after a reboot. -->
<string name="pref_start_on_boot_desc">Automatically start the app after a reboot.</string>
<!-- Title for the preference to override Android's proxy settings. -->
<string name="pref_proxy_override_name">Override proxy settings</string>
<!-- Description for the preference to override Android's proxy settings. -->
<string name="pref_proxy_override_desc">Use a custom proxy instead of the proxy specified in Android\'s network settings.</string>
<!-- Title for the preference that shows the version number. The description is two lines: the first line is BasicSync's version and the second line in Syncthing's version. -->
<string name="pref_version_name">Version</string>
<!-- Title for the debug preference that saves logs to a file. -->
Expand Down Expand Up @@ -248,6 +252,8 @@
<string name="dialog_wifi_network_message">Enter the name (SSID) of the Wi-Fi network. This also works for hidden Wi-Fi networks.</string>
<!-- Textbox hint shown in the dialog for manually adding an allowed Wi-Fi network. -->
<string name="dialog_wifi_network_hint">SSID</string>
<!-- Textbox hint shown in the dialog for overriding the proxy URL. -->
<string name="dialog_proxy_override_hint">Proxy URL</string>

<!-- Tooltip shown in Syncthing's web UI for selecting a local folder when adding a new shared Syncthing folder. Keep as short as possible. -->
<string name="web_ui_select_folder">Select folder</string>
Expand Down
2 changes: 1 addition & 1 deletion external/syncthing
Loading
Loading