diff --git a/Assets/settings-default.json b/Assets/settings-default.json index 5057d9929..7ed5088c3 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -381,7 +381,6 @@ "indicatorOpacity": 0.6 }, "network": { - "airplaneModeEnabled": false, "bluetoothRssiPollingEnabled": false, "bluetoothRssiPollIntervalMs": 60000, "networkPanelView": "wifi", diff --git a/Commons/Settings.qml b/Commons/Settings.qml index ad6508e94..df3e42da6 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -579,7 +579,6 @@ Singleton { // network property JsonObject network: JsonObject { - property bool airplaneModeEnabled: false property bool bluetoothRssiPollingEnabled: false // Opt-in Bluetooth RSSI polling (uses bluetoothctl) property int bluetoothRssiPollIntervalMs: 60000 // Polling interval in milliseconds for RSSI queries property string networkPanelView: "wifi" diff --git a/Modules/Bar/Widgets/Bluetooth.qml b/Modules/Bar/Widgets/Bluetooth.qml index cd6146fb9..1b24d5155 100644 --- a/Modules/Bar/Widgets/Bluetooth.qml +++ b/Modules/Bar/Widgets/Bluetooth.qml @@ -49,7 +49,7 @@ Item { "label": BluetoothService.enabled ? I18n.tr("actions.disable-bluetooth") : I18n.tr("actions.enable-bluetooth"), "action": "toggle-bluetooth", "icon": BluetoothService.enabled ? "bluetooth-off" : "bluetooth", - "enabled": !Settings.data.network.airplaneModeEnabled && BluetoothService.bluetoothAvailable + "enabled": !NetworkService.airplaneModeEnabled && BluetoothService.bluetoothAvailable }, { "label": I18n.tr("common.bluetooth") + " " + I18n.tr("tooltips.open-settings"), diff --git a/Modules/Bar/Widgets/Network.qml b/Modules/Bar/Widgets/Network.qml index 8a61ff8e1..e7687abb3 100644 --- a/Modules/Bar/Widgets/Network.qml +++ b/Modules/Bar/Widgets/Network.qml @@ -49,7 +49,7 @@ Item { "label": NetworkService.wifiEnabled ? I18n.tr("actions.disable-wifi") : I18n.tr("actions.enable-wifi"), "action": "toggle-wifi", "icon": NetworkService.wifiEnabled ? "wifi-off" : "wifi", - "enabled": !Settings.data.network.airplaneModeEnabled && NetworkService.wifiAvailable + "enabled": !NetworkService.airplaneModeEnabled && NetworkService.wifiAvailable }, { "label": I18n.tr("common.wifi") + " " + I18n.tr("tooltips.open-settings"), diff --git a/Modules/Panels/Bluetooth/BluetoothPanel.qml b/Modules/Panels/Bluetooth/BluetoothPanel.qml index 92464d7b7..90a59ee95 100644 --- a/Modules/Panels/Bluetooth/BluetoothPanel.qml +++ b/Modules/Panels/Bluetooth/BluetoothPanel.qml @@ -54,7 +54,7 @@ SmartPanel { NToggle { id: bluetoothSwitch checked: BluetoothService.enabled - enabled: !Settings.data.network.airplaneModeEnabled && BluetoothService.bluetoothAvailable + enabled: !NetworkService.airplaneModeEnabled && BluetoothService.bluetoothAvailable onToggled: checked => BluetoothService.setBluetoothEnabled(checked) baseSize: Style.baseWidgetSize * 0.65 } diff --git a/Modules/Panels/ControlCenter/Widgets/AirplaneMode.qml b/Modules/Panels/ControlCenter/Widgets/AirplaneMode.qml index 6f4679bbd..d22963b8c 100644 --- a/Modules/Panels/ControlCenter/Widgets/AirplaneMode.qml +++ b/Modules/Panels/ControlCenter/Widgets/AirplaneMode.qml @@ -8,10 +8,11 @@ import qs.Widgets NIconButtonHot { property ShellScreen screen - icon: !Settings.data.network.airplaneModeEnabled ? "plane-off" : "plane" - hot: Settings.data.network.airplaneModeEnabled + icon: !NetworkService.airplaneModeEnabled ? "plane-off" : "plane" + hot: NetworkService.airplaneModeEnabled tooltipText: I18n.tr("toast.airplane-mode.title") onClicked: { - BluetoothService.setAirplaneMode(!Settings.data.network.airplaneModeEnabled); + NetworkService.setAirplaneMode(!NetworkService.airplaneModeEnabled); } + enabled: NetworkService.wifiAvailable && BluetoothService.bluetoothAvailable } diff --git a/Modules/Panels/ControlCenter/Widgets/Bluetooth.qml b/Modules/Panels/ControlCenter/Widgets/Bluetooth.qml index 071013601..b6ede8415 100644 --- a/Modules/Panels/ControlCenter/Widgets/Bluetooth.qml +++ b/Modules/Panels/ControlCenter/Widgets/Bluetooth.qml @@ -16,7 +16,7 @@ NIconButtonHot { p.toggle(this); } onRightClicked: { - if (!Settings.data.network.airplaneModeEnabled) { + if (!NetworkService.airplaneModeEnabled) { BluetoothService.setBluetoothEnabled(!BluetoothService.enabled); } } diff --git a/Modules/Panels/ControlCenter/Widgets/Network.qml b/Modules/Panels/ControlCenter/Widgets/Network.qml index 82bb55e03..9fe91eed2 100644 --- a/Modules/Panels/ControlCenter/Widgets/Network.qml +++ b/Modules/Panels/ControlCenter/Widgets/Network.qml @@ -14,7 +14,7 @@ NIconButtonHot { panel?.toggle(this); } onRightClicked: { - if (!Settings.data.network.airplaneModeEnabled) { + if (!NetworkService.airplaneModeEnabled) { NetworkService.setWifiEnabled(!NetworkService.wifiEnabled); } } diff --git a/Modules/Panels/Network/NetworkPanel.qml b/Modules/Panels/Network/NetworkPanel.qml index c0718d380..2918c6182 100644 --- a/Modules/Panels/Network/NetworkPanel.qml +++ b/Modules/Panels/Network/NetworkPanel.qml @@ -144,7 +144,7 @@ SmartPanel { id: wifiSwitch visible: panelViewMode === "wifi" checked: NetworkService.wifiEnabled - enabled: !Settings.data.network.airplaneModeEnabled && NetworkService.wifiAvailable + enabled: !NetworkService.airplaneModeEnabled && NetworkService.wifiAvailable onToggled: checked => NetworkService.setWifiEnabled(checked) baseSize: Style.baseWidgetSize * 0.7 // Slightly smaller } diff --git a/Modules/Panels/Settings/Tabs/Connections/BluetoothSubTab.qml b/Modules/Panels/Settings/Tabs/Connections/BluetoothSubTab.qml index 3183b6ad2..c424a52eb 100644 --- a/Modules/Panels/Settings/Tabs/Connections/BluetoothSubTab.qml +++ b/Modules/Panels/Settings/Tabs/Connections/BluetoothSubTab.qml @@ -161,7 +161,7 @@ Item { label: I18n.tr("common.bluetooth") icon: BluetoothService.enabled ? "bluetooth" : "bluetooth-off" checked: BluetoothService.enabled - enabled: !Settings.data.network.airplaneModeEnabled && BluetoothService.bluetoothAvailable + enabled: !NetworkService.airplaneModeEnabled && BluetoothService.bluetoothAvailable onToggled: checked => BluetoothService.setBluetoothEnabled(checked) Layout.alignment: Qt.AlignVCenter } diff --git a/Modules/Panels/Settings/Tabs/Connections/WifiSubTab.qml b/Modules/Panels/Settings/Tabs/Connections/WifiSubTab.qml index 918d5dd5d..092ac7720 100644 --- a/Modules/Panels/Settings/Tabs/Connections/WifiSubTab.qml +++ b/Modules/Panels/Settings/Tabs/Connections/WifiSubTab.qml @@ -149,7 +149,7 @@ Item { icon: NetworkService.wifiEnabled ? "wifi" : "wifi-off" checked: NetworkService.wifiEnabled onToggled: checked => NetworkService.setWifiEnabled(checked) - enabled: ProgramCheckerService.nmcliAvailable && !Settings.data.network.airplaneModeEnabled && NetworkService.wifiAvailable + enabled: !NetworkService.airplaneModeEnabled && NetworkService.wifiAvailable Layout.alignment: Qt.AlignVCenter } } @@ -324,7 +324,7 @@ Item { // Airplane Mode NBox { id: miscSettingsBox - visible: !root.showOnlyLists + visible: !root.showOnlyLists && miscSettingsCol.visibleChildren.length > 0 Layout.fillWidth: true Layout.preferredHeight: miscSettingsCol.implicitHeight + Style.margin2XL color: Color.mSurface @@ -336,11 +336,12 @@ Item { spacing: Style.marginM NToggle { + visible: NetworkService.wifiAvailable && BluetoothService.bluetoothAvailable label: I18n.tr("toast.airplane-mode.title") description: I18n.tr("toast.airplane-mode.description") - icon: Settings.data.network.airplaneModeEnabled ? "plane" : "plane-off" - checked: Settings.data.network.airplaneModeEnabled - onToggled: checked => BluetoothService.setAirplaneMode(checked) + icon: NetworkService.airplaneModeEnabled ? "plane" : "plane-off" + checked: NetworkService.airplaneModeEnabled + onToggled: checked => NetworkService.setAirplaneMode(checked) } } } diff --git a/Services/Control/IPCService.qml b/Services/Control/IPCService.qml index 13778f2bd..f07bb7bfe 100644 --- a/Services/Control/IPCService.qml +++ b/Services/Control/IPCService.qml @@ -705,13 +705,13 @@ Singleton { IpcHandler { target: "airplaneMode" function toggle() { - BluetoothService.setAirplaneMode(!Settings.data.network.airplaneModeEnabled); + NetworkService.setAirplaneMode(!NetworkService.airplaneModeEnabled); } function enable() { - BluetoothService.setAirplaneMode(true); + NetworkService.setAirplaneMode(true); } function disable() { - BluetoothService.setAirplaneMode(false); + NetworkService.setAirplaneMode(false); } } diff --git a/Services/Networking/BluetoothService.qml b/Services/Networking/BluetoothService.qml index 7ad9f02b1..972ffd599 100644 --- a/Services/Networking/BluetoothService.qml +++ b/Services/Networking/BluetoothService.qml @@ -12,30 +12,18 @@ import qs.Services.UI Singleton { id: root - // Constants (centralized tunables) - readonly property int ctlPollMs: 10000 - readonly property int ctlPollSoonMs: 250 - readonly property BluetoothAdapter adapter: Bluetooth.defaultAdapter - // Airplane mode status - readonly property bool airplaneModeEnabled: Settings.data.network.airplaneModeEnabled - property bool airplaneModeToggled: false - - // Power/blocked/availability state - property bool ctlAvailable: false - readonly property bool bluetoothAvailable: !!adapter || root.ctlAvailable - readonly property bool enabled: adapter?.enabled ?? root.ctlPowered - property bool ctlPowered: false - property bool ctlPowerBlocked: false - property bool ctlDiscovering: false - property bool ctlDiscoverable: false + // Power/availability state + readonly property bool bluetoothAvailable: !!adapter + readonly property bool enabled: adapter?.enabled ?? false + readonly property bool blocked: adapter?.state === BluetoothAdapter.Blocked // Exposed scanning flag for UI button state; reflects adapter discovery when available - readonly property bool scanningActive: adapter?.discovering ?? root.ctlDiscovering + readonly property bool scanningActive: adapter?.discovering ?? false // Adapter discoverability (advertising) flag - readonly property bool discoverable: adapter?.discoverable ?? root.ctlDiscoverable + readonly property bool discoverable: adapter?.discoverable ?? false readonly property var devices: adapter ? adapter.devices : null readonly property var connectedDevices: { if (!adapter || !adapter.devices) { @@ -44,9 +32,6 @@ Singleton { return adapter.devices.values.filter(dev => dev && dev.connected); } - // Current backend in use for scanning (e.g., "native", "bluetoothctl") - property string backendUsed: "" - // Experimental: best‑effort RSSI polling for connected devices (without root) // Enabled in debug mode or via user setting in Settings > Network property bool rssiPollingEnabled: Settings?.data?.network?.bluetoothRssiPollingEnabled || Settings?.isDebug || false @@ -64,6 +49,9 @@ Singleton { property int connectAttempts: 5 property int connectRetryIntervalMs: 2000 + // Interaction state + property bool pinRequired: false + // Internal variables property bool _discoveryWasRunning: false property bool _ctlInit: false @@ -83,31 +71,29 @@ Singleton { } } - function getDeviceAutoConnect(device) { - if (!device || !device.address || !cacheAdapter.autoConnectSettings) { - return false; + // Handle system wakeup to force-poll and ensure state is up-to-date + Connections { + target: Time + function onResumed() { + Logger.i("Bluetooth", "System resumed - forcing state poll"); + ctlPollTimer.restart(); } - const mac = device.address; - const settings = cacheAdapter.autoConnectSettings[mac]; - return settings ? !!settings.autoConnect : false; } - function setDeviceAutoConnect(device, enabled) { - if (!device || !device.address) { - return; + // Track adapter state changes + Connections { + target: adapter + function onStateChanged() { + if (!adapter || adapter.state === BluetoothAdapter.Enabling || adapter.state === BluetoothAdapter.Disabling) { + return; + } + checkAirplaneMode(); } - const mac = device.address; - let settings = cacheAdapter.autoConnectSettings || ({}); - if (enabled) { - settings[mac] = { - autoConnect: true, - deviceName: device.name || device.deviceName || "" - }; - } else { - delete settings[mac]; + function onEnabledChanged() { + if (adapter && adapter.enabled && Settings.data.network.bluetoothAutoConnect) { + autoConnectTimer.restart(); + } } - cacheAdapter.autoConnectSettings = settings; - cacheFileView.writeAdapter(); } Connections { @@ -121,11 +107,16 @@ Singleton { } } + Component.onCompleted: { + Logger.i("Bluetooth", "Service started"); + autoConnectTimer.restart(); + } + Timer { id: autoConnectTimer interval: 1500 repeat: false - onTriggered: root.attemptAutoConnect() + onTriggered: attemptAutoConnect() } Timer { @@ -144,303 +135,78 @@ Singleton { } } - function init() { - Logger.i("Bluetooth", "Service started"); - } - - Component.onCompleted: { - pollCtlState(); - // Ensure Airplane Mode persists upon reboot - if (root.airplaneModeEnabled) { - Quickshell.execDetached(["rfkill", "block", "wifi"]); - Quickshell.execDetached(["rfkill", "block", "bluetooth"]); - } - // Auto-connect on startup - autoConnectTimer.restart(); - } - - // Handle system wakeup to force-poll and ensure state is up-to-date - Connections { - target: Time - function onResumed() { - Logger.i("Bluetooth", "System resumed - forcing state poll"); - requestCtlPoll(); - } - } - - // Track adapter state changes - Connections { - target: adapter - function onStateChanged() { - if (!adapter || adapter.state === BluetoothAdapter.Enabling || adapter.state === BluetoothAdapter.Disabling) { - return; - } - checkAirplaneMode.running = true; - } - function onEnabledChanged() { - if (adapter && adapter.enabled && Settings.data.network.bluetoothAutoConnect) { - autoConnectTimer.restart(); - } - } - } - - onAdapterChanged: { - pollCtlState(); - if (!adapter) { - ctlPollTimer.interval = 2000; - } - } - - // Re-run polling once bluetoothctl availability is known - Connections { - target: ProgramCheckerService - function onBluetoothctlAvailableChanged() { - if (!adapter && ProgramCheckerService.bluetoothctlAvailable) { - requestCtlPoll(0); - } - } - } - - function setAirplaneMode(state) { - if (state) { - Quickshell.execDetached(["rfkill", "block", "wifi"]); - Quickshell.execDetached(["rfkill", "block", "bluetooth"]); - } else { - Quickshell.execDetached(["rfkill", "unblock", "wifi"]); - Quickshell.execDetached(["rfkill", "unblock", "bluetooth"]); - } - if (!adapter) { - root.ctlPowered = !state; - root.ctlPowerBlocked = state; - root.airplaneModeToggled = true; - NetworkService.setWifiEnabled(!state); - Settings.data.network.airplaneModeEnabled = state; - ToastService.showNotice(I18n.tr("toast.airplane-mode.title"), state ? I18n.tr("common.enabled") : I18n.tr("common.disabled"), state ? "plane" : "plane-off"); - Logger.i("AirplaneMode", state ? "Wi-Fi & Bluetooth adapter blocked" : "Wi-Fi & Bluetooth adapter unblocked"); - root.airplaneModeToggled = false; - } - } - - Process { - id: checkAirplaneMode - running: false - command: ["rfkill", "list"] - stdout: StdioCollector { - onStreamFinished: { - var output = this.text || ""; - var wifiBlocked = /^\d+:.*Wireless LAN[^\n]*\n\s*Soft blocked:\s*yes/im.test(output); - var btBlocked = /^\d+:.*Bluetooth[^\n]*\n\s*Soft blocked:\s*yes/im.test(output); - var isAirplaneModeActive = wifiBlocked && btBlocked; - - // Check if airplane mode has been toggled - if (isAirplaneModeActive && !root.airplaneModeEnabled) { - root.airplaneModeToggled = true; - NetworkService.setWifiEnabled(false); - Settings.data.network.airplaneModeEnabled = true; - ToastService.showNotice(I18n.tr("toast.airplane-mode.title"), I18n.tr("common.enabled"), "plane"); - Logger.i("AirplaneMode", "Wi-Fi & Bluetooth adapter blocked"); - } else if (!isAirplaneModeActive && root.airplaneModeEnabled) { - root.airplaneModeToggled = true; - NetworkService.setWifiEnabled(true); - Settings.data.network.airplaneModeEnabled = false; - ToastService.showNotice(I18n.tr("toast.airplane-mode.title"), I18n.tr("common.disabled"), "plane-off"); - Logger.i("AirplaneMode", "Wi-Fi & Bluetooth adapter unblocked"); - } else if (adapter ? adapter.enabled : root.ctlPowered) { - ToastService.showNotice(I18n.tr("common.bluetooth"), I18n.tr("common.enabled"), "bluetooth"); - Logger.d("Bluetooth", "Adapter enabled"); - } else { - ToastService.showNotice(I18n.tr("common.bluetooth"), I18n.tr("common.disabled"), "bluetooth-off"); - Logger.d("Bluetooth", "Adapter disabled"); - } - root.airplaneModeToggled = false; - } - } - stderr: StdioCollector { - onStreamFinished: { - if (text && text.trim()) { - Logger.w("AirplaneMode", "rfkill stderr:", text.trim()); - } - } - } - } - - // Periodic state polling - readonly property bool _lockScreenActive: PanelService.lockScreen?.active ?? false - Timer { id: ctlPollTimer - interval: adapter ? ctlPollMs : 2000 - repeat: true - running: (adapter || ProgramCheckerService.bluetoothctlAvailable) && !_lockScreenActive - onTriggered: { - pollCtlState(); - var targetInterval = adapter ? ctlPollMs : 2000; - if (interval !== targetInterval) { - interval = targetInterval; - } - } - } - - function requestCtlPoll(delayMs) { - if (!adapter && !ProgramCheckerService.bluetoothctlAvailable) { - return; - } - ctlPollTimer.interval = Math.max(50, delayMs || ctlPollSoonMs); - ctlPollTimer.restart(); - } - - function pollCtlState() { - if (!adapter || !ProgramCheckerService.bluetoothctlAvailable) { - return; - } - if (ctlShowProcess.running) { - return; - } - try { - ctlShowProcess.running = true; - } catch (_) {} - } - - // bluetoothctl state polling - Process { - id: ctlShowProcess - command: ["bluetoothctl", "show"] + interval: 250 running: false - stdout: StdioCollector { - id: ctlStdout - } - onExited: function (exitCode, exitStatus) { - try { - var text = ctlStdout.text || ""; - var lines = text.split('\n'); - var foundController = false; - var powered = false; - var powerBlocked = false; - var discoverable = false; - var discovering = false; - - for (var i = 0; i < lines.length; i++) { - var line = lines[i].trim(); - if (line.indexOf("Controller") === 0) { - foundController = true; - } - - var mp = line.match(/\bPowered:\s*(yes|no)\b/i); - if (mp) { - powered = (mp[1].toLowerCase() === "yes"); - } - - var mps = line.match(/\bPowerState:\s*([A-Za-z-]+)\b/i); - if (mps) { - powerBlocked = (mps[1].toLowerCase() === "off-blocked"); - } - - var md = line.match(/\bDiscoverable:\s*(yes|no)\b/i); - if (md) { - discoverable = (md[1].toLowerCase() === "yes"); - } - - var ms = line.match(/\bDiscovering:\s*(yes|no)\b/i); - if (ms) { - discovering = (ms[1].toLowerCase() === "yes"); - } - } - - if (!adapter && (root.ctlPowered !== powered || root.ctlPowerBlocked !== powerBlocked)) { - root.ctlPowered = powered; - root.ctlPowerBlocked = powerBlocked; - if (root._ctlInit) { - checkAirplaneMode.running = true; - } - root._ctlInit = true; - } - - root.ctlAvailable = foundController; - root.ctlPowered = powered; - root.ctlPowerBlocked = powerBlocked; - root.ctlDiscoverable = discoverable; - root.ctlDiscovering = discovering; - } catch (e) { - Logger.d("Bluetooth", "Failed to parse bluetoothctl show output", e); + onTriggered: { + if (!adapter || !ProgramCheckerService.bluetoothctlAvailable) { + return; } + ctlPollProcess.running = true; } } - // Persistent process for bluetoothctl scanning when native discovery is unavailable - Process { - id: bluetoothctlScanProcess - command: ["bluetoothctl", "scan", "on"] - onExited: Logger.d("Bluetooth", "bluetoothctl scan process exited.") - } - - // Unify discovery controls - function setScanActive(active) { - if (!adapter && !ProgramCheckerService.bluetoothctlAvailable) { - Logger.d("Bluetooth", "Scan request ignored: bluetoothctl unavailable"); - return; - } - var nativeSuccess = false; - try { - if (adapter && adapter.discovering !== undefined) { - if (active || adapter.discovering) { // Only attempt to set if activating, or if deactivating and currently currently discovering - adapter.discovering = active; - } - nativeSuccess = true; // Mark as success if adapter was handled without error - } - } catch (e) { - Logger.e("Bluetooth", "setScanActive native failed", e); - } - - if (!nativeSuccess) { - if (active) { - bluetoothctlScanProcess.running = true; - } else { - bluetoothctlScanProcess.running = false; - btExec(["bluetoothctl", "scan", "off"]); - } - } else if (bluetoothctlScanProcess.running) { - bluetoothctlScanProcess.running = false; - } - - requestCtlPoll(ctlPollSoonMs); - } - // Adapter power (enable/disable) via bluetoothctl function setBluetoothEnabled(state) { - Logger.i("Bluetooth", "SetBluetoothEnabled", state); - if (!adapter && !ProgramCheckerService.bluetoothctlAvailable) { - Logger.i("Bluetooth", "Enable/Disable skipped: no adapter or bluetoothctl"); + if (!adapter) { + Logger.d("Bluetooth", "Enable/Disable skipped: no adapter"); return; } try { - if (adapter) { - adapter.enabled = state; - } else { - root.ctlPowered = state; - btExec(["bluetoothctl", "power", state ? "on" : "off"]); - ToastService.showNotice(I18n.tr("common.bluetooth"), state ? I18n.tr("common.enabled") : I18n.tr("common.disabled"), state ? "bluetooth" : "bluetooth-off"); - Logger.d("Bluetooth", state ? "Adapter enabled" : "Adapter disabled"); - } + adapter.enabled = state; + Logger.i("Bluetooth", "SetBluetoothEnabled", state); } catch (e) { Logger.w("Bluetooth", "Enable/Disable failed", e); ToastService.showWarning(I18n.tr("common.bluetooth"), I18n.tr("toast.bluetooth.state-change-failed")); } } - // Toggle adapter discoverability (advertising visibility) via bluetoothctl - function setDiscoverable(state) { - if (!adapter && !ProgramCheckerService.bluetoothctlAvailable) { - Logger.d("Bluetooth", "Discoverable change skipped: no adapter or bluetoothctl"); + // Check if airplane mode has been toggled + function checkAirplaneMode() { + var isAirplaneModeActive = !NetworkService.wifiEnabled && adapter.state === BluetoothAdapter.Blocked + if (isAirplaneModeActive && !NetworkService.airplaneModeEnabled) { + NetworkService.airplaneModeToggled = true; + NetworkService.airplaneModeEnabled = true; + ToastService.showNotice(I18n.tr("toast.airplane-mode.title"), I18n.tr("common.enabled"), "plane"); + Logger.i("AirplaneMode", "Enabled"); + } else if (!isAirplaneModeActive && NetworkService.airplaneModeEnabled) { + NetworkService.airplaneModeToggled = true; + NetworkService.airplaneModeEnabled = false; + ToastService.showNotice(I18n.tr("toast.airplane-mode.title"), I18n.tr("common.disabled"), "plane-off"); + Logger.i("AirplaneMode", "Disabled"); + } else if (adapter.enabled) { + ToastService.showNotice(I18n.tr("common.bluetooth"), I18n.tr("common.enabled"), "bluetooth"); + Logger.d("Bluetooth", "Adapter enabled"); + } else { + ToastService.showNotice(I18n.tr("common.bluetooth"), I18n.tr("common.disabled"), "bluetooth-off"); + Logger.d("Bluetooth", "Adapter disabled"); + } + } + + // Unify discovery controls + function setScanActive(active) { + if (!adapter) { + Logger.d("Bluetooth", "Scan request ignored: adapter unavailable"); return; } try { - if (adapter) { - adapter.discoverable = state; - } else { - btExec(["bluetoothctl", "discoverable", state ? "on" : "off"]); - root.ctlDiscoverable = state; // optimistic - requestCtlPoll(ctlPollSoonMs); + if (active || adapter.discovering) { // Only attempt to set if activating, or if deactivating and currently currently discovering + adapter.discovering = active; } + } catch (e) { + Logger.e("Bluetooth", "setScanActive failed", e); + } + } + + // Toggle adapter discoverability (advertising visibility) via bluetoothctl + function setDiscoverable(state) { + if (!adapter) { + Logger.d("Bluetooth", "Discoverable change skipped: no adapter"); + return; + } + try { + adapter.discoverable = state; Logger.i("Bluetooth", "Discoverable state set to:", state); } catch (e) { Logger.w("Bluetooth", "Failed to change discoverable state", e); @@ -566,9 +332,6 @@ Singleton { } } - // Interaction state - property bool pinRequired: false - function submitPin(pin) { if (pairingProcess.running) { pairingProcess.write(pin + "\n"); @@ -583,33 +346,6 @@ Singleton { root.pinRequired = false; } - // Interactive pairing process - Process { - id: pairingProcess - stdout: SplitParser { - onRead: data => { - Logger.d("Bluetooth", data); - if (data.indexOf("PIN_REQUIRED") !== -1) { - root.pinRequired = true; - Logger.i("Bluetooth", "PIN required for pairing"); - } - } - } - onExited: { - root.pinRequired = false; - Logger.i("Bluetooth", "Pairing process exited."); - // Restore discovery if we paused it - if (root._discoveryWasRunning) { - root.setScanActive(true); - } - root._discoveryWasRunning = false; - root.requestCtlPoll(); - } - environment: ({ - "LC_ALL": "C" - }) - } - // Pair using bluetoothctl which registers its own BlueZ agent internally. function pairWithBluetoothctl(device) { if (!device) { @@ -675,8 +411,35 @@ Singleton { forgetDevice(device); } + function getDeviceAutoConnect(device) { + if (!device || !device.address || !cacheAdapter.autoConnectSettings) { + return false; + } + const mac = device.address; + const settings = cacheAdapter.autoConnectSettings[mac]; + return settings ? !!settings.autoConnect : false; + } + + function setDeviceAutoConnect(device, enabled) { + if (!device || !device.address) { + return; + } + const mac = device.address; + let settings = cacheAdapter.autoConnectSettings || ({}); + if (enabled) { + settings[mac] = { + autoConnect: true, + deviceName: device.name || device.deviceName || "" + }; + } else { + delete settings[mac]; + } + cacheAdapter.autoConnectSettings = settings; + cacheFileView.writeAdapter(); + } + function attemptAutoConnect() { - if (airplaneModeEnabled || !adapter || !adapter.enabled || !Settings.data.network.bluetoothAutoConnect) { + if (NetworkService.airplaneModeEnabled || !adapter || !adapter.enabled || !Settings.data.network.bluetoothAutoConnect) { return; } @@ -724,4 +487,56 @@ Singleton { ToastService.showWarning(I18n.tr("common.bluetooth"), I18n.tr("toast.bluetooth.forget-failed")); } } + + // Poll Bluetooth power state with bluetoothctl to handle a Quickshell bug on resume after suspend + Process { + id: ctlPollProcess + command: ["bluetoothctl", "show"] + running: false + stdout: StdioCollector { + onStreamFinished: { + var powered = false; + var mp = text.match(/\bPowered:\s*(yes|no)\b/i); + if (mp) { + powered = mp[1].toLowerCase() === 'yes'; + } + if (adapter.enabled !== powered) { + adapter.enabled = powered; + } + } + } + stderr: StdioCollector { + onStreamFinished: { + if (text.trim()) { + Logger.d("Bluetooth", "Failed to parse bluetoothctl show output" + text); + } + } + } + } + + // Interactive pairing process + Process { + id: pairingProcess + stdout: SplitParser { + onRead: data => { + Logger.d("Bluetooth", data); + if (data.indexOf("PIN_REQUIRED") !== -1) { + root.pinRequired = true; + Logger.i("Bluetooth", "PIN required for pairing"); + } + } + } + onExited: { + root.pinRequired = false; + Logger.i("Bluetooth", "Pairing process exited."); + // Restore discovery if we paused it + if (root._discoveryWasRunning) { + root.setScanActive(true); + } + root._discoveryWasRunning = false; + } + environment: ({ + "LC_ALL": "C" + }) + } } diff --git a/Services/Networking/NetworkService.qml b/Services/Networking/NetworkService.qml index 43ec1216d..0a71f3b9b 100644 --- a/Services/Networking/NetworkService.qml +++ b/Services/Networking/NetworkService.qml @@ -59,6 +59,7 @@ Singleton { property bool _internetConnectivity: false property string lastError: "" property int activeDetailsTtlMs: 10000 + // Ethernet properties property var ethernetInterfaces: ([]) property var activeEthernetDetails: ({}) @@ -66,6 +67,7 @@ Singleton { property string activeEthernetIf: "" property bool ethernetDetailsLoading: false property double activeEthernetDetailsTimestamp: 0 + // Wi-Fi properties readonly property bool wifiEnabled: Networking.wifiEnabled property var networks: ({}) @@ -75,6 +77,7 @@ Singleton { property bool wifiDetailsLoading: false property double activeWifiDetailsTimestamp: 0 property bool wifiInit: false + // Wi-Fi adapter/connection properties property bool connecting: false property string connectingTo: "" @@ -84,26 +87,20 @@ Singleton { property bool scanningActive: false property var existingProfiles: ({}) + // Airplane mode status + property bool airplaneModeEnabled: false + property bool airplaneModeToggled: false + Connections { target: root function onWifiEnabledChanged() { - if (!root.wifiInit || BluetoothService.airplaneModeToggled) { + if (!root.wifiInit) { return; } wifiDebounce.restart(); } } - // Handle system resume to refresh state and connectivity - Connections { - target: Time - function onResumed() { - Logger.i("Network", "System resumed - forcing state poll"); - deviceStatusProcess.running = true; - connectivityCheckProcess.running = true; - } - } - // Start initial checks when nmcli becomes available Connections { target: ProgramCheckerService @@ -129,11 +126,14 @@ Singleton { // Prevent an initial "Wi-Fi enabled" toast and trigger initial scan Timer { id: wifiInitTimer - interval: 100 + interval: 500 onTriggered: { root.wifiInit = true; if (root.wifiEnabled) { - root.scan(); + scan(); + } + if (!root.wifiEnabled && BluetoothService.blocked) { + root.airplaneModeEnabled = true; } } } @@ -143,14 +143,40 @@ Singleton { id: wifiDebounce interval: 300 onTriggered: { + if (!ProgramCheckerService.nmcliAvailable) { + return; + } + if (root.airplaneModeToggled) { + root.airplaneModeToggled = false; + if (root.wifiEnabled) { + scan(); + } else { + root.networks = ({}); + } + return; + } + var isAirplaneModeActive = !root.wifiEnabled && BluetoothService.blocked + // Extra check for Airplane Mode if Bluetooth has been blocked before Wi-Fi + if (isAirplaneModeActive && !root.airplaneModeEnabled) { + root.airplaneModeEnabled = true; + ToastService.showNotice(I18n.tr("toast.airplane-mode.title"), I18n.tr("common.enabled"), "plane"); + Logger.i("AirplaneMode", "Enabled"); + root.networks = ({}); + return; + } + // Extra check for Airplane Mode if Wi-Fi has been unblocked before Bluetooth + if (!isAirplaneModeActive && root.airplaneModeEnabled) { + root.airplaneModeEnabled = false; + ToastService.showNotice(I18n.tr("toast.airplane-mode.title"), I18n.tr("common.disabled"), "plane-off"); + Logger.i("AirplaneMode", "Disabled"); + scan(); + return; + } if (root.wifiEnabled) { ToastService.showNotice(I18n.tr("common.wifi"), I18n.tr("common.enabled"), "wifi"); - connectivityCheckProcess.running = true; - deviceStatusProcess.running = true; - root.scan(); + scan(); } else { ToastService.showNotice(I18n.tr("common.wifi"), I18n.tr("common.disabled"), "wifi-off"); - root.scanningActive = false; root.networks = ({}); } } @@ -172,30 +198,6 @@ Singleton { onTriggered: scan() } - // Refresh details for the currently active Wi‑Fi link - function refreshActiveWifiDetails() { - const now = Date.now(); - if (wifiDetailsLoading || (activeWifiIf && wifiConnected && activeWifiDetails && (now - activeWifiDetailsTimestamp) < activeDetailsTtlMs)) { - return; - } - if (wifiConnected && activeWifiIf) { - wifiDetailsLoading = true; - deviceStatusProcess.running = true; - } - } - - // Refresh details for the currently active Ethernet link - function refreshActiveEthernetDetails() { - const now = Date.now(); - if (ethernetDetailsLoading || activeEthernetIf && activeEthernetDetails && (now - activeEthernetDetailsTimestamp) < activeDetailsTtlMs) { - return; - } - if (ethernetConnected && activeEthernetIf) { - ethernetDetailsLoading = true; - deviceStatusProcess.running = true; - } - } - // Core functions function setWifiEnabled(enabled) { if (!ProgramCheckerService.nmcliAvailable) { @@ -205,6 +207,14 @@ Singleton { Networking.wifiEnabled = enabled; } + function setAirplaneMode(state) { + if (state) { + Quickshell.execDetached(["rfkill", "block", "all"]); + } else { + Quickshell.execDetached(["rfkill", "unblock", "all"]); + } + } + function scan() { if (!ProgramCheckerService.nmcliAvailable || !root.wifiEnabled) { return; @@ -276,6 +286,30 @@ Singleton { forgetProcess.running = true; } + // Refresh details for the currently active Wi‑Fi link + function refreshActiveWifiDetails() { + const now = Date.now(); + if (wifiDetailsLoading || (activeWifiIf && wifiConnected && activeWifiDetails && (now - activeWifiDetailsTimestamp) < activeDetailsTtlMs)) { + return; + } + if (wifiConnected && activeWifiIf) { + wifiDetailsLoading = true; + deviceStatusProcess.running = true; + } + } + + // Refresh details for the currently active Ethernet link + function refreshActiveEthernetDetails() { + const now = Date.now(); + if (ethernetDetailsLoading || activeEthernetIf && activeEthernetDetails && (now - activeEthernetDetailsTimestamp) < activeDetailsTtlMs) { + return; + } + if (ethernetConnected && activeEthernetIf) { + ethernetDetailsLoading = true; + deviceStatusProcess.running = true; + } + } + // Helper function to immediately update network status function updateNetworkStatus(ssid, connected) { let nets = networks; @@ -411,7 +445,7 @@ Singleton { return root.connectingTo ? I18n.tr("common.connecting") + " " + root.connectingTo : I18n.tr("common.connecting"); } - if (Settings.data.network.airplaneModeEnabled) { + if (NetworkService.airplaneModeEnabled) { return I18n.tr("toast.airplane-mode.title"); } if (!root.wifiEnabled) { @@ -438,7 +472,7 @@ Singleton { } function getIcon(forceEthernet = false) { - if (Settings.data.network.airplaneModeEnabled && !forceEthernet) { + if (NetworkService.airplaneModeEnabled && !forceEthernet) { return "plane"; } @@ -644,7 +678,7 @@ Singleton { } let enhancedBand = band; - if (channel && width) { + if (channel && width && width !== "0 MHz") { enhancedBand = `${band} / ${channel} (${width})`; } else if (channel) { enhancedBand = `${band} / ${channel}`; @@ -1119,6 +1153,7 @@ Singleton { if (data.endsWith(": connected") || data.endsWith(": disconnected")) { Logger.d("Network", "State changed: " + data); deviceStatusProcess.running = true; + connectivityCheckProcess.running = true; } } } diff --git a/shell.qml b/shell.qml index 6fc7bd80b..9a9b88695 100644 --- a/shell.qml +++ b/shell.qml @@ -107,7 +107,6 @@ ShellRoot { Qt.callLater(function () { LocationService.init(); NightLightService.apply(); - BluetoothService.init(); IdleInhibitorService.init(); IdleService.init(); PowerProfileService.init();