1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-16 16:15:23 -04:00

feat(network): add saved WiFi state to settings (#2648)

This commit is contained in:
jbwfu
2026-06-16 21:06:29 +08:00
committed by GitHub
parent ddf943846f
commit 85b63219b9
22 changed files with 1436 additions and 207 deletions
+2
View File
@@ -125,6 +125,8 @@ State updates are sent whenever network configuration changes:
- `wifiConnected`: Whether associated with an access point
- `wifiSSID`: Currently connected network name
- `wifiIP`: Assigned IP address (empty until DHCP completes)
- `savedWifiNetworks` (API v26+): Saved WiFi profiles exposed at SSID granularity. If a backend has multiple profiles for the same SSID, DMS merges them into one SSID-level entry. Clients talking to older servers should derive saved visible networks from `wifiNetworks` entries where `saved` is true.
- `savedWifiNetworks[].outOfRange` (API v26+): Whether the saved profile is not currently visible in scan results. Fallback entries derived from `wifiNetworks` should be treated as visible (`outOfRange: false`).
- `lastError`: Error message from last failed connection attempt
### network.credentials Service Events
+1
View File
@@ -67,6 +67,7 @@ type BackendState struct {
WiFiBSSID string
WiFiSignal uint8
WiFiNetworks []WiFiNetwork
SavedWiFiNetworks []WiFiNetwork
WiFiDevices []WiFiDevice
WiredConnections []WiredConnection
VPNProfiles []VPNProfile
@@ -27,6 +27,19 @@ func TestHybridIwdNetworkdBackend_GetCurrentState_MergesState(t *testing.T) {
wifi.state.WiFiBSSID = "00:11:22:33:44:55"
wifi.state.WiFiSignal = 75
wifi.state.WiFiDevice = "wlan0"
wifi.state.SavedWiFiNetworks = []WiFiNetwork{
{
SSID: "TestNetwork",
Saved: true,
Autoconnect: true,
Connected: true,
},
{
SSID: "AwayNetwork",
Saved: true,
OutOfRange: true,
},
}
l3.state.WiFiIP = "192.168.1.100"
l3.state.EthernetConnected = false
@@ -42,6 +55,9 @@ func TestHybridIwdNetworkdBackend_GetCurrentState_MergesState(t *testing.T) {
assert.True(t, state.WiFiConnected)
assert.False(t, state.EthernetConnected)
assert.Equal(t, StatusWiFi, state.NetworkStatus)
assert.Len(t, state.SavedWiFiNetworks, 2)
assert.Equal(t, "TestNetwork", state.SavedWiFiNetworks[0].SSID)
assert.True(t, state.SavedWiFiNetworks[1].OutOfRange)
}
func TestHybridIwdNetworkdBackend_GetCurrentState_EthernetPriority(t *testing.T) {
@@ -80,6 +80,10 @@ func (b *IWDBackend) Initialize() error {
return fmt.Errorf("failed to discover iwd devices: %w", err)
}
if err := b.updateSavedWiFiNetworks(); err != nil {
log.Warnf("Failed to get initial saved WiFi networks: %v", err)
}
if err := b.updateState(); err != nil {
conn.Close()
return fmt.Errorf("failed to get initial state: %w", err)
@@ -145,6 +149,7 @@ func (b *IWDBackend) GetCurrentState() (*BackendState, error) {
state := *b.state
state.WiFiNetworks = append([]WiFiNetwork(nil), b.state.WiFiNetworks...)
state.SavedWiFiNetworks = append([]WiFiNetwork(nil), b.state.SavedWiFiNetworks...)
state.WiredConnections = append([]WiredConnection(nil), b.state.WiredConnections...)
state.WiFiDevices = b.getWiFiDevicesLocked()
@@ -45,12 +45,42 @@ func (b *IWDBackend) StartMonitoring(onStateChange func()) error {
}
}
if err := b.conn.AddMatchSignal(
dbus.WithMatchInterface(dbusPropertiesInterface),
dbus.WithMatchMember("PropertiesChanged"),
dbus.WithMatchArg(0, iwdKnownNetworkInterface),
); err != nil {
return fmt.Errorf("failed to add known network signal match: %w", err)
}
if err := b.conn.AddMatchSignal(
dbus.WithMatchInterface(dbusObjectManager),
dbus.WithMatchMember("InterfacesAdded"),
); err != nil {
return fmt.Errorf("failed to add iwd interfaces-added signal match: %w", err)
}
if err := b.conn.AddMatchSignal(
dbus.WithMatchInterface(dbusObjectManager),
dbus.WithMatchMember("InterfacesRemoved"),
); err != nil {
return fmt.Errorf("failed to add iwd interfaces-removed signal match: %w", err)
}
b.sigWG.Add(1)
go b.signalHandler(sigChan)
return nil
}
func (b *IWDBackend) refreshWiFiNetworkState() bool {
_, err := b.updateWiFiNetworks()
if err == nil {
return true
}
return b.updateSavedWiFiNetworks() == nil
}
func (b *IWDBackend) signalHandler(sigChan chan *dbus.Signal) {
defer b.sigWG.Done()
@@ -66,11 +96,36 @@ func (b *IWDBackend) signalHandler(sigChan chan *dbus.Signal) {
return
}
if sig.Name != dbusPropertiesInterface+".PropertiesChanged" {
if sig.Name == dbusObjectManager+".InterfacesAdded" {
if len(sig.Body) >= 2 {
if interfaces, ok := sig.Body[1].(map[string]map[string]dbus.Variant); ok {
if _, ok := interfaces[iwdKnownNetworkInterface]; ok {
if b.refreshWiFiNetworkState() && b.onStateChange != nil {
b.onStateChange()
}
}
}
}
continue
}
if len(sig.Body) < 2 {
if sig.Name == dbusObjectManager+".InterfacesRemoved" {
if len(sig.Body) >= 2 {
if interfaces, ok := sig.Body[1].([]string); ok {
for _, iface := range interfaces {
if iface == iwdKnownNetworkInterface {
if b.refreshWiFiNetworkState() && b.onStateChange != nil {
b.onStateChange()
}
break
}
}
}
}
continue
}
if sig.Name != dbusPropertiesInterface+".PropertiesChanged" || len(sig.Body) < 2 {
continue
}
@@ -87,6 +142,9 @@ func (b *IWDBackend) signalHandler(sigChan chan *dbus.Signal) {
stateChanged := false
switch iface {
case iwdKnownNetworkInterface:
stateChanged = b.refreshWiFiNetworkState()
case iwdDeviceInterface:
if sig.Path == b.devicePath {
if poweredVar, ok := changed["Powered"]; ok {
@@ -105,13 +163,7 @@ func (b *IWDBackend) signalHandler(sigChan chan *dbus.Signal) {
if sig.Path == b.stationPath {
if scanningVar, ok := changed["Scanning"]; ok {
if scanning, ok := scanningVar.Value().(bool); ok && !scanning {
networks, err := b.updateWiFiNetworks()
if err == nil {
b.stateMutex.Lock()
b.state.WiFiNetworks = networks
b.stateMutex.Unlock()
stateChanged = true
}
stateChanged = b.refreshWiFiNetworkState() || stateChanged
b.stateMutex.RLock()
wifiConnected := b.state.WiFiConnected
@@ -236,6 +288,7 @@ func (b *IWDBackend) signalHandler(sigChan chan *dbus.Signal) {
}
}
b.refreshWiFiNetworkState()
stateChanged = true
if att != nil && isTarget {
@@ -282,6 +335,7 @@ func (b *IWDBackend) signalHandler(sigChan chan *dbus.Signal) {
b.state.NetworkStatus = StatusDisconnected
}
b.stateMutex.Unlock()
b.refreshWiFiNetworkState()
stateChanged = true
}
}
@@ -342,6 +396,7 @@ func (b *IWDBackend) signalHandler(sigChan chan *dbus.Signal) {
stateChanged = true
}
b.stateMutex.Unlock()
b.refreshWiFiNetworkState()
}
}
}
@@ -4,6 +4,7 @@ import (
"testing"
"time"
"github.com/godbus/dbus/v5"
"github.com/stretchr/testify/assert"
)
@@ -168,6 +169,92 @@ func TestIWDBackend_MapIwdDBusError(t *testing.T) {
}
}
func TestIWDSavedWiFiProfilesFromManagedObjects(t *testing.T) {
objects := map[dbus.ObjectPath]map[string]map[string]dbus.Variant{
"/net/connman/iwd/known_network/1": {
iwdKnownNetworkInterface: {
"Name": dbus.MakeVariant("Home"),
"AutoConnect": dbus.MakeVariant(false),
"Hidden": dbus.MakeVariant(true),
"Type": dbus.MakeVariant("psk"),
},
},
"/net/connman/iwd/known_network/2": {
iwdKnownNetworkInterface: {
"Name": dbus.MakeVariant("Office"),
"Type": dbus.MakeVariant("8021x"),
},
},
"/net/connman/iwd/known_network/3": {
iwdKnownNetworkInterface: {
"Name": dbus.MakeVariant("Cafe"),
"Type": dbus.MakeVariant("open"),
},
},
"/net/connman/iwd/network/1": {
iwdNetworkInterface: {
"Name": dbus.MakeVariant("VisibleOnly"),
},
},
}
profiles := iwdSavedWiFiProfilesFromManagedObjects(objects)
assert.Len(t, profiles, 3)
assert.False(t, profiles["Home"].Autoconnect)
assert.True(t, profiles["Home"].Hidden)
assert.True(t, profiles["Home"].Secured)
assert.False(t, profiles["Home"].Enterprise)
assert.True(t, profiles["Office"].Autoconnect)
assert.True(t, profiles["Office"].Secured)
assert.True(t, profiles["Office"].Enterprise)
assert.True(t, profiles["Cafe"].Autoconnect)
assert.False(t, profiles["Cafe"].Secured)
assert.False(t, profiles["Cafe"].Enterprise)
}
func TestIWDWiFiNetworksFromVisibleIncludesConnectedHiddenFallback(t *testing.T) {
profiles := map[string]savedWiFiProfile{
"Home": {
Autoconnect: true,
Secured: true,
Hidden: true,
Mode: "infrastructure",
},
}
visible := []WiFiNetwork{
{
SSID: "Cafe",
Signal: 42,
Secured: false,
},
}
networks := iwdWiFiNetworksFromVisible(visible, profiles, "Home", true, 68)
savedNetworks := savedWiFiNetworksFromProfiles(profiles, map[string]WiFiNetwork{
networks[0].SSID: networks[0],
networks[1].SSID: networks[1],
}, "Home", true)
assert.Len(t, networks, 2)
assert.Equal(t, "Cafe", networks[0].SSID)
assert.False(t, networks[0].Connected)
assert.Equal(t, "Home", networks[1].SSID)
assert.True(t, networks[1].Connected)
assert.True(t, networks[1].Hidden)
assert.True(t, networks[1].Saved)
assert.True(t, networks[1].Autoconnect)
assert.Equal(t, uint8(68), networks[1].Signal)
assert.Len(t, savedNetworks, 1)
assert.Equal(t, "Home", savedNetworks[0].SSID)
assert.True(t, savedNetworks[0].Connected)
assert.False(t, savedNetworks[0].OutOfRange)
}
func TestConnectAttempt_Finalization(t *testing.T) {
backend, _ := NewIWDBackend()
backend.state = &BackendState{}
+138 -55
View File
@@ -164,22 +164,18 @@ func (b *IWDBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
return nil, fmt.Errorf("failed to get networks: %w", err)
}
knownNetworks, err := b.getKnownNetworks()
savedProfiles, err := b.getIWDSavedWiFiProfiles()
if err != nil {
knownNetworks = make(map[string]bool)
}
autoconnectMap, err := b.getAutoconnectSettings()
if err != nil {
autoconnectMap = make(map[string]bool)
savedProfiles = make(map[string]savedWiFiProfile)
}
b.stateMutex.RLock()
currentSSID := b.state.WiFiSSID
wifiConnected := b.state.WiFiConnected
wifiSignal := b.state.WiFiSignal
b.stateMutex.RUnlock()
networks := make([]WiFiNetwork, 0, len(orderedNetworks))
visibleNetworks := make([]WiFiNetwork, 0, len(orderedNetworks))
for _, netData := range orderedNetworks {
if len(netData) < 2 {
continue
@@ -225,23 +221,26 @@ func (b *IWDBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
secured := netType != "open"
network := WiFiNetwork{
SSID: name,
Signal: signal,
Secured: secured,
Connected: wifiConnected && name == currentSSID,
Saved: knownNetworks[name],
Autoconnect: autoconnectMap[name],
Enterprise: netType == "8021x",
}
networks = append(networks, network)
visibleNetworks = append(visibleNetworks, WiFiNetwork{
SSID: name,
Signal: signal,
Secured: secured,
Enterprise: netType == "8021x",
})
}
networks := iwdWiFiNetworksFromVisible(visibleNetworks, savedProfiles, currentSSID, wifiConnected, wifiSignal)
visibleNetworkMap := make(map[string]WiFiNetwork, len(networks))
for _, network := range networks {
visibleNetworkMap[network.SSID] = network
}
savedNetworks := savedWiFiNetworksFromProfiles(savedProfiles, visibleNetworkMap, currentSSID, wifiConnected)
sortWiFiNetworks(networks)
b.stateMutex.Lock()
b.state.WiFiNetworks = networks
b.state.SavedWiFiNetworks = savedNetworks
b.stateMutex.Unlock()
now := time.Now()
@@ -254,30 +253,129 @@ func (b *IWDBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
return networks, nil
}
func (b *IWDBackend) getKnownNetworks() (map[string]bool, error) {
obj := b.conn.Object(iwdBusName, iwdObjectPath)
var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant
err := obj.Call(dbusObjectManager+".GetManagedObjects", 0).Store(&objects)
func (b *IWDBackend) updateSavedWiFiNetworks() error {
savedProfiles, err := b.getIWDSavedWiFiProfiles()
if err != nil {
return nil, err
return err
}
known := make(map[string]bool)
for _, interfaces := range objects {
if knownProps, ok := interfaces[iwdKnownNetworkInterface]; ok {
if nameVar, ok := knownProps["Name"]; ok {
if name, ok := nameVar.Value().(string); ok {
known[name] = true
}
}
}
}
b.stateMutex.RLock()
currentSSID := b.state.WiFiSSID
wifiConnected := b.state.WiFiConnected
wifiNetworks := append([]WiFiNetwork(nil), b.state.WiFiNetworks...)
b.stateMutex.RUnlock()
return known, nil
wifiNetworks, savedNetworks := refreshSavedWiFiState(wifiNetworks, savedProfiles, currentSSID, wifiConnected)
b.stateMutex.Lock()
b.state.WiFiNetworks = wifiNetworks
b.state.SavedWiFiNetworks = savedNetworks
b.stateMutex.Unlock()
return nil
}
func (b *IWDBackend) getAutoconnectSettings() (map[string]bool, error) {
func iwdWiFiNetworksFromVisible(visibleNetworks []WiFiNetwork, savedProfiles map[string]savedWiFiProfile, currentSSID string, wifiConnected bool, wifiSignal uint8) []WiFiNetwork {
networks := make([]WiFiNetwork, 0, len(visibleNetworks)+1)
seenSSIDs := make(map[string]struct{}, len(visibleNetworks)+1)
for _, network := range visibleNetworks {
profile, saved := savedProfiles[network.SSID]
network.Connected = wifiConnected && network.SSID == currentSSID
network.Saved = saved
network.Autoconnect = profile.Autoconnect
network.Hidden = network.Hidden || profile.Hidden
network.Secured = network.Secured || profile.Secured
network.Enterprise = network.Enterprise || profile.Enterprise
if network.Mode == "" {
network.Mode = profile.Mode
}
networks = append(networks, network)
seenSSIDs[network.SSID] = struct{}{}
}
if wifiConnected && currentSSID != "" {
if _, exists := seenSSIDs[currentSSID]; !exists {
profile, saved := savedProfiles[currentSSID]
secured := profile.Secured
if !saved {
secured = true
}
mode := profile.Mode
if mode == "" {
mode = "infrastructure"
}
networks = append(networks, WiFiNetwork{
SSID: currentSSID,
Signal: wifiSignal,
Secured: secured,
Enterprise: profile.Enterprise,
Connected: true,
Saved: saved,
Autoconnect: profile.Autoconnect,
Hidden: true,
Mode: mode,
})
}
}
return networks
}
func iwdSavedWiFiProfilesFromManagedObjects(objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant) map[string]savedWiFiProfile {
profiles := make(map[string]savedWiFiProfile)
for _, interfaces := range objects {
knownProps, ok := interfaces[iwdKnownNetworkInterface]
if !ok {
continue
}
nameVar, ok := knownProps["Name"]
if !ok {
continue
}
name, ok := nameVar.Value().(string)
if !ok || name == "" {
continue
}
profile := savedWiFiProfile{
Autoconnect: true,
Mode: "infrastructure",
}
if acVar, ok := knownProps["AutoConnect"]; ok {
if autoconnect, ok := acVar.Value().(bool); ok {
profile.Autoconnect = autoconnect
}
}
if hiddenVar, ok := knownProps["Hidden"]; ok {
if hidden, ok := hiddenVar.Value().(bool); ok {
profile.Hidden = hidden
}
}
if typeVar, ok := knownProps["Type"]; ok {
if networkType, ok := typeVar.Value().(string); ok {
profile.Secured = networkType != "" && networkType != "open"
profile.Enterprise = networkType == "8021x"
}
}
if existing, ok := profiles[name]; ok {
profile.Autoconnect = profile.Autoconnect || existing.Autoconnect
profile.Hidden = profile.Hidden || existing.Hidden
profile.Secured = profile.Secured || existing.Secured
profile.Enterprise = profile.Enterprise || existing.Enterprise
}
profiles[name] = profile
}
return profiles
}
func (b *IWDBackend) getIWDSavedWiFiProfiles() (map[string]savedWiFiProfile, error) {
obj := b.conn.Object(iwdBusName, iwdObjectPath)
var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant
@@ -286,24 +384,7 @@ func (b *IWDBackend) getAutoconnectSettings() (map[string]bool, error) {
return nil, err
}
autoconnectMap := make(map[string]bool)
for _, interfaces := range objects {
if knownProps, ok := interfaces[iwdKnownNetworkInterface]; ok {
if nameVar, ok := knownProps["Name"]; ok {
if name, ok := nameVar.Value().(string); ok {
autoconnect := true
if acVar, ok := knownProps["AutoConnect"]; ok {
if ac, ok := acVar.Value().(bool); ok {
autoconnect = ac
}
}
autoconnectMap[name] = autoconnect
}
}
}
}
return autoconnectMap, nil
return iwdSavedWiFiProfilesFromManagedObjects(objects), nil
}
func (b *IWDBackend) GetWiFiNetworkDetails(ssid string) (*NetworkInfoResponse, error) {
@@ -614,6 +695,8 @@ func (b *IWDBackend) ForgetWiFiNetwork(ssid string) error {
b.stateMutex.Unlock()
}
_, _ = b.updateWiFiNetworks()
if b.onStateChange != nil {
b.onStateChange()
}
@@ -222,6 +222,10 @@ func (b *NetworkManagerBackend) Initialize() error {
log.Warnf("Failed to update WiFi state: %v", err)
}
if err := b.updateSavedWiFiNetworks(); err != nil {
log.Warnf("Failed to get initial saved WiFi networks: %v", err)
}
if wifiEnabled {
if _, err := b.updateWiFiNetworks(); err != nil {
log.Warnf("Failed to get initial networks: %v", err)
@@ -261,6 +265,7 @@ func (b *NetworkManagerBackend) GetCurrentState() (*BackendState, error) {
state := *b.state
state.WiFiNetworks = append([]WiFiNetwork(nil), b.state.WiFiNetworks...)
state.SavedWiFiNetworks = append([]WiFiNetwork(nil), b.state.SavedWiFiNetworks...)
state.WiFiDevices = append([]WiFiDevice(nil), b.state.WiFiDevices...)
state.WiredConnections = append([]WiredConnection(nil), b.state.WiredConnections...)
state.EthernetDevices = append([]EthernetDevice(nil), b.state.EthernetDevices...)
@@ -5,6 +5,12 @@ import (
"github.com/godbus/dbus/v5"
)
const (
dbusNMSettingsPath = "/org/freedesktop/NetworkManager/Settings"
dbusNMSettingsInterface = "org.freedesktop.NetworkManager.Settings"
dbusNMSettingsConnectionInterface = "org.freedesktop.NetworkManager.Settings.Connection"
)
func (b *NetworkManagerBackend) startSignalPump() error {
conn, err := dbus.ConnectSystemBus()
if err != nil {
@@ -27,8 +33,8 @@ func (b *NetworkManagerBackend) startSignalPump() error {
}
if err := conn.AddMatchSignal(
dbus.WithMatchObjectPath(dbus.ObjectPath("/org/freedesktop/NetworkManager/Settings")),
dbus.WithMatchInterface("org.freedesktop.NetworkManager.Settings"),
dbus.WithMatchObjectPath(dbus.ObjectPath(dbusNMSettingsPath)),
dbus.WithMatchInterface(dbusNMSettingsInterface),
dbus.WithMatchMember("NewConnection"),
); err != nil {
conn.RemoveMatchSignal(
@@ -42,8 +48,8 @@ func (b *NetworkManagerBackend) startSignalPump() error {
}
if err := conn.AddMatchSignal(
dbus.WithMatchObjectPath(dbus.ObjectPath("/org/freedesktop/NetworkManager/Settings")),
dbus.WithMatchInterface("org.freedesktop.NetworkManager.Settings"),
dbus.WithMatchObjectPath(dbus.ObjectPath(dbusNMSettingsPath)),
dbus.WithMatchInterface(dbusNMSettingsInterface),
dbus.WithMatchMember("ConnectionRemoved"),
); err != nil {
conn.RemoveMatchSignal(
@@ -52,8 +58,8 @@ func (b *NetworkManagerBackend) startSignalPump() error {
dbus.WithMatchMember("PropertiesChanged"),
)
conn.RemoveMatchSignal(
dbus.WithMatchObjectPath(dbus.ObjectPath("/org/freedesktop/NetworkManager/Settings")),
dbus.WithMatchInterface("org.freedesktop.NetworkManager.Settings"),
dbus.WithMatchObjectPath(dbus.ObjectPath(dbusNMSettingsPath)),
dbus.WithMatchInterface(dbusNMSettingsInterface),
dbus.WithMatchMember("NewConnection"),
)
conn.RemoveSignal(signals)
@@ -61,6 +67,31 @@ func (b *NetworkManagerBackend) startSignalPump() error {
return err
}
if err := conn.AddMatchSignal(
dbus.WithMatchPathNamespace(dbus.ObjectPath(dbusNMSettingsPath)),
dbus.WithMatchInterface(dbusNMSettingsConnectionInterface),
dbus.WithMatchMember("Updated"),
); err != nil {
conn.RemoveMatchSignal(
dbus.WithMatchObjectPath(dbus.ObjectPath(dbusNMPath)),
dbus.WithMatchInterface(dbusPropsInterface),
dbus.WithMatchMember("PropertiesChanged"),
)
conn.RemoveMatchSignal(
dbus.WithMatchObjectPath(dbus.ObjectPath(dbusNMSettingsPath)),
dbus.WithMatchInterface(dbusNMSettingsInterface),
dbus.WithMatchMember("NewConnection"),
)
conn.RemoveMatchSignal(
dbus.WithMatchObjectPath(dbus.ObjectPath(dbusNMSettingsPath)),
dbus.WithMatchInterface(dbusNMSettingsInterface),
dbus.WithMatchMember("ConnectionRemoved"),
)
conn.RemoveSignal(signals)
conn.Close()
return err
}
if err := conn.AddMatchSignal(
dbus.WithMatchObjectPath(dbus.ObjectPath(dbusNMPath)),
dbus.WithMatchInterface(dbusNMInterface),
@@ -137,6 +168,32 @@ func (b *NetworkManagerBackend) stopSignalPump() {
dbus.WithMatchMember("PropertiesChanged"),
)
b.dbusConn.RemoveMatchSignal(
dbus.WithMatchObjectPath(dbus.ObjectPath(dbusNMSettingsPath)),
dbus.WithMatchInterface(dbusNMSettingsInterface),
dbus.WithMatchMember("NewConnection"),
)
b.dbusConn.RemoveMatchSignal(
dbus.WithMatchObjectPath(dbus.ObjectPath(dbusNMSettingsPath)),
dbus.WithMatchInterface(dbusNMSettingsInterface),
dbus.WithMatchMember("ConnectionRemoved"),
)
b.dbusConn.RemoveMatchSignal(
dbus.WithMatchPathNamespace(dbus.ObjectPath(dbusNMSettingsPath)),
dbus.WithMatchInterface(dbusNMSettingsConnectionInterface),
dbus.WithMatchMember("Updated"),
)
b.dbusConn.RemoveMatchSignal(
dbus.WithMatchObjectPath(dbus.ObjectPath(dbusNMPath)),
dbus.WithMatchInterface(dbusNMInterface),
dbus.WithMatchMember("DeviceAdded"),
)
b.dbusConn.RemoveMatchSignal(
dbus.WithMatchObjectPath(dbus.ObjectPath(dbusNMPath)),
dbus.WithMatchInterface(dbusNMInterface),
dbus.WithMatchMember("DeviceRemoved"),
)
for _, info := range b.wifiDevices {
b.dbusConn.RemoveMatchSignal(
dbus.WithMatchObjectPath(dbus.ObjectPath(info.device.GetPath())),
@@ -164,9 +221,13 @@ func (b *NetworkManagerBackend) stopSignalPump() {
}
func (b *NetworkManagerBackend) handleDBusSignal(sig *dbus.Signal) {
if sig.Name == "org.freedesktop.NetworkManager.Settings.NewConnection" ||
sig.Name == "org.freedesktop.NetworkManager.Settings.ConnectionRemoved" {
if sig.Name == dbusNMSettingsInterface+".NewConnection" ||
sig.Name == dbusNMSettingsInterface+".ConnectionRemoved" ||
sig.Name == dbusNMSettingsConnectionInterface+".Updated" {
b.ListVPNProfiles()
if err := b.updateSavedWiFiNetworks(); err != nil {
b.updateWiFiNetworks()
}
if b.onStateChange != nil {
b.onStateChange()
}
@@ -225,24 +225,14 @@ func (b *NetworkManagerBackend) GetWiFiQRCodeContent(ssid string) (string, error
return "", fmt.Errorf("failed to identify security type of network `%s`", ssid)
}
var securityType string
switch keyMgmt {
case "none":
authAlg, _ := secSettings["auth-alg"].(string)
switch authAlg {
case "open":
securityType = "nopass"
default:
securityType = "WEP"
}
return "", fmt.Errorf("QR code generation only supports WPA-PSK connections, `%s` is open or WEP", ssid)
case "ieee8021x":
securityType = "WEP"
return "", fmt.Errorf("QR code generation only supports WPA-PSK connections, `%s` is enterprise", ssid)
case "wpa-psk", "sae", "wpa-psk-sae":
default:
securityType = "WPA"
}
if securityType != "WPA" {
return "", fmt.Errorf("QR code generation only supports WPA connections, `%s` uses %s", ssid, securityType)
return "", fmt.Errorf("QR code generation only supports WPA-PSK connections, `%s` uses %s", ssid, keyMgmt)
}
var psk string
@@ -276,7 +266,7 @@ func (b *NetworkManagerBackend) GetWiFiQRCodeContent(ssid string) (string, error
return "", fmt.Errorf("failed to retrieve password for `%s`", ssid)
}
return FormatWiFiQRString(securityType, ssid, psk), nil
return FormatWiFiQRString("WPA", ssid, psk), nil
}
func (b *NetworkManagerBackend) ConnectWiFi(req ConnectionRequest) error {
@@ -405,6 +395,74 @@ func (b *NetworkManagerBackend) ForgetWiFiNetwork(ssid string) error {
return nil
}
func getSavedWiFiProfiles(connections []gonetworkmanager.Connection) map[string]savedWiFiProfile {
profiles := make(map[string]savedWiFiProfile)
for _, conn := range connections {
connSettings, err := conn.GetSettings()
if err != nil {
continue
}
connMeta, ok := connSettings["connection"]
if !ok {
continue
}
connType, ok := connMeta["type"].(string)
if !ok || connType != "802-11-wireless" {
continue
}
wifiSettings, ok := connSettings["802-11-wireless"]
if !ok {
continue
}
ssidBytes, ok := wifiSettings["ssid"].([]byte)
if !ok || len(ssidBytes) == 0 {
continue
}
ssid := string(ssidBytes)
profile := savedWiFiProfile{
Autoconnect: true,
Mode: "infrastructure",
}
if ac, ok := connMeta["autoconnect"].(bool); ok {
profile.Autoconnect = ac
}
if hidden, ok := wifiSettings["hidden"].(bool); ok {
profile.Hidden = hidden
}
if mode, ok := wifiSettings["mode"].(string); ok && mode != "" {
profile.Mode = mode
}
if _, ok := connSettings["802-11-wireless-security"]; ok {
profile.Secured = true
}
if _, ok := connSettings["802-1x"]; ok {
profile.Enterprise = true
profile.Secured = true
}
if existing, ok := profiles[ssid]; ok {
profile.Autoconnect = profile.Autoconnect || existing.Autoconnect
profile.Hidden = profile.Hidden || existing.Hidden
profile.Secured = profile.Secured || existing.Secured
profile.Enterprise = profile.Enterprise || existing.Enterprise
if profile.Mode == "" {
profile.Mode = existing.Mode
}
}
profiles[ssid] = profile
}
return profiles
}
func (b *NetworkManagerBackend) IsConnectingTo(ssid string) bool {
b.stateMutex.RLock()
defer b.stateMutex.RUnlock()
@@ -442,47 +500,7 @@ func (b *NetworkManagerBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
return nil, fmt.Errorf("failed to get connections: %w", err)
}
savedSSIDs := make(map[string]bool)
autoconnectMap := make(map[string]bool)
hiddenSSIDs := make(map[string]bool)
for _, conn := range connections {
connSettings, err := conn.GetSettings()
if err != nil {
continue
}
connMeta, ok := connSettings["connection"]
if !ok {
continue
}
connType, ok := connMeta["type"].(string)
if !ok || connType != "802-11-wireless" {
continue
}
wifiSettings, ok := connSettings["802-11-wireless"]
if !ok {
continue
}
ssidBytes, ok := wifiSettings["ssid"].([]byte)
if !ok {
continue
}
ssid := string(ssidBytes)
savedSSIDs[ssid] = true
autoconnect := true
if ac, ok := connMeta["autoconnect"].(bool); ok {
autoconnect = ac
}
autoconnectMap[ssid] = autoconnect
if hidden, ok := wifiSettings["hidden"].(bool); ok && hidden {
hiddenSSIDs[ssid] = true
}
}
savedProfiles := getSavedWiFiProfiles(connections)
b.stateMutex.RLock()
currentSSID := b.state.WiFiSSID
@@ -491,8 +509,8 @@ func (b *NetworkManagerBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
wifiBSSID := b.state.WiFiBSSID
b.stateMutex.RUnlock()
seenSSIDs := make(map[string]*WiFiNetwork)
networks := []WiFiNetwork{}
seenSSIDs := make(map[string]int)
networks := make([]WiFiNetwork, 0, len(apPaths)+1)
for _, ap := range apPaths {
ssid, err := ap.GetPropertySSID()
@@ -500,7 +518,8 @@ func (b *NetworkManagerBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
continue
}
if existing, exists := seenSSIDs[ssid]; exists {
if existingIndex, exists := seenSSIDs[ssid]; exists {
existing := &networks[existingIndex]
strength, _ := ap.GetPropertyStrength()
if strength > existing.Signal {
existing.Signal = strength
@@ -550,6 +569,7 @@ func (b *NetworkManagerBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
}
}
profile, saved := savedProfiles[ssid]
network := WiFiNetwork{
SSID: ssid,
BSSID: bssid,
@@ -557,45 +577,86 @@ func (b *NetworkManagerBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
Secured: secured,
Enterprise: enterprise,
Connected: isConnected,
Saved: savedSSIDs[ssid],
Autoconnect: autoconnectMap[ssid],
Hidden: hiddenSSIDs[ssid],
Saved: saved,
Autoconnect: profile.Autoconnect,
Hidden: profile.Hidden,
Frequency: freq,
Mode: modeStr,
Rate: rate,
Channel: channel,
}
seenSSIDs[ssid] = &network
networks = append(networks, network)
seenSSIDs[ssid] = len(networks) - 1
}
if wifiConnected && currentSSID != "" {
if _, exists := seenSSIDs[currentSSID]; !exists {
profile, saved := savedProfiles[currentSSID]
hiddenNetwork := WiFiNetwork{
SSID: currentSSID,
BSSID: wifiBSSID,
Signal: wifiSignal,
Secured: true,
Connected: true,
Saved: savedSSIDs[currentSSID],
Autoconnect: autoconnectMap[currentSSID],
Saved: saved,
Autoconnect: profile.Autoconnect,
Hidden: true,
Mode: "infrastructure",
}
networks = append(networks, hiddenNetwork)
seenSSIDs[currentSSID] = len(networks) - 1
}
}
visibleNetworks := wiFiNetworksBySSID(networks, true)
savedNetworks := savedWiFiNetworksFromProfiles(savedProfiles, visibleNetworks, currentSSID, wifiConnected)
sortWiFiNetworks(networks)
b.stateMutex.Lock()
b.state.WiFiNetworks = networks
b.state.SavedWiFiNetworks = savedNetworks
b.stateMutex.Unlock()
return networks, nil
}
func (b *NetworkManagerBackend) updateSavedWiFiNetworks() error {
s := b.settings
if s == nil {
var err error
s, err = gonetworkmanager.NewSettings()
if err != nil {
return fmt.Errorf("failed to get settings: %w", err)
}
b.settings = s
}
settingsMgr := s.(gonetworkmanager.Settings)
connections, err := settingsMgr.ListConnections()
if err != nil {
return fmt.Errorf("failed to get connections: %w", err)
}
savedProfiles := getSavedWiFiProfiles(connections)
b.stateMutex.RLock()
currentSSID := b.state.WiFiSSID
wifiConnected := b.state.WiFiConnected
wifiNetworks := append([]WiFiNetwork(nil), b.state.WiFiNetworks...)
b.stateMutex.RUnlock()
wifiNetworks, savedNetworks := refreshSavedWiFiState(wifiNetworks, savedProfiles, currentSSID, wifiConnected)
b.stateMutex.Lock()
b.state.WiFiNetworks = wifiNetworks
b.state.SavedWiFiNetworks = savedNetworks
b.stateMutex.Unlock()
return nil
}
func (b *NetworkManagerBackend) findConnection(ssid string) (gonetworkmanager.Connection, error) {
s := b.settings
if s == nil {
@@ -975,49 +1036,14 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
return
}
savedSSIDs := make(map[string]bool)
autoconnectMap := make(map[string]bool)
hiddenSSIDs := make(map[string]bool)
for _, conn := range connections {
connSettings, err := conn.GetSettings()
if err != nil {
continue
}
connMeta, ok := connSettings["connection"]
if !ok {
continue
}
connType, ok := connMeta["type"].(string)
if !ok || connType != "802-11-wireless" {
continue
}
wifiSettings, ok := connSettings["802-11-wireless"]
if !ok {
continue
}
ssidBytes, ok := wifiSettings["ssid"].([]byte)
if !ok {
continue
}
ssid := string(ssidBytes)
savedSSIDs[ssid] = true
autoconnect := true
if ac, ok := connMeta["autoconnect"].(bool); ok {
autoconnect = ac
}
autoconnectMap[ssid] = autoconnect
if hidden, ok := wifiSettings["hidden"].(bool); ok && hidden {
hiddenSSIDs[ssid] = true
}
}
savedProfiles := getSavedWiFiProfiles(connections)
var devices []WiFiDevice
visibleNetworks := make(map[string]WiFiNetwork)
b.stateMutex.RLock()
currentSSID := b.state.WiFiSSID
wifiConnected := b.state.WiFiConnected
b.stateMutex.RUnlock()
for name, devInfo := range b.wifiDevices {
state, _ := devInfo.device.GetPropertyState()
@@ -1050,14 +1076,16 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
apPaths, err := devInfo.wireless.GetAccessPoints()
var networks []WiFiNetwork
if err == nil {
seenSSIDs := make(map[string]*WiFiNetwork)
seenSSIDs := make(map[string]int)
networks = make([]WiFiNetwork, 0, len(apPaths)+1)
for _, ap := range apPaths {
apSSID, err := ap.GetPropertySSID()
if err != nil || apSSID == "" {
continue
}
if existing, exists := seenSSIDs[apSSID]; exists {
if existingIndex, exists := seenSSIDs[apSSID]; exists {
existing := &networks[existingIndex]
strength, _ := ap.GetPropertyStrength()
if strength > existing.Signal {
existing.Signal = strength
@@ -1107,6 +1135,7 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
}
}
profile, saved := savedProfiles[apSSID]
network := WiFiNetwork{
SSID: apSSID,
BSSID: apBSSID,
@@ -1114,9 +1143,9 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
Secured: secured,
Enterprise: enterprise,
Connected: isConnected,
Saved: savedSSIDs[apSSID],
Autoconnect: autoconnectMap[apSSID],
Hidden: hiddenSSIDs[apSSID],
Saved: saved,
Autoconnect: profile.Autoconnect,
Hidden: profile.Hidden,
Frequency: freq,
Mode: modeStr,
Rate: rate,
@@ -1124,25 +1153,31 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
Device: name,
}
seenSSIDs[apSSID] = &network
networks = append(networks, network)
seenSSIDs[apSSID] = len(networks) - 1
if existing, ok := visibleNetworks[apSSID]; !ok || network.Signal > existing.Signal {
visibleNetworks[apSSID] = network
}
}
if connected && ssid != "" {
if _, exists := seenSSIDs[ssid]; !exists {
profile, saved := savedProfiles[ssid]
hiddenNetwork := WiFiNetwork{
SSID: ssid,
BSSID: bssid,
Signal: signal,
Secured: true,
Connected: true,
Saved: savedSSIDs[ssid],
Autoconnect: autoconnectMap[ssid],
Saved: saved,
Autoconnect: profile.Autoconnect,
Hidden: true,
Mode: "infrastructure",
Device: name,
}
networks = append(networks, hiddenNetwork)
seenSSIDs[ssid] = len(networks) - 1
visibleNetworks[ssid] = hiddenNetwork
}
}
@@ -1168,6 +1203,7 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
b.stateMutex.Lock()
b.state.WiFiDevices = devices
b.state.SavedWiFiNetworks = savedWiFiNetworksFromProfiles(savedProfiles, visibleNetworks, currentSSID, wifiConnected)
b.stateMutex.Unlock()
}
@@ -4,6 +4,7 @@ import (
"testing"
mock_gonetworkmanager "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/github.com/Wifx/gonetworkmanager/v2"
"github.com/Wifx/gonetworkmanager/v2"
"github.com/stretchr/testify/assert"
)
@@ -176,6 +177,54 @@ func TestNetworkManagerBackend_UpdateWiFiNetworks_NoDevice(t *testing.T) {
assert.Contains(t, err.Error(), "no WiFi device available")
}
func TestNetworkManagerBackend_UpdateSavedWiFiNetworksPreservesVisibleSavedNetworks(t *testing.T) {
mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
mockSettings := mock_gonetworkmanager.NewMockSettings(t)
mockConn := mock_gonetworkmanager.NewMockConnection(t)
backend, err := NewNetworkManagerBackend(mockNM)
assert.NoError(t, err)
backend.settings = mockSettings
backend.stateMutex.Lock()
backend.state.WiFiNetworks = []WiFiNetwork{
{
SSID: "Home",
Signal: 76,
},
}
backend.stateMutex.Unlock()
settings := gonetworkmanager.ConnectionSettings{
"connection": {
"type": "802-11-wireless",
"autoconnect": true,
},
"802-11-wireless": {
"ssid": []byte("Home"),
},
"802-11-wireless-security": {},
}
mockSettings.EXPECT().ListConnections().Return([]gonetworkmanager.Connection{mockConn}, nil)
mockConn.EXPECT().GetSettings().Return(settings, nil)
err = backend.updateSavedWiFiNetworks()
assert.NoError(t, err)
backend.stateMutex.RLock()
savedNetworks := append([]WiFiNetwork(nil), backend.state.SavedWiFiNetworks...)
wifiNetworks := append([]WiFiNetwork(nil), backend.state.WiFiNetworks...)
backend.stateMutex.RUnlock()
assert.Len(t, wifiNetworks, 1)
assert.True(t, wifiNetworks[0].Saved)
assert.Len(t, savedNetworks, 1)
assert.Equal(t, "Home", savedNetworks[0].SSID)
assert.True(t, savedNetworks[0].Saved)
assert.False(t, savedNetworks[0].OutOfRange)
assert.Equal(t, uint8(76), savedNetworks[0].Signal)
}
func TestNetworkManagerBackend_FindConnection_NoSettings(t *testing.T) {
mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
+26 -3
View File
@@ -64,9 +64,10 @@ func NewManager() (*Manager, error) {
m := &Manager{
backend: backend,
state: &NetworkState{
NetworkStatus: StatusDisconnected,
Preference: PreferenceAuto,
WiFiNetworks: []WiFiNetwork{},
NetworkStatus: StatusDisconnected,
Preference: PreferenceAuto,
WiFiNetworks: []WiFiNetwork{},
SavedWiFiNetworks: []WiFiNetwork{},
},
stateMutex: sync.RWMutex{},
@@ -120,6 +121,7 @@ func (m *Manager) syncStateFromBackend() error {
m.state.WiFiBSSID = backendState.WiFiBSSID
m.state.WiFiSignal = backendState.WiFiSignal
m.state.WiFiNetworks = backendState.WiFiNetworks
m.state.SavedWiFiNetworks = backendState.SavedWiFiNetworks
m.state.WiFiDevices = backendState.WiFiDevices
m.state.WiredConnections = backendState.WiredConnections
m.state.VPNProfiles = backendState.VPNProfiles
@@ -156,6 +158,7 @@ func (m *Manager) snapshotState() NetworkState {
defer m.stateMutex.RUnlock()
s := *m.state
s.WiFiNetworks = append([]WiFiNetwork(nil), m.state.WiFiNetworks...)
s.SavedWiFiNetworks = append([]WiFiNetwork(nil), m.state.SavedWiFiNetworks...)
s.WiFiDevices = append([]WiFiDevice(nil), m.state.WiFiDevices...)
s.WiredConnections = append([]WiredConnection(nil), m.state.WiredConnections...)
s.EthernetDevices = append([]EthernetDevice(nil), m.state.EthernetDevices...)
@@ -211,6 +214,9 @@ func stateChangedMeaningfully(old, new *NetworkState) bool {
if len(old.WiFiNetworks) != len(new.WiFiNetworks) {
return true
}
if len(old.SavedWiFiNetworks) != len(new.SavedWiFiNetworks) {
return true
}
if len(old.WiFiDevices) != len(new.WiFiDevices) {
return true
}
@@ -238,6 +244,23 @@ func stateChangedMeaningfully(old, new *NetworkState) bool {
}
}
for i := range old.SavedWiFiNetworks {
oldNet := &old.SavedWiFiNetworks[i]
newNet := &new.SavedWiFiNetworks[i]
if oldNet.SSID != newNet.SSID {
return true
}
if oldNet.Connected != newNet.Connected {
return true
}
if oldNet.Autoconnect != newNet.Autoconnect {
return true
}
if oldNet.OutOfRange != newNet.OutOfRange {
return true
}
}
for i := range old.WiredConnections {
oldNet := &old.WiredConnections[i]
newNet := &new.WiredConnections[i]
+2
View File
@@ -34,6 +34,7 @@ type WiFiNetwork struct {
Saved bool `json:"saved"`
Autoconnect bool `json:"autoconnect"`
Hidden bool `json:"hidden"`
OutOfRange bool `json:"outOfRange"`
Frequency uint32 `json:"frequency"`
Mode string `json:"mode"`
Rate uint32 `json:"rate"`
@@ -111,6 +112,7 @@ type NetworkState struct {
WiFiBSSID string `json:"wifiBSSID"`
WiFiSignal uint8 `json:"wifiSignal"`
WiFiNetworks []WiFiNetwork `json:"wifiNetworks"`
SavedWiFiNetworks []WiFiNetwork `json:"savedWifiNetworks"`
WiFiDevices []WiFiDevice `json:"wifiDevices"`
WiredConnections []WiredConnection `json:"wiredConnections"`
VPNProfiles []VPNProfile `json:"vpnProfiles"`
+103
View File
@@ -0,0 +1,103 @@
package network
import "sort"
type savedWiFiProfile struct {
Autoconnect bool
Hidden bool
Secured bool
Enterprise bool
Mode string
}
// Saved WiFi state is keyed by SSID because the UI/API accepts SSID actions.
// Multiple backend profiles for the same SSID are intentionally collapsed here.
func mergeSavedProfilesIntoWiFiNetworks(networks []WiFiNetwork, profiles map[string]savedWiFiProfile, currentSSID string, wifiConnected bool) []WiFiNetwork {
merged := make([]WiFiNetwork, len(networks))
for i, network := range networks {
profile, saved := profiles[network.SSID]
network.Connected = wifiConnected && network.SSID == currentSSID
network.Saved = saved
if saved {
network.Autoconnect = profile.Autoconnect
network.Hidden = network.Hidden || profile.Hidden
network.Secured = network.Secured || profile.Secured
network.Enterprise = network.Enterprise || profile.Enterprise
if network.Mode == "" {
network.Mode = profile.Mode
}
} else {
network.Autoconnect = false
}
merged[i] = network
}
return merged
}
func wiFiNetworksBySSID(networks []WiFiNetwork, visibleOnly bool) map[string]WiFiNetwork {
visible := make(map[string]WiFiNetwork, len(networks))
for _, network := range networks {
if visibleOnly && network.OutOfRange {
continue
}
visible[network.SSID] = network
}
return visible
}
func refreshSavedWiFiState(networks []WiFiNetwork, profiles map[string]savedWiFiProfile, currentSSID string, wifiConnected bool) ([]WiFiNetwork, []WiFiNetwork) {
mergedNetworks := mergeSavedProfilesIntoWiFiNetworks(networks, profiles, currentSSID, wifiConnected)
visibleNetworks := wiFiNetworksBySSID(mergedNetworks, true)
savedNetworks := savedWiFiNetworksFromProfiles(profiles, visibleNetworks, currentSSID, wifiConnected)
return mergedNetworks, savedNetworks
}
func savedWiFiNetworksFromProfiles(profiles map[string]savedWiFiProfile, visible map[string]WiFiNetwork, currentSSID string, wifiConnected bool) []WiFiNetwork {
networks := make([]WiFiNetwork, 0, len(profiles))
for ssid, profile := range profiles {
if network, ok := visible[ssid]; ok {
network.Saved = true
network.Autoconnect = profile.Autoconnect
network.Hidden = network.Hidden || profile.Hidden
network.Secured = network.Secured || profile.Secured
network.Enterprise = network.Enterprise || profile.Enterprise
network.OutOfRange = false
if network.Mode == "" {
network.Mode = profile.Mode
}
networks = append(networks, network)
continue
}
isConnected := wifiConnected && ssid == currentSSID
networks = append(networks, WiFiNetwork{
SSID: ssid,
Secured: profile.Secured,
Enterprise: profile.Enterprise,
Connected: isConnected,
Saved: true,
Autoconnect: profile.Autoconnect,
Hidden: profile.Hidden,
OutOfRange: !isConnected,
Mode: profile.Mode,
})
}
sort.Slice(networks, func(i, j int) bool {
if networks[i].Connected && !networks[j].Connected {
return true
}
if !networks[i].Connected && networks[j].Connected {
return false
}
if networks[i].OutOfRange != networks[j].OutOfRange {
return !networks[i].OutOfRange
}
if networks[i].Signal != networks[j].Signal {
return networks[i].Signal > networks[j].Signal
}
return networks[i].SSID < networks[j].SSID
})
return networks
}
@@ -0,0 +1,170 @@
package network
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestMergeSavedProfilesIntoWiFiNetworks(t *testing.T) {
networks := []WiFiNetwork{
{
SSID: "Home",
Signal: 80,
Secured: false,
Autoconnect: false,
},
{
SSID: "Cafe",
Signal: 50,
Secured: false,
Autoconnect: true,
},
}
profiles := map[string]savedWiFiProfile{
"Home": {
Autoconnect: true,
Hidden: true,
Secured: true,
Mode: "infrastructure",
},
}
merged := mergeSavedProfilesIntoWiFiNetworks(networks, profiles, "Home", true)
assert.Len(t, merged, 2)
assert.Equal(t, "Home", merged[0].SSID)
assert.True(t, merged[0].Connected)
assert.True(t, merged[0].Saved)
assert.True(t, merged[0].Autoconnect)
assert.True(t, merged[0].Hidden)
assert.True(t, merged[0].Secured)
assert.Equal(t, "infrastructure", merged[0].Mode)
assert.Equal(t, "Cafe", merged[1].SSID)
assert.False(t, merged[1].Saved)
assert.False(t, merged[1].Autoconnect)
}
func TestSavedWiFiNetworksFromProfilesOutOfRangeWithoutVisibleNetworks(t *testing.T) {
profiles := map[string]savedWiFiProfile{
"Home": {
Autoconnect: true,
Secured: true,
Mode: "infrastructure",
},
}
networks := savedWiFiNetworksFromProfiles(profiles, nil, "", false)
assert.Len(t, networks, 1)
assert.Equal(t, "Home", networks[0].SSID)
assert.True(t, networks[0].Saved)
assert.True(t, networks[0].OutOfRange)
assert.Equal(t, uint8(0), networks[0].Signal)
}
func TestSavedWiFiNetworksFromProfilesKeepsConnectedCurrentNetworkInRange(t *testing.T) {
profiles := map[string]savedWiFiProfile{
"Home": {
Autoconnect: true,
Secured: true,
},
}
networks := savedWiFiNetworksFromProfiles(profiles, nil, "Home", true)
assert.Len(t, networks, 1)
assert.Equal(t, "Home", networks[0].SSID)
assert.True(t, networks[0].Connected)
assert.False(t, networks[0].OutOfRange)
}
func TestSavedWiFiNetworksFromProfilesIncludesOutOfRange(t *testing.T) {
profiles := map[string]savedWiFiProfile{
"Home": {
Autoconnect: true,
Hidden: true,
Secured: true,
Mode: "infrastructure",
},
"Office": {
Autoconnect: false,
Secured: true,
Enterprise: true,
Mode: "infrastructure",
},
}
visible := map[string]WiFiNetwork{
"Home": {
SSID: "Home",
Signal: 72,
Secured: true,
Connected: true,
},
}
networks := savedWiFiNetworksFromProfiles(profiles, visible, "Home", true)
assert.Len(t, networks, 2)
assert.Equal(t, "Home", networks[0].SSID)
assert.True(t, networks[0].Saved)
assert.True(t, networks[0].Connected)
assert.False(t, networks[0].OutOfRange)
assert.True(t, networks[0].Hidden)
assert.Equal(t, uint8(72), networks[0].Signal)
assert.Equal(t, "Office", networks[1].SSID)
assert.True(t, networks[1].Saved)
assert.False(t, networks[1].Autoconnect)
assert.True(t, networks[1].Enterprise)
assert.True(t, networks[1].OutOfRange)
}
func TestWiFiNetworksBySSIDVisibleOnlySkipsOutOfRange(t *testing.T) {
visible := wiFiNetworksBySSID([]WiFiNetwork{
{SSID: "Home", Signal: 70},
{SSID: "Office", Signal: 0, OutOfRange: true},
}, true)
assert.Contains(t, visible, "Home")
assert.NotContains(t, visible, "Office")
}
func TestRefreshSavedWiFiStatePreservesVisibleSavedNetworks(t *testing.T) {
networks := []WiFiNetwork{
{
SSID: "Home",
Signal: 82,
},
}
profiles := map[string]savedWiFiProfile{
"Home": {
Autoconnect: true,
Secured: true,
Mode: "infrastructure",
},
"Office": {
Autoconnect: false,
Secured: true,
Mode: "infrastructure",
},
}
mergedNetworks, savedNetworks := refreshSavedWiFiState(networks, profiles, "", false)
assert.Len(t, mergedNetworks, 1)
assert.Equal(t, "Home", mergedNetworks[0].SSID)
assert.True(t, mergedNetworks[0].Saved)
assert.True(t, mergedNetworks[0].Autoconnect)
assert.Len(t, savedNetworks, 2)
assert.Equal(t, "Home", savedNetworks[0].SSID)
assert.True(t, savedNetworks[0].Saved)
assert.False(t, savedNetworks[0].OutOfRange)
assert.Equal(t, uint8(82), savedNetworks[0].Signal)
assert.Equal(t, "Office", savedNetworks[1].SSID)
assert.True(t, savedNetworks[1].Saved)
assert.True(t, savedNetworks[1].OutOfRange)
}
+1 -1
View File
@@ -38,7 +38,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
)
const APIVersion = 25
const APIVersion = 26
var CLIVersion = "dev"
@@ -721,7 +721,7 @@ Rectangle {
DankActionButton {
id: qrCodeButton
visible: modelData.secured && modelData.saved
visible: modelData.secured && modelData.saved && !(modelData.enterprise || false)
anchors.right: parent.right
anchors.rightMargin: optionsButton.width + pinWifiRow.width + 3 * Theme.spacingM + Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
+493 -8
View File
@@ -1,6 +1,7 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Common
import qs.Modules.Settings.Widgets
@@ -16,6 +17,7 @@ Item {
Component.onCompleted: {
NetworkService.addRef();
Qt.callLater(() => NetworkService.refreshSavedWifiNetworks());
}
Component.onDestruction: {
@@ -40,6 +42,7 @@ Item {
id: root
property string expandedWifiSsid: ""
property string expandedSavedWifiSsid: ""
property int maxPinnedWifiNetworks: 3
function normalizePinList(value) {
@@ -84,6 +87,79 @@ Item {
settingKey: "networkWifi"
tags: ["wifi", "wi-fi", "wireless", "network", "ssid", "adapter", "radio"]
function visibleWifiBySsid(ssid) {
const networks = NetworkService.wifiNetworks || [];
return networks.find(network => network.ssid === ssid) || null;
}
function mergedSavedWifiNetworks() {
const saved = NetworkService.savedWifiNetworks || [];
const supportsSavedWifiState = DMSService.apiVersion >= NetworkService.savedWifiStateApiVersion;
const result = [];
const seen = new Set();
for (const network of saved) {
if (!network?.ssid || seen.has(network.ssid))
continue;
const isOutOfRange = supportsSavedWifiState ? network.outOfRange === true : false;
const visibleNetwork = !isOutOfRange ? visibleWifiBySsid(network.ssid) : null;
if (visibleNetwork) {
result.push(Object.assign({}, network, visibleNetwork, {
saved: true,
autoconnect: network.autoconnect ?? visibleNetwork.autoconnect,
hidden: (network.hidden || false) || (visibleNetwork.hidden || false),
outOfRange: false
}));
} else {
result.push(Object.assign({}, network, {
saved: true,
outOfRange: isOutOfRange
}));
}
seen.add(network.ssid);
}
return result;
}
function sortedSavedWifiNetworks() {
const ssid = NetworkService.currentWifiSSID;
const pinnedList = root.getPinnedWifiNetworks();
let sorted = root.mergedSavedWifiNetworks();
sorted.sort((a, b) => {
const aPinnedIndex = pinnedList.indexOf(a.ssid);
const bPinnedIndex = pinnedList.indexOf(b.ssid);
if (aPinnedIndex !== -1 || bPinnedIndex !== -1) {
if (aPinnedIndex === -1)
return 1;
if (bPinnedIndex === -1)
return -1;
return aPinnedIndex - bPinnedIndex;
}
if (a.ssid === ssid)
return -1;
if (b.ssid === ssid)
return 1;
if ((a.outOfRange || false) !== (b.outOfRange || false))
return (a.outOfRange || false) ? 1 : -1;
if ((a.signal || 0) !== (b.signal || 0))
return (b.signal || 0) - (a.signal || 0);
return (a.ssid || "").localeCompare(b.ssid || "");
});
return sorted;
}
function showForgetNetworkConfirm(ssid) {
forgetNetworkConfirm.showWithOptions({
title: I18n.tr("Forget Network"),
message: I18n.tr("Forget \"%1\"?").arg(ssid),
confirmText: I18n.tr("Forget"),
confirmColor: Theme.error,
onConfirm: () => NetworkService.forgetWifiNetwork(ssid)
});
}
Column {
id: wifiSection
@@ -563,7 +639,7 @@ Item {
DankActionButton {
iconName: "qr_code"
buttonSize: 28
visible: modelData.secured && modelData.saved
visible: modelData.secured && modelData.saved && !(modelData.enterprise || false)
onClicked: {
PopoutService.showWifiQRCodeModal(modelData.ssid);
}
@@ -584,13 +660,7 @@ Item {
iconColor: Theme.error
visible: modelData.saved || isConnected
onClicked: {
forgetNetworkConfirm.showWithOptions({
title: I18n.tr("Forget Network"),
message: I18n.tr("Forget \"%1\"?").arg(modelData.ssid),
confirmText: I18n.tr("Forget"),
confirmColor: Theme.error,
onConfirm: () => NetworkService.forgetWifiNetwork(modelData.ssid)
});
root.showForgetNetworkConfirm(modelData.ssid);
}
}
}
@@ -756,6 +826,421 @@ Item {
}
}
}
SettingsCard {
id: savedWifiCard
readonly property var savedNetworks: root.sortedSavedWifiNetworks()
width: parent.width
title: I18n.tr("Saved Networks")
iconName: "bookmark"
settingKey: "networkSavedWifi"
tags: ["wifi", "wi-fi", "wireless", "network", "saved", "known", "ssid", "autoconnect", "forget"]
collapsible: true
expanded: false
visible: savedNetworks.length > 0
headerActions: [
StyledText {
text: savedWifiCard.savedNetworks.length
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
verticalAlignment: Text.AlignVCenter
}
]
Column {
width: parent.width
spacing: 4
Repeater {
model: savedWifiCard.expanded ? savedWifiCard.savedNetworks : []
delegate: Rectangle {
id: savedWifiDelegate
required property var modelData
required property int index
readonly property bool isConnected: modelData.ssid === NetworkService.currentWifiSSID
readonly property bool isPinned: root.getPinnedWifiNetworks().includes(modelData.ssid)
readonly property bool isOutOfRange: modelData.outOfRange || false
readonly property bool isExpanded: !isOutOfRange && root.expandedSavedWifiSsid === modelData.ssid
width: parent.width
height: isExpanded ? 56 + savedWifiExpandedContent.height : 56
radius: Theme.cornerRadius
color: savedWifiMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
border.width: isConnected ? 2 : 0
border.color: Theme.primary
clip: true
Behavior on height {
NumberAnimation {
duration: 150
easing.type: Easing.OutQuad
}
}
Column {
anchors.fill: parent
spacing: 0
Item {
width: parent.width
height: 56
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
anchors.right: savedWifiActions.left
anchors.rightMargin: Theme.spacingS
spacing: Theme.spacingS
DankIcon {
name: {
if (isOutOfRange)
return "wifi_off";
const s = modelData.signal || 0;
if (s >= 50)
return "wifi";
if (s >= 25)
return "wifi_2_bar";
return "wifi_1_bar";
}
size: 20
color: isConnected ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
width: parent.width - 20 - Theme.spacingS
Row {
anchors.left: parent.left
spacing: Theme.spacingXS
width: parent.width
StyledText {
text: modelData.ssid || I18n.tr("Unknown")
font.pixelSize: Theme.fontSizeMedium
color: isConnected ? Theme.primary : Theme.surfaceText
font.weight: isConnected ? Font.Medium : Font.Normal
elide: Text.ElideRight
width: Math.max(0, parent.width - (savedWifiHiddenIcon.visible ? savedWifiHiddenIcon.width + Theme.spacingXS : 0))
}
DankIcon {
id: savedWifiHiddenIcon
name: "visibility_off"
size: 14
color: Theme.surfaceVariantText
visible: modelData.hidden || false
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: {
const parts = [isConnected ? I18n.tr("Connected") : (modelData.secured ? I18n.tr("Secured") : I18n.tr("Open"))];
parts.push(isOutOfRange ? I18n.tr("Unavailable") : (modelData.signal || 0) + "%");
if (modelData.hidden || false)
parts.push(I18n.tr("Hidden"));
return parts.join(" • ");
}
font.pixelSize: Theme.fontSizeSmall
color: isConnected ? Theme.primary : Theme.surfaceVariantText
width: parent.width
elide: Text.ElideRight
}
}
}
Row {
id: savedWifiActions
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
Rectangle {
width: 28
height: 28
radius: 14
color: savedWifiExpandBtn.containsMouse ? Theme.surfacePressed : "transparent"
visible: !isOutOfRange
DankIcon {
anchors.centerIn: parent
name: isExpanded ? "expand_less" : "expand_more"
size: 18
color: Theme.surfaceText
}
MouseArea {
id: savedWifiExpandBtn
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (isExpanded) {
root.expandedSavedWifiSsid = "";
} else {
root.expandedSavedWifiSsid = modelData.ssid;
}
}
}
}
DankActionButton {
iconName: "qr_code"
buttonSize: 28
visible: modelData.secured && !(modelData.enterprise || false)
onClicked: {
PopoutService.showWifiQRCodeModal(modelData.ssid);
}
}
DankActionButton {
iconName: "push_pin"
buttonSize: 28
iconColor: isPinned ? Theme.primary : Theme.surfaceVariantText
onClicked: {
root.toggleWifiPin(modelData.ssid);
}
}
DankActionButton {
id: savedWifiMoreButton
iconName: "more_horiz"
buttonSize: 28
onClicked: {
if (savedWifiMenu.visible) {
savedWifiMenu.close();
return;
}
savedWifiMenu.popup(savedWifiMoreButton, -savedWifiMenu.width + savedWifiMoreButton.width, savedWifiMoreButton.height + Theme.spacingXS);
}
}
}
MouseArea {
id: savedWifiMouseArea
anchors.fill: parent
anchors.rightMargin: savedWifiActions.width + Theme.spacingM
hoverEnabled: true
cursorShape: isOutOfRange ? Qt.ArrowCursor : Qt.PointingHandCursor
onClicked: {
if (isOutOfRange)
return;
if (isExpanded) {
root.expandedSavedWifiSsid = "";
} else {
root.expandedSavedWifiSsid = modelData.ssid;
}
}
}
}
Column {
id: savedWifiExpandedContent
width: parent.width
visible: isExpanded
Rectangle {
width: parent.width - Theme.spacingM * 2
height: 1
x: Theme.spacingM
color: Theme.outlineLight
}
Item {
width: parent.width
height: savedWifiDetailsColumn.implicitHeight + Theme.spacingM * 2
Column {
id: savedWifiDetailsColumn
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
Flow {
width: parent.width
spacing: Theme.spacingXS
Repeater {
model: {
const fields = [];
const net = modelData;
if (!net)
return fields;
fields.push({
label: I18n.tr("Signal"),
value: (net.signal || 0) + "%"
});
if (net.frequency)
fields.push({
label: I18n.tr("Frequency"),
value: (net.frequency / 1000).toFixed(1) + " GHz"
});
if (net.channel)
fields.push({
label: I18n.tr("Channel"),
value: String(net.channel)
});
if (net.rate)
fields.push({
label: I18n.tr("Rate"),
value: net.rate + " Mbps"
});
if (net.mode)
fields.push({
label: I18n.tr("Mode"),
value: net.mode
});
if (net.bssid)
fields.push({
label: I18n.tr("BSSID"),
value: net.bssid
});
fields.push({
label: I18n.tr("Security"),
value: net.secured ? (net.enterprise ? I18n.tr("Enterprise") : I18n.tr("WPA/WPA2")) : I18n.tr("Open")
});
return fields;
}
delegate: Rectangle {
required property var modelData
required property int index
width: savedWifiFieldContent.width + Theme.spacingM * 2
height: 32
radius: Theme.cornerRadius - 2
color: Theme.surfaceContainerHigh
border.width: 1
border.color: Theme.outlineLight
Row {
id: savedWifiFieldContent
anchors.centerIn: parent
spacing: Theme.spacingXS
StyledText {
text: modelData.label + ":"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: modelData.value
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
}
}
}
}
Menu {
id: savedWifiMenu
width: 170
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
background: Rectangle {
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
border.width: 0
}
MenuItem {
text: isConnected ? I18n.tr("Disconnect") : I18n.tr("Connect")
height: isOutOfRange ? 0 : 32
visible: !isOutOfRange
contentItem: StyledText {
text: parent.text
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
leftPadding: Theme.spacingS
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.cornerRadius / 2
}
onTriggered: {
if (isConnected) {
NetworkService.disconnectWifi();
return;
}
NetworkService.connectToWifi(modelData.ssid);
}
}
MenuItem {
text: modelData.autoconnect ? I18n.tr("Disable Autoconnect") : I18n.tr("Enable Autoconnect")
height: DMSService.apiVersion > 13 ? 32 : 0
visible: DMSService.apiVersion > 13
contentItem: StyledText {
text: parent.text
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
leftPadding: Theme.spacingS
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.cornerRadius / 2
}
onTriggered: {
NetworkService.setWifiAutoconnect(modelData.ssid, !(modelData.autoconnect || false));
}
}
MenuItem {
text: I18n.tr("Forget Network")
height: 32
contentItem: StyledText {
text: parent.text
font.pixelSize: Theme.fontSizeSmall
color: Theme.error
leftPadding: Theme.spacingS
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.hovered ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08) : "transparent"
radius: Theme.cornerRadius / 2
}
onTriggered: {
root.showForgetNetworkConfirm(modelData.ssid);
}
}
}
}
}
}
}
}
}
}
+20 -7
View File
@@ -69,6 +69,7 @@ Singleton {
property bool changingPreference: false
property string targetPreference: ""
property var savedWifiNetworks: []
readonly property int savedWifiStateApiVersion: 26
property string connectionStatus: ""
property string lastConnectionError: ""
property bool passwordDialogShouldReopen: false
@@ -309,17 +310,21 @@ Singleton {
if (state.wifiNetworks) {
wifiNetworks = state.wifiNetworks;
}
if (state.wifiNetworks || state.savedWifiNetworks) {
const hasSavedWifiState = DMSService.apiVersion >= savedWifiStateApiVersion && Array.isArray(state.savedWifiNetworks);
const sourceSavedNetworks = hasSavedWifiState ? state.savedWifiNetworks : (state.wifiNetworks || []).filter(network => network.saved);
const saved = [];
const mapping = {};
for (const network of state.wifiNetworks) {
if (network.saved) {
saved.push({
ssid: network.ssid,
saved: true
});
for (const network of sourceSavedNetworks) {
const normalized = Object.assign({}, network, {
saved: true,
outOfRange: hasSavedWifiState ? network.outOfRange === true : false
});
saved.push(normalized);
if (network?.ssid)
mapping[network.ssid] = network.ssid;
}
}
savedConnections = saved;
savedWifiNetworks = saved;
@@ -596,6 +601,7 @@ Singleton {
}
wifiNetworks = updated;
networksUpdated();
Qt.callLater(() => refreshSavedWifiNetworks());
}
forgetSSID = "";
});
@@ -985,4 +991,11 @@ Singleton {
}
});
}
function refreshSavedWifiNetworks() {
if (!networkAvailable)
return;
getState();
}
}
+3 -1
View File
@@ -142,9 +142,11 @@ Singleton {
readonly property var savedConnections: wifiNetworks.filter(n => n.saved).map(n => ({
"ssid": n.ssid,
"saved": true
"saved": true,
"outOfRange": false
}))
readonly property var savedWifiNetworks: savedConnections
readonly property int savedWifiStateApiVersion: 26
readonly property var ssidToConnectionName: {
const map = {};
for (const n of wifiNetworks) {
+7
View File
@@ -54,6 +54,7 @@ Singleton {
property bool changingPreference: activeService?.changingPreference ?? false
property string targetPreference: activeService?.targetPreference ?? ""
property var savedWifiNetworks: activeService?.savedWifiNetworks ?? []
readonly property int savedWifiStateApiVersion: activeService?.savedWifiStateApiVersion ?? 26
property string connectionStatus: activeService?.connectionStatus ?? ""
property string lastConnectionError: activeService?.lastConnectionError ?? ""
property bool passwordDialogShouldReopen: activeService?.passwordDialogShouldReopen ?? false
@@ -180,6 +181,12 @@ Singleton {
}
}
function refreshSavedWifiNetworks() {
if (activeService && activeService.refreshSavedWifiNetworks) {
activeService.refreshSavedWifiNetworks();
}
}
function connectToWifi(ssid, password = "", username = "", anonymousIdentity = "", domainSuffixMatch = "") {
if (activeService && activeService.connectToWifi) {
activeService.connectToWifi(ssid, password, username, anonymousIdentity, domainSuffixMatch);
@@ -8882,6 +8882,30 @@
],
"icon": "settings_ethernet"
},
{
"section": "networkSavedWifi",
"label": "Saved Networks",
"tabIndex": 40,
"category": "Network",
"keywords": [
"autoconnect",
"connection",
"connectivity",
"ethernet",
"forget",
"internet",
"known",
"network",
"networks",
"online",
"saved",
"ssid",
"wi-fi",
"wifi",
"wireless"
],
"icon": "bookmark"
},
{
"section": "networkWifi",
"label": "WiFi",