diff --git a/README.md b/README.md index c06b7ce..7f683dd 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ privileges, and NetSwitcher runs exactly the commands you'd type over `adb shell | Action | Command | |---|---| | Turn Wi-Fi on/off | `cmd wifi set-wifi-enabled enabled\|disabled` | -| Connect to a network | `cmd wifi connect-network wpa2 [-h] [-b ]` | +| Connect to a network | `cmd wifi connect-network wpa2 [-h] [-b ] [-d]` | | Turn the radio on only | `cmd wifi set-wifi-enabled enabled` — the network is picked by auto-connect | | Airplane mode | `cmd connectivity airplane-mode enable\|disable` → `settings put global airplane_mode_on` + broadcast | | Mobile data | `svc data enable\|disable` | @@ -221,6 +221,16 @@ already connected to disconnects from it. There's no dedicated "disconnect" shel command, so this is done by turning the radio off — this behavior can be turned off with the "Tapping again disconnects" flag on the profile. +Every Wi-Fi network also has an "Auto-connect" flag (`autoJoin` in YAML). Turn it off +and connecting through NetSwitcher saves the network in Android with auto-join disabled +(`connect-network … -d`): the phone no longer hops back onto it by itself when it comes +into range or when the radio turns on — only when you press the button. That's for +guest and IoT networks you visit on purpose rather than live on; the stock `Guest` and +`IoT` profiles ship with it off. The flag is written into the system's saved network at +the moment the app connects, so a network already saved in Android with auto-join on +gets it turned off after the first connection via the button. The reverse holds too: +connecting through a profile with auto-connect on turns it back on in the system entry. + ## Profiles out of the box Toggles for `Wi-Fi` / `LTE` / `Ethernet` / `Airplane mode`, networks `Home`, `Home 5G`, diff --git a/README.ru.md b/README.ru.md index 0cf30d1..ebaeb2b 100644 --- a/README.ru.md +++ b/README.ru.md @@ -47,7 +47,7 @@ | Действие | Команда | |---|---| | Включить/выключить Wi-Fi | `cmd wifi set-wifi-enabled enabled\|disabled` | -| Подключиться к сети | `cmd wifi connect-network wpa2 [-h] [-b ]` | +| Подключиться к сети | `cmd wifi connect-network wpa2 [-h] [-b ] [-d]` | | Только включить радио | `cmd wifi set-wifi-enabled enabled` — сеть выбирает автоподключение | | Авиарежим | `cmd connectivity airplane-mode enable\|disable` → `settings put global airplane_mode_on` + broadcast | | Мобильные данные | `svc data enable\|disable` | @@ -212,6 +212,17 @@ Wi-Fi-профили конкретных сетей тоже работают существует, поэтому это делается выключением радио — поведение отключается флажком «Повторное нажатие отключает от сети» в профиле. +У каждой Wi-Fi-сети есть и флажок «Автоподключение к сети» (в YAML — `autoJoin`). Если +его выключить, при подключении через NetSwitcher сеть сохраняется в Android с +отключённым автоподключением (`connect-network … -d`): телефон больше не будет сам +возвращаться в неё при появлении в зоне действия или после включения радио — только по +кнопке. Это для гостевых и IoT-сетей, в которых бывают по делу, а не живут; у профилей +`Guest` и `IoT` из коробки он выключен. Флаг записывается в системную запись сети в +момент подключения через приложение, поэтому у сети, которая уже сохранена в Android с +автоподключением, оно выключится после первого подключения кнопкой. Обратное тоже +верно: подключение через профиль с включённым автоподключением включает его в системной +записи заново. + ## Профили из коробки Переключатели `Wi-Fi` / `LTE` / `Ethernet` / `Авиарежим`, сети `Home`, `Home 5G`, diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c990000..aff08ea 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -14,8 +14,8 @@ android { applicationId = "icu.nd4y.netswitcher" minSdk = 31 targetSdk = 36 - versionCode = 19 - versionName = "1.18" + versionCode = 20 + versionName = "1.19" } // Release signing comes from the environment so the key never lives in the repo. diff --git a/app/src/main/java/icu/nd4y/netswitcher/data/Model.kt b/app/src/main/java/icu/nd4y/netswitcher/data/Model.kt index a369df5..c8f558b 100644 --- a/app/src/main/java/icu/nd4y/netswitcher/data/Model.kt +++ b/app/src/main/java/icu/nd4y/netswitcher/data/Model.kt @@ -80,6 +80,12 @@ data class Profile( val password: String = "", val hiddenSsid: Boolean = false, val bssid: String = "", + /** + * WIFI: whether Android may rejoin this network on its own. Off means the saved + * network is written with auto-join disabled (`connect-network -d`), so the phone + * only lands on it when the button is pressed — the guest / IoT case. + */ + val autoJoin: Boolean = true, // --- Cellular --- /** Subscription id of the SIM to make the default data SIM; -1 = leave as is. */ val subscriptionId: Int = -1, @@ -178,11 +184,12 @@ data class Config( const val TILE_COUNT = 8 fun default(): Config { - val wifi = { id: String, name: String, ssid: String -> - Profile(id = id, name = name, kind = ProfileKind.WIFI, ssid = ssid) - } + fun wifi(id: String, name: String, ssid: String, autoJoin: Boolean = true) = + Profile(id = id, name = name, kind = ProfileKind.WIFI, ssid = ssid, autoJoin = autoJoin) // Placeholder SSIDs — the point is to show the shape of a profile, the // user replaces them (or imports a YAML config) with their own networks. + // Guest and IoT ship with auto-join off: those are networks you visit on + // purpose, not ones the phone should drift onto by itself. val profiles = listOf( Profile(id = "wifi_sw", name = "Wi-Fi", kind = ProfileKind.WIFI_TOGGLE), Profile(id = "lte_sw", name = "LTE", kind = ProfileKind.CELLULAR_TOGGLE), @@ -190,9 +197,9 @@ data class Config( Profile(id = "air_sw", name = "Авиарежим", kind = ProfileKind.AIRPLANE_TOGGLE), wifi("home", "Home", "Home"), wifi("home5", "Home 5G", "Home-5G"), - wifi("guest", "Guest", "Guest"), - wifi("guest5", "Guest 5G", "Guest-5G"), - wifi("iot", "IoT", "IoT"), + wifi("guest", "Guest", "Guest", autoJoin = false), + wifi("guest5", "Guest 5G", "Guest-5G", autoJoin = false), + wifi("iot", "IoT", "IoT", autoJoin = false), Profile( id = "lte", // "only"-suffixed so the one-shot is distinguishable from the LTE diff --git a/app/src/main/java/icu/nd4y/netswitcher/data/YamlConfig.kt b/app/src/main/java/icu/nd4y/netswitcher/data/YamlConfig.kt index 8c30564..399ad9d 100644 --- a/app/src/main/java/icu/nd4y/netswitcher/data/YamlConfig.kt +++ b/app/src/main/java/icu/nd4y/netswitcher/data/YamlConfig.kt @@ -37,6 +37,7 @@ object YamlConfig { appendLine(" bssid: ${quote(profile.bssid)}") } appendLine(" tapAgainDisconnects: ${profile.tapAgainDisconnects}") + appendLine(" autoJoin: ${profile.autoJoin} # false = только по кнопке") } ProfileKind.CELLULAR, ProfileKind.CELLULAR_TOGGLE -> { @@ -98,6 +99,7 @@ object YamlConfig { mobileData = map.enum("mobileData", MobileDataAction.entries, MobileDataAction.KEEP), disableWifi = map.bool("disableWifi") ?: true, tapAgainDisconnects = map.bool("tapAgainDisconnects") ?: true, + autoJoin = map.bool("autoJoin") ?: true, ) } if (profiles.isEmpty()) error("В файле нет ни одного профиля") diff --git a/app/src/main/java/icu/nd4y/netswitcher/engine/SwitchEngine.kt b/app/src/main/java/icu/nd4y/netswitcher/engine/SwitchEngine.kt index e852e95..75e69b4 100644 --- a/app/src/main/java/icu/nd4y/netswitcher/engine/SwitchEngine.kt +++ b/app/src/main/java/icu/nd4y/netswitcher/engine/SwitchEngine.kt @@ -384,6 +384,12 @@ class SwitchEngine(context: Context) { } if (profile.hiddenSsid) append(" -h") if (profile.bssid.isNotBlank()) append(" -b ${profile.bssid}") + // `-d` lands in the saved network as allowAutojoin=false, and the framework + // copies that flag over on every re-add — so the button both connects now + // and keeps the phone from rejoining this SSID on its own later. Without + // `-d` a fresh WifiConfiguration carries allowAutojoin=true, which is how + // flipping the switch back on re-enables auto-join in the system entry. + if (!profile.autoJoin) append(" -d") } /** [logAs] is what lands in the on-screen log — pass a redacted copy for secrets. */ @@ -421,6 +427,7 @@ class SwitchEngine(context: Context) { } } if (profile.hiddenSsid) builder.setIsHiddenSsid(true) + if (!profile.autoJoin) builder.setIsInitialAutojoinEnabled(false) builder.setIsAppInteractionRequired(false) val manager = appContext.getSystemService(WifiManager::class.java) manager?.removeNetworkSuggestions(emptyList()) diff --git a/app/src/main/java/icu/nd4y/netswitcher/ui/EditProfileDialog.kt b/app/src/main/java/icu/nd4y/netswitcher/ui/EditProfileDialog.kt index e8c0c2d..52a221e 100644 --- a/app/src/main/java/icu/nd4y/netswitcher/ui/EditProfileDialog.kt +++ b/app/src/main/java/icu/nd4y/netswitcher/ui/EditProfileDialog.kt @@ -246,6 +246,17 @@ private fun WifiFields(draft: Profile, onChange: (Profile) -> Unit) { checked = draft.hiddenSsid, onChange = { onChange(draft.copy(hiddenSsid = it)) }, ) + ToggleRow( + label = "Автоподключение к сети", + checked = draft.autoJoin, + onChange = { onChange(draft.copy(autoJoin = it)) }, + ) + Text( + text = "Выключите для гостевых и IoT-сетей: телефон не будет сам возвращаться " + + "в эту сеть, подключение — только кнопкой. Флаг попадает в системную " + + "запись сети при подключении через NetSwitcher.", + style = MaterialTheme.typography.bodySmall, + ) } } diff --git a/app/src/test/java/icu/nd4y/netswitcher/data/YamlConfigTest.kt b/app/src/test/java/icu/nd4y/netswitcher/data/YamlConfigTest.kt index 8e21940..1b4e730 100644 --- a/app/src/test/java/icu/nd4y/netswitcher/data/YamlConfigTest.kt +++ b/app/src/test/java/icu/nd4y/netswitcher/data/YamlConfigTest.kt @@ -1,6 +1,7 @@ package icu.nd4y.netswitcher.data import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test @@ -213,6 +214,34 @@ class YamlConfigTest { assertEquals(listOf("w"), seeded.homeIds) } + /** + * `autoJoin` is what keeps the phone off guest / IoT networks. A file written + * before the key existed must keep auto-join on, and an explicit `false` has to + * survive export → import — otherwise a round trip would silently re-enable it. + */ + @Test + fun `autoJoin defaults to on and an explicit off survives a round trip`() { + val yaml = """ + profiles: + - id: "home" + name: "Home" + kind: WIFI + ssid: "Home" + - id: "guest" + name: "Guest" + kind: WIFI + ssid: "Guest" + autoJoin: false + """.trimIndent() + + val config = YamlConfig.decode(yaml).getOrThrow() + assertTrue(config.profile("home")!!.autoJoin) + assertFalse(config.profile("guest")!!.autoJoin) + + val again = YamlConfig.decode(YamlConfig.encode(config)).getOrThrow() + assertEquals(config.profiles, again.profiles) + } + @Test fun `garbage input fails instead of wiping the configuration`() { assertTrue(YamlConfig.decode("не yaml вовсе: [[[").isFailure) diff --git a/app/src/test/java/icu/nd4y/netswitcher/ui/EditProfileDialogTest.kt b/app/src/test/java/icu/nd4y/netswitcher/ui/EditProfileDialogTest.kt index 172eac5..c9b90d2 100644 --- a/app/src/test/java/icu/nd4y/netswitcher/ui/EditProfileDialogTest.kt +++ b/app/src/test/java/icu/nd4y/netswitcher/ui/EditProfileDialogTest.kt @@ -36,6 +36,7 @@ class EditProfileDialogTest { } compose.onNodeWithText("SSID").assertExists() compose.onNodeWithText("Пароль").assertExists() + compose.onNodeWithText("Автоподключение к сети").assertExists() compose.onNodeWithText("Сохранить").assertExists() } diff --git a/examples/example-config.yaml b/examples/example-config.yaml index 79ca974..c9b6aa1 100644 --- a/examples/example-config.yaml +++ b/examples/example-config.yaml @@ -14,6 +14,10 @@ # CELLULAR_TOGGLE — переключатель мобильных данных # ETHERNET_TOGGLE — переключатель проводного интерфейса # AIRPLANE_TOGGLE — переключатель авиарежима +# +# autoJoin: false у Wi-Fi-профиля — при подключении через NetSwitcher сеть сохраняется +# в Android без автоподключения: телефон не вернётся в неё сам, только по кнопке. +# Для гостевых и IoT-сетей. Отсутствующий ключ = true. version: 1 backend: AUTO # AUTO | SHIZUKU | ROOT | NONE startNotification: SHADE # SHADE | HEADS_UP @@ -42,6 +46,7 @@ profiles: security: WPA2 password: "" tapAgainDisconnects: true + autoJoin: true mobileData: KEEP - id: "home5" name: "Home 5G" @@ -50,6 +55,7 @@ profiles: security: WPA2 password: "" tapAgainDisconnects: true + autoJoin: true mobileData: KEEP - id: "guest" name: "Guest" @@ -58,6 +64,7 @@ profiles: security: WPA2 password: "" tapAgainDisconnects: true + autoJoin: false # не возвращаться в сеть самостоятельно mobileData: KEEP - id: "iot" name: "IoT" @@ -66,6 +73,7 @@ profiles: security: WPA2 password: "" tapAgainDisconnects: true + autoJoin: false # не возвращаться в сеть самостоятельно mobileData: KEEP - id: "lte" name: "LTE only"