From e9fe0cf2097bfd5b4b7be4290d450ea4d81b3dc6 Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Wed, 7 Oct 2026 00:42:55 -0700 Subject: [PATCH 01/14] feat(android): register boot autostart receiver --- app/src/main/AndroidManifest.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0b4cfea72..10eff0663 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,6 +5,7 @@ + @@ -42,6 +43,15 @@ android:exported="false" android:theme="@android:style/Theme.NoTitleBar" /> + + + + + + Date: Wed, 7 Oct 2026 00:43:18 -0700 Subject: [PATCH 02/14] feat(android): add boot autostart preference --- .../amurcanov/tgwsproxy/AutostartPreferences.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 app/src/main/java/com/amurcanov/tgwsproxy/AutostartPreferences.kt diff --git a/app/src/main/java/com/amurcanov/tgwsproxy/AutostartPreferences.kt b/app/src/main/java/com/amurcanov/tgwsproxy/AutostartPreferences.kt new file mode 100644 index 000000000..1740cc558 --- /dev/null +++ b/app/src/main/java/com/amurcanov/tgwsproxy/AutostartPreferences.kt @@ -0,0 +1,17 @@ +package com.amurcanov.tgwsproxy + +import android.content.SharedPreferences + +internal class AutostartPreferences( + private val prefs: SharedPreferences, +) { + fun isEnabled(): Boolean = prefs.getBoolean(KEY_AUTOSTART_ON_BOOT, false) + + fun setEnabled(enabled: Boolean) { + prefs.edit().putBoolean(KEY_AUTOSTART_ON_BOOT, enabled).apply() + } + + companion object { + const val KEY_AUTOSTART_ON_BOOT = "autostart_on_boot" + } +} From d661d363a5ff41073323221e1fc1207fdb872fc8 Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Wed, 7 Oct 2026 00:43:25 -0700 Subject: [PATCH 03/14] feat(ui): add autostart settings control --- .../tgwsproxy/AutostartSettingsSection.kt | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 app/src/main/java/com/amurcanov/tgwsproxy/AutostartSettingsSection.kt diff --git a/app/src/main/java/com/amurcanov/tgwsproxy/AutostartSettingsSection.kt b/app/src/main/java/com/amurcanov/tgwsproxy/AutostartSettingsSection.kt new file mode 100644 index 000000000..35ef83318 --- /dev/null +++ b/app/src/main/java/com/amurcanov/tgwsproxy/AutostartSettingsSection.kt @@ -0,0 +1,50 @@ +package com.amurcanov.tgwsproxy + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp + +@Composable +internal fun AutostartSettingsSection( + enabled: Boolean, + onEnabledChange: (Boolean) -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Column( + modifier = Modifier + .weight(1f) + .padding(end = 12.dp), + ) { + Text( + text = stringResource(R.string.autostart_on_boot_title), + style = MaterialTheme.typography.bodyLarge, + ) + Text( + text = stringResource(R.string.autostart_on_boot_hint), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 2.dp), + ) + } + Switch( + checked = enabled, + onCheckedChange = onEnabledChange, + ) + } +} From 86a3e207c401308f5e21758ecb1f40736f7bb6cf Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Wed, 7 Oct 2026 00:43:32 -0700 Subject: [PATCH 04/14] refactor(proxy): centralize saved startup config --- .../tgwsproxy/ProxySavedConfigStore.kt | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 app/src/main/java/com/amurcanov/tgwsproxy/ProxySavedConfigStore.kt diff --git a/app/src/main/java/com/amurcanov/tgwsproxy/ProxySavedConfigStore.kt b/app/src/main/java/com/amurcanov/tgwsproxy/ProxySavedConfigStore.kt new file mode 100644 index 000000000..1cf9b3ca0 --- /dev/null +++ b/app/src/main/java/com/amurcanov/tgwsproxy/ProxySavedConfigStore.kt @@ -0,0 +1,67 @@ +package com.amurcanov.tgwsproxy + +import android.content.SharedPreferences + +internal const val PROXY_PREFS_NAME = "ProxyPrefs" + +internal data class ProxySavedConfig( + val port: Int = DEFAULT_LOCAL_PROXY_PORT, + val runtimeConfig: String = "", + val poolSize: Int = 4, + val frontendType: LocalProxyFrontendType = LocalProxyFrontendType.DEFAULT, +) { + fun isSufficientForStart(): Boolean { + if (port !in 1..65535 || poolSize <= 0) { + return false + } + return when (frontendType) { + LocalProxyFrontendType.SOCKS5 -> runtimeConfig.isNotBlank() + LocalProxyFrontendType.MTPROTO_EXPERIMENTAL -> true + } + } +} + +internal class ProxySavedConfigStore( + private val prefs: SharedPreferences, +) { + fun load(): ProxySavedConfig { + val savedPort = prefs.getInt(KEY_LAST_PORT, DEFAULT_LOCAL_PROXY_PORT) + val migrationDone = prefs.getBoolean(KEY_LAST_PORT_DEFAULT_MIGRATED, false) + val port = if (!migrationDone && savedPort == LEGACY_DEFAULT_LOCAL_PROXY_PORT) { + DEFAULT_LOCAL_PROXY_PORT + } else { + savedPort + } + + if (savedPort != port || !migrationDone) { + prefs.edit() + .putInt(KEY_LAST_PORT, port) + .putBoolean(KEY_LAST_PORT_DEFAULT_MIGRATED, true) + .apply() + } + + return ProxySavedConfig( + port = port, + runtimeConfig = prefs.getString(KEY_LAST_IPS, "").orEmpty(), + poolSize = prefs.getInt(KEY_LAST_POOL, 4), + frontendType = LocalProxyFrontendRepository(prefs).load(), + ) + } + + fun save(config: ProxySavedConfig) { + prefs.edit() + .putInt(KEY_LAST_PORT, config.port) + .putBoolean(KEY_LAST_PORT_DEFAULT_MIGRATED, true) + .putString(KEY_LAST_IPS, config.runtimeConfig) + .putInt(KEY_LAST_POOL, config.poolSize) + .apply() + LocalProxyFrontendRepository(prefs).save(config.frontendType) + } + + companion object { + private const val KEY_LAST_PORT = "last_proxy_port" + private const val KEY_LAST_PORT_DEFAULT_MIGRATED = "last_proxy_port_default_1443_migrated" + private const val KEY_LAST_IPS = "last_runtime_ips" + private const val KEY_LAST_POOL = "last_proxy_pool" + } +} From f500f7cdb74a78052f899cb7f434729c3dcbb06d Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Wed, 7 Oct 2026 00:43:34 -0700 Subject: [PATCH 05/14] feat(android): start proxy after boot when enabled --- .../tgwsproxy/BootCompletedReceiver.kt | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 app/src/main/java/com/amurcanov/tgwsproxy/BootCompletedReceiver.kt diff --git a/app/src/main/java/com/amurcanov/tgwsproxy/BootCompletedReceiver.kt b/app/src/main/java/com/amurcanov/tgwsproxy/BootCompletedReceiver.kt new file mode 100644 index 000000000..e729fc83b --- /dev/null +++ b/app/src/main/java/com/amurcanov/tgwsproxy/BootCompletedReceiver.kt @@ -0,0 +1,104 @@ +package com.amurcanov.tgwsproxy + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import androidx.core.content.ContextCompat + +internal enum class BootAutostartDecision { + DISABLED, + ALREADY_RUNNING, + INVALID_SAVED_CONFIG, + START, +} + +internal object BootAutostartPolicy { + fun evaluate( + enabled: Boolean, + savedConfig: ProxySavedConfig, + alreadyRunning: Boolean, + ): BootAutostartDecision { + if (!enabled) { + return BootAutostartDecision.DISABLED + } + if (alreadyRunning) { + return BootAutostartDecision.ALREADY_RUNNING + } + if (!savedConfig.isSufficientForStart()) { + return BootAutostartDecision.INVALID_SAVED_CONFIG + } + return BootAutostartDecision.START + } +} + +class BootCompletedReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (intent.action != Intent.ACTION_BOOT_COMPLETED) { + return + } + + val appContext = context.applicationContext + AppLogger.i( + context = appContext, + category = AppLogCategory.APP, + message = "BOOT_COMPLETED_RECEIVED", + ) + + val prefs = appContext.getSharedPreferences(PROXY_PREFS_NAME, Context.MODE_PRIVATE) + val savedConfig = ProxySavedConfigStore(prefs).load() + when ( + BootAutostartPolicy.evaluate( + enabled = AutostartPreferences(prefs).isEnabled(), + savedConfig = savedConfig, + alreadyRunning = ProxyService.isRunning.value, + ) + ) { + BootAutostartDecision.DISABLED -> { + AppLogger.i( + context = appContext, + category = AppLogCategory.APP, + message = "BOOT_AUTOSTART_DISABLED", + ) + } + + BootAutostartDecision.ALREADY_RUNNING -> { + AppLogger.i( + context = appContext, + category = AppLogCategory.APP, + message = "BOOT_AUTOSTART_ALREADY_RUNNING", + ) + } + + BootAutostartDecision.INVALID_SAVED_CONFIG -> { + AppLogger.w( + context = appContext, + category = AppLogCategory.APP, + message = "BOOT_AUTOSTART_SKIPPED_INVALID_STATE", + details = mapOf("frontend" to savedConfig.frontendType.name), + ) + } + + BootAutostartDecision.START -> { + val serviceIntent = Intent(appContext, ProxyService::class.java).apply { + action = ProxyService.ACTION_START + } + try { + ContextCompat.startForegroundService(appContext, serviceIntent) + AppLogger.i( + context = appContext, + category = AppLogCategory.APP, + message = "BOOT_AUTOSTART_START_REQUESTED", + details = mapOf("frontend" to savedConfig.frontendType.name), + ) + } catch (error: RuntimeException) { + AppLogger.e( + context = appContext, + category = AppLogCategory.APP, + message = "BOOT_AUTOSTART_START_FAILED", + details = mapOf("error" to error.javaClass.simpleName), + ) + } + } + } + } +} From 8d080fbcd53da8b103bbc653c022af80e02fa69f Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Wed, 7 Oct 2026 00:43:46 -0700 Subject: [PATCH 06/14] refactor(proxy): reuse saved config store for service startup --- .../com/amurcanov/tgwsproxy/ProxyService.kt | 55 +++++++------------ 1 file changed, 20 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/com/amurcanov/tgwsproxy/ProxyService.kt b/app/src/main/java/com/amurcanov/tgwsproxy/ProxyService.kt index aab439113..dff2165ff 100644 --- a/app/src/main/java/com/amurcanov/tgwsproxy/ProxyService.kt +++ b/app/src/main/java/com/amurcanov/tgwsproxy/ProxyService.kt @@ -51,12 +51,8 @@ class ProxyService : Service() { ) private val LEGACY_NOTIFICATION_IDS = listOf(1, 2) - private const val PREFS = "ProxyPrefs" + private const val PREFS = PROXY_PREFS_NAME private const val KEY_NOTIFICATION_CHANNELS_MIGRATED = "notification_channels_v3_migrated" - private const val KEY_LAST_PORT = "last_proxy_port" - private const val KEY_LAST_PORT_DEFAULT_MIGRATED = "last_proxy_port_default_1443_migrated" - private const val KEY_LAST_IPS = "last_runtime_ips" - private const val KEY_LAST_POOL = "last_proxy_pool" private val _isRunning = MutableStateFlow(false) val isRunning: StateFlow = _isRunning @@ -118,39 +114,28 @@ class ProxyService : Service() { poolSize: Int, frontendType: LocalProxyFrontendType, ) { - lastFrontendType = frontendType - val prefs = getSharedPreferences(PREFS, Context.MODE_PRIVATE) - val editor = prefs.edit() - lastPort = port - lastIps = ips - lastPoolSize = poolSize - editor - .putInt(KEY_LAST_PORT, port) - .putBoolean(KEY_LAST_PORT_DEFAULT_MIGRATED, true) - .putString(KEY_LAST_IPS, ips) - .putInt(KEY_LAST_POOL, poolSize) - editor.apply() - LocalProxyFrontendRepository(prefs).save(frontendType) + val config = ProxySavedConfig( + port = port, + runtimeConfig = ips, + poolSize = poolSize, + frontendType = frontendType, + ) + ProxySavedConfigStore(getSharedPreferences(PREFS, Context.MODE_PRIVATE)).save(config) + applyLastConfig(config) } private fun loadLastConfig() { - val prefs = getSharedPreferences(PREFS, Context.MODE_PRIVATE) - val savedPort = prefs.getInt(KEY_LAST_PORT, DEFAULT_LOCAL_PROXY_PORT) - val portMigrationDone = prefs.getBoolean(KEY_LAST_PORT_DEFAULT_MIGRATED, false) - lastPort = if (!portMigrationDone && savedPort == LEGACY_DEFAULT_LOCAL_PROXY_PORT) { - DEFAULT_LOCAL_PROXY_PORT - } else { - savedPort - } - lastIps = prefs.getString(KEY_LAST_IPS, "").orEmpty() - lastPoolSize = prefs.getInt(KEY_LAST_POOL, 4) - lastFrontendType = LocalProxyFrontendRepository(prefs).load() - if (savedPort != lastPort || !portMigrationDone) { - prefs.edit() - .putInt(KEY_LAST_PORT, lastPort) - .putBoolean(KEY_LAST_PORT_DEFAULT_MIGRATED, true) - .apply() - } + val config = ProxySavedConfigStore( + getSharedPreferences(PREFS, Context.MODE_PRIVATE), + ).load() + applyLastConfig(config) + } + + private fun applyLastConfig(config: ProxySavedConfig) { + lastPort = config.port + lastIps = config.runtimeConfig + lastPoolSize = config.poolSize + lastFrontendType = config.frontendType } private fun startProxy( From 08a73fd1fb5830937638ee53a58a615fbd69f7fd Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Wed, 7 Oct 2026 00:43:57 -0700 Subject: [PATCH 07/14] feat(ui): expose boot autostart in app settings --- .../java/com/amurcanov/tgwsproxy/MainActivity.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/src/main/java/com/amurcanov/tgwsproxy/MainActivity.kt b/app/src/main/java/com/amurcanov/tgwsproxy/MainActivity.kt index 9dea59854..c39584347 100644 --- a/app/src/main/java/com/amurcanov/tgwsproxy/MainActivity.kt +++ b/app/src/main/java/com/amurcanov/tgwsproxy/MainActivity.kt @@ -335,6 +335,10 @@ private fun ProxyScreen( var notificationPrefs by remember { mutableStateOf(NotificationPreferences.load(context)) } + val autostartPreferences = remember { AutostartPreferences(prefs) } + var autostartOnBoot by remember { + mutableStateOf(autostartPreferences.isEnabled()) + } var showOnboarding by remember { mutableStateOf(!prefs.getBoolean("onboarding_completed", false)) } @@ -2876,6 +2880,16 @@ private fun ProxyScreen( titleRes = R.string.settings_section_app_title, subtitleRes = R.string.settings_section_app_subtitle, ) + SettingsSectionCard(titleRes = R.string.section_service) { + AutostartSettingsSection( + enabled = autostartOnBoot, + onEnabledChange = { enabled -> + autostartOnBoot = enabled + autostartPreferences.setEnabled(enabled) + }, + ) + } + SettingsSectionCard( titleRes = R.string.section_notifications, subtitle = stringResource(R.string.section_notifications_hint), From 0ecbc4b51e37b04cd9dbb1f77691115f5281e34e Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Wed, 7 Oct 2026 00:44:09 -0700 Subject: [PATCH 08/14] feat(ui): add boot autostart strings --- app/src/main/res/values/strings.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2368b5bc6..d1ff2a1f1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -361,6 +361,9 @@ Latency: %1$s Connections: %1$d Last error: %1$s + Service + Start automatically when device turns on + The proxy will start automatically after Android finishes booting using the last saved configuration. Notifications Android requires a visible notification for foreground services. TgWsProxy can make it compact or minimal, but cannot fully remove it while the proxy is running. Service notification From 85da3ef79a59b7883212cebc77b2a7e8fd73df3b Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Wed, 7 Oct 2026 00:44:13 -0700 Subject: [PATCH 09/14] feat(ui): add Russian boot autostart strings --- app/src/main/res/values-ru/strings.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index c94f8aec1..347d3027b 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -361,6 +361,9 @@ Задержка: %1$s Соединения: %1$d Последняя ошибка: %1$s + Служба + Автозапуск при включении устройства + Прокси будет автоматически запускаться после загрузки Android с последней сохранённой конфигурацией. Уведомления Android требует видимое уведомление для foreground service. TgWsProxy может сделать его компактным или минимальным, но не может полностью убрать его во время работы прокси. Уведомление сервиса From e653b5ed3dae17250bf161dbd6f9daf04e04b300 Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Wed, 7 Oct 2026 00:44:16 -0700 Subject: [PATCH 10/14] docs(testing): add boot autostart smoke checklist --- docs/testing/README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/testing/README.md b/docs/testing/README.md index de1cdc0c5..f2c039153 100644 --- a/docs/testing/README.md +++ b/docs/testing/README.md @@ -101,6 +101,18 @@ Automation contract: [../development/github-automation.md](../development/github - [ ] Stop/start the proxy after a failed route attempt - [ ] Verify the local listener is usable after reconnect/restart +## Boot autostart + +- [ ] Leave **Autostart on boot** disabled, reboot the device, and verify that the proxy service does not start. +- [ ] Start the proxy once with a valid configuration, enable **Autostart on boot**, then reboot without manually opening the app. +- [ ] After `BOOT_COMPLETED`, verify that the existing foreground notification appears and the saved local proxy port is reachable. +- [ ] Open the app and verify that the UI reports the already-running `ProxyService`. +- [ ] Stop the proxy manually, verify that the autostart setting remains enabled, reboot again, and verify that the proxy starts. +- [ ] For SOCKS5, remove/clear the saved runtime configuration in a test setup and verify that boot start is skipped without a crash or restart loop. +- [ ] Verify that repeated boot delivery does not create a second proxy runtime. +- [ ] Disable autostart, reboot again, and verify that no proxy service is started. +- [ ] Review `TgWsProxy` logcat for privacy-safe `BOOT_*` events only; no domains, keys, tokens, or private runtime configuration should be logged. + ## Release smoke (before tagging) Release process: [../releases/release.md](../releases/release.md). From a79b5738240b3e9de625cc34cea337be1a39e8df Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Wed, 7 Oct 2026 00:44:29 -0700 Subject: [PATCH 11/14] test(android): cover boot autostart preference --- .../tgwsproxy/AutostartPreferencesTest.kt | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 app/src/test/java/com/amurcanov/tgwsproxy/AutostartPreferencesTest.kt diff --git a/app/src/test/java/com/amurcanov/tgwsproxy/AutostartPreferencesTest.kt b/app/src/test/java/com/amurcanov/tgwsproxy/AutostartPreferencesTest.kt new file mode 100644 index 000000000..d22681645 --- /dev/null +++ b/app/src/test/java/com/amurcanov/tgwsproxy/AutostartPreferencesTest.kt @@ -0,0 +1,26 @@ +package com.amurcanov.tgwsproxy + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class AutostartPreferencesTest { + @Test + fun default_isDisabled() { + val store = AutostartPreferences(InMemorySharedPreferences()) + + assertFalse(store.isEnabled()) + } + + @Test + fun setEnabled_persistsValue() { + val prefs = InMemorySharedPreferences() + val store = AutostartPreferences(prefs) + + store.setEnabled(true) + assertTrue(store.isEnabled()) + + store.setEnabled(false) + assertFalse(store.isEnabled()) + } +} From f059f2a3b3ddaa854c101ba6cb786eda1e7dc966 Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Wed, 7 Oct 2026 00:44:31 -0700 Subject: [PATCH 12/14] test(android): cover boot autostart decisions --- .../tgwsproxy/BootAutostartPolicyTest.kt | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 app/src/test/java/com/amurcanov/tgwsproxy/BootAutostartPolicyTest.kt diff --git a/app/src/test/java/com/amurcanov/tgwsproxy/BootAutostartPolicyTest.kt b/app/src/test/java/com/amurcanov/tgwsproxy/BootAutostartPolicyTest.kt new file mode 100644 index 000000000..98d9fb803 --- /dev/null +++ b/app/src/test/java/com/amurcanov/tgwsproxy/BootAutostartPolicyTest.kt @@ -0,0 +1,80 @@ +package com.amurcanov.tgwsproxy + +import org.junit.Assert.assertEquals +import org.junit.Test + +class BootAutostartPolicyTest { + @Test + fun disabled_skipsStart() { + assertEquals( + BootAutostartDecision.DISABLED, + BootAutostartPolicy.evaluate( + enabled = false, + savedConfig = validSocksConfig(), + alreadyRunning = false, + ), + ) + } + + @Test + fun enabledAndValid_starts() { + assertEquals( + BootAutostartDecision.START, + BootAutostartPolicy.evaluate( + enabled = true, + savedConfig = validSocksConfig(), + alreadyRunning = false, + ), + ) + } + + @Test + fun enabledAndInvalidSocksConfig_skipsStart() { + assertEquals( + BootAutostartDecision.INVALID_SAVED_CONFIG, + BootAutostartPolicy.evaluate( + enabled = true, + savedConfig = validSocksConfig().copy(runtimeConfig = ""), + alreadyRunning = false, + ), + ) + } + + @Test + fun mtProto_doesNotRequireRuntimeIps() { + assertEquals( + BootAutostartDecision.START, + BootAutostartPolicy.evaluate( + enabled = true, + savedConfig = ProxySavedConfig( + port = DEFAULT_LOCAL_PROXY_PORT, + runtimeConfig = "", + poolSize = 4, + frontendType = LocalProxyFrontendType.MTPROTO_EXPERIMENTAL, + ), + alreadyRunning = false, + ), + ) + } + + @Test + fun repeatedDelivery_whenServiceAlreadyRunning_skipsSecondStart() { + assertEquals( + BootAutostartDecision.ALREADY_RUNNING, + BootAutostartPolicy.evaluate( + enabled = true, + savedConfig = validSocksConfig(), + alreadyRunning = true, + ), + ) + } + + private fun validSocksConfig(): ProxySavedConfig { + return ProxySavedConfig( + port = DEFAULT_LOCAL_PROXY_PORT, + runtimeConfig = "runtime-config", + poolSize = 4, + frontendType = LocalProxyFrontendType.SOCKS5, + ) + } +} From cffb87150950781245fc33dbc60d1d65c9d3739b Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Wed, 7 Oct 2026 00:44:34 -0700 Subject: [PATCH 13/14] test(proxy): cover shared saved config store --- .../tgwsproxy/ProxySavedConfigStoreTest.kt | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 app/src/test/java/com/amurcanov/tgwsproxy/ProxySavedConfigStoreTest.kt diff --git a/app/src/test/java/com/amurcanov/tgwsproxy/ProxySavedConfigStoreTest.kt b/app/src/test/java/com/amurcanov/tgwsproxy/ProxySavedConfigStoreTest.kt new file mode 100644 index 000000000..91ddf597e --- /dev/null +++ b/app/src/test/java/com/amurcanov/tgwsproxy/ProxySavedConfigStoreTest.kt @@ -0,0 +1,38 @@ +package com.amurcanov.tgwsproxy + +import org.junit.Assert.assertEquals +import org.junit.Test + +class ProxySavedConfigStoreTest { + @Test + fun saveAndLoad_roundTripsServiceConfig() { + val prefs = InMemorySharedPreferences() + val store = ProxySavedConfigStore(prefs) + val expected = ProxySavedConfig( + port = 2443, + runtimeConfig = "saved-runtime-config", + poolSize = 6, + frontendType = LocalProxyFrontendType.SOCKS5, + ) + + store.save(expected) + + assertEquals(expected, store.load()) + } + + @Test + fun load_migratesLegacyDefaultPort() { + val prefs = InMemorySharedPreferences() + prefs.edit() + .putInt("last_proxy_port", LEGACY_DEFAULT_LOCAL_PROXY_PORT) + .putString( + LocalProxyFrontendRepository.KEY_FRONTEND_TYPE, + LocalProxyFrontendType.MTPROTO_EXPERIMENTAL.prefValue, + ) + .apply() + + val loaded = ProxySavedConfigStore(prefs).load() + + assertEquals(DEFAULT_LOCAL_PROXY_PORT, loaded.port) + } +} From fcde4f5db3b812909f6a922cdf9463503b1ea8a3 Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Wed, 7 Oct 2026 00:46:03 -0700 Subject: [PATCH 14/14] fix(android): short-circuit disabled boot autostart --- .../tgwsproxy/BootCompletedReceiver.kt | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/amurcanov/tgwsproxy/BootCompletedReceiver.kt b/app/src/main/java/com/amurcanov/tgwsproxy/BootCompletedReceiver.kt index e729fc83b..89b5f9281 100644 --- a/app/src/main/java/com/amurcanov/tgwsproxy/BootCompletedReceiver.kt +++ b/app/src/main/java/com/amurcanov/tgwsproxy/BootCompletedReceiver.kt @@ -45,21 +45,25 @@ class BootCompletedReceiver : BroadcastReceiver() { ) val prefs = appContext.getSharedPreferences(PROXY_PREFS_NAME, Context.MODE_PRIVATE) + val autostartEnabled = AutostartPreferences(prefs).isEnabled() + if (!autostartEnabled) { + AppLogger.i( + context = appContext, + category = AppLogCategory.APP, + message = "BOOT_AUTOSTART_DISABLED", + ) + return + } + val savedConfig = ProxySavedConfigStore(prefs).load() when ( BootAutostartPolicy.evaluate( - enabled = AutostartPreferences(prefs).isEnabled(), + enabled = true, savedConfig = savedConfig, alreadyRunning = ProxyService.isRunning.value, ) ) { - BootAutostartDecision.DISABLED -> { - AppLogger.i( - context = appContext, - category = AppLogCategory.APP, - message = "BOOT_AUTOSTART_DISABLED", - ) - } + BootAutostartDecision.DISABLED -> Unit BootAutostartDecision.ALREADY_RUNNING -> { AppLogger.i(