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
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <ssid> wpa2 <pass> [-h] [-b <bssid>]` |
| Connect to a network | `cmd wifi connect-network <ssid> wpa2 <pass> [-h] [-b <bssid>] [-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` |
Expand Down Expand Up @@ -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`,
Expand Down
13 changes: 12 additions & 1 deletion README.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
| Действие | Команда |
|---|---|
| Включить/выключить Wi-Fi | `cmd wifi set-wifi-enabled enabled\|disabled` |
| Подключиться к сети | `cmd wifi connect-network <ssid> wpa2 <pass> [-h] [-b <bssid>]` |
| Подключиться к сети | `cmd wifi connect-network <ssid> wpa2 <pass> [-h] [-b <bssid>] [-d]` |
| Только включить радио | `cmd wifi set-wifi-enabled enabled` — сеть выбирает автоподключение |
| Авиарежим | `cmd connectivity airplane-mode enable\|disable` → `settings put global airplane_mode_on` + broadcast |
| Мобильные данные | `svc data enable\|disable` |
Expand Down Expand Up @@ -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`,
Expand Down
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
19 changes: 13 additions & 6 deletions app/src/main/java/icu/nd4y/netswitcher/data/Model.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -178,21 +184,22 @@ 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),
Profile(id = "eth_sw", name = "Ethernet", kind = ProfileKind.ETHERNET_TOGGLE),
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
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/icu/nd4y/netswitcher/data/YamlConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 -> {
Expand Down Expand Up @@ -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("В файле нет ни одного профиля")
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/java/icu/nd4y/netswitcher/engine/SwitchEngine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -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())
Expand Down
11 changes: 11 additions & 0 deletions app/src/main/java/icu/nd4y/netswitcher/ui/EditProfileDialog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
}
}

Expand Down
29 changes: 29 additions & 0 deletions app/src/test/java/icu/nd4y/netswitcher/data/YamlConfigTest.kt
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class EditProfileDialogTest {
}
compose.onNodeWithText("SSID").assertExists()
compose.onNodeWithText("Пароль").assertExists()
compose.onNodeWithText("Автоподключение к сети").assertExists()
compose.onNodeWithText("Сохранить").assertExists()
}

Expand Down
8 changes: 8 additions & 0 deletions examples/example-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -42,6 +46,7 @@ profiles:
security: WPA2
password: ""
tapAgainDisconnects: true
autoJoin: true
mobileData: KEEP
- id: "home5"
name: "Home 5G"
Expand All @@ -50,6 +55,7 @@ profiles:
security: WPA2
password: ""
tapAgainDisconnects: true
autoJoin: true
mobileData: KEEP
- id: "guest"
name: "Guest"
Expand All @@ -58,6 +64,7 @@ profiles:
security: WPA2
password: ""
tapAgainDisconnects: true
autoJoin: false # не возвращаться в сеть самостоятельно
mobileData: KEEP
- id: "iot"
name: "IoT"
Expand All @@ -66,6 +73,7 @@ profiles:
security: WPA2
password: ""
tapAgainDisconnects: true
autoJoin: false # не возвращаться в сеть самостоятельно
mobileData: KEEP
- id: "lte"
name: "LTE only"
Expand Down
Loading