mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
Merge pull request #2312 from notiant/patch-2
Rework Airplane Mode handling & remove bluetoothctl fallback
This commit is contained in:
@@ -381,7 +381,6 @@
|
||||
"indicatorOpacity": 0.6
|
||||
},
|
||||
"network": {
|
||||
"airplaneModeEnabled": false,
|
||||
"bluetoothRssiPollingEnabled": false,
|
||||
"bluetoothRssiPollIntervalMs": 60000,
|
||||
"networkPanelView": "wifi",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ NIconButtonHot {
|
||||
p.toggle(this);
|
||||
}
|
||||
onRightClicked: {
|
||||
if (!Settings.data.network.airplaneModeEnabled) {
|
||||
if (!NetworkService.airplaneModeEnabled) {
|
||||
BluetoothService.setBluetoothEnabled(!BluetoothService.enabled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ NIconButtonHot {
|
||||
panel?.toggle(this);
|
||||
}
|
||||
onRightClicked: {
|
||||
if (!Settings.data.network.airplaneModeEnabled) {
|
||||
if (!NetworkService.airplaneModeEnabled) {
|
||||
NetworkService.setWifiEnabled(!NetworkService.wifiEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user