Stop Bluetooth discovery when the panel closes (#6794)
The discovery retry timer turned adapter.discovering on every second while the panel was open, and nothing ever turned it off. The BlueZ discovery session behind it is held by quickshell's D-Bus connection, so one visit to the panel left the radio in inquiry until the next shell restart — continuously starving A2DP audio on the same controller into stuttering, and 'bluetoothctl show' kept reporting 'Discovering: yes' long after the panel was gone. The panel now tracks the StopDiscovery it owes BlueZ and settles it once closed. A timer bound to the confirmed discovery state does the stopping, rather than a write in the close handler: quickshell only forwards a discovering write that differs from the last state BlueZ reported, so a stop issued while a just-fired StartDiscovery is still awaiting confirmation would be swallowed and leak the session. Binding to adapter.discovering re-arms the stop whenever the confirmation lands, a reopen inside the first interval keeps the scan running uninterrupted, and attempts are bounded so a session another BlueZ client holds up cannot draw StopDiscovery calls forever. One widget instance exists per monitor and they all share the default adapter — the same shared-backend shape the network panel's wifi scanner fix (#6772) dealt with — so the debt follows the session: an instance opening onto a running scan adopts it, a closing instance hands it to a panel still open on another monitor (the popout handoff closes one instance as it opens the next), and a destroyed instance passes it to a surviving sibling. Fixes #6789 Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
f075a789f4
commit
457aec6a8c
@@ -22,6 +22,13 @@ Panel {
|
||||
property var pendingActions: ({})
|
||||
|
||||
readonly property var adapter: Bluetooth.defaultAdapter
|
||||
|
||||
// True while this instance owes BlueZ a StopDiscovery: set when it starts
|
||||
// discovery (or opens onto a session already running) and cleared once
|
||||
// discovery is confirmed down after close. Ownership, not state — BlueZ's
|
||||
// Discovering property also reflects sessions other clients hold, which are
|
||||
// never this panel's to stop.
|
||||
property bool owesDiscoveryStop: false
|
||||
readonly property var devices: Bluetooth.devices ? Bluetooth.devices.values : []
|
||||
readonly property var pipewireNodes: Pipewire.nodes ? Pipewire.nodes.values : []
|
||||
property var pendingAudioOutputDevice: null
|
||||
@@ -400,6 +407,10 @@ Panel {
|
||||
|
||||
onOpenedChanged: {
|
||||
if (opened) {
|
||||
// Adopt a discovery session that is already running — a popout handoff
|
||||
// from another monitor, or one leaked by an instance that could not
|
||||
// finish its own stop — so this close settles it either way.
|
||||
if (adapter !== null && adapter.discovering) owesDiscoveryStop = true
|
||||
if (connectedDevices.length > 0) { focusSection = "connected"; selectedIndex = 0 }
|
||||
else if (knownDevices.length > 0) { focusSection = "known"; selectedIndex = 0 }
|
||||
else if (discoveredDevices.length > 0) { focusSection = "discovered"; selectedIndex = 0 }
|
||||
@@ -409,6 +420,19 @@ Panel {
|
||||
}
|
||||
}
|
||||
|
||||
// Another per-monitor instance of this widget whose panel is open, if any.
|
||||
// All instances share the default adapter, and switching the popout to a
|
||||
// different monitor closes one instance as it opens the next, so the
|
||||
// closing side has to leave the scan alone for the side still on screen.
|
||||
function openSibling() {
|
||||
if (!bar || typeof bar.moduleWidgets !== "function") return null
|
||||
var items = bar.moduleWidgets(moduleName)
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
if (items[i] && items[i] !== root && items[i].opened === true) return items[i]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function updateFocusedAddress() {
|
||||
var d = deviceAt(focusSection, selectedIndex)
|
||||
focusedDeviceAddress = d ? (d.address || "") : ""
|
||||
@@ -486,7 +510,71 @@ Panel {
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
running: root.opened && root.adapter !== null && root.adapter.enabled && !root.adapter.discovering
|
||||
onTriggered: root.adapter.discovering = true
|
||||
onTriggered: {
|
||||
root.owesDiscoveryStop = true
|
||||
root.adapter.discovering = true
|
||||
}
|
||||
}
|
||||
|
||||
// The way back down. The BlueZ discovery session behind adapter.discovering
|
||||
// is held by quickshell's D-Bus connection, so nothing ends it at close:
|
||||
// without this timer, one visit to the panel left the radio in inquiry
|
||||
// until the next shell restart, starving A2DP audio on the same controller
|
||||
// into stutters.
|
||||
//
|
||||
// A timer bound to the confirmed state rather than a write at close time:
|
||||
// quickshell only forwards a discovering write that differs from the last
|
||||
// state BlueZ reported, so a stop issued while a just-fired StartDiscovery
|
||||
// is still awaiting confirmation would be swallowed and leak the session.
|
||||
// Binding to adapter.discovering means a confirmation landing at any point
|
||||
// after close re-arms the stop, and a reopen inside the first interval
|
||||
// keeps the scan running uninterrupted. Attempts are bounded so a session
|
||||
// some other BlueZ client keeps up cannot draw StopDiscovery fire forever.
|
||||
Timer {
|
||||
id: discoveryStop
|
||||
interval: 1000
|
||||
repeat: true
|
||||
property int attempts: 0
|
||||
running: !root.opened && root.owesDiscoveryStop && root.adapter !== null && root.adapter.discovering === true
|
||||
onRunningChanged: if (running) attempts = 0
|
||||
onTriggered: {
|
||||
// The scan now serves the open panel, so the debt moves with it — B may
|
||||
// have opened before BlueZ confirmed A's start, in which case B's own
|
||||
// open-time adoption saw nothing to adopt.
|
||||
var sibling = root.openSibling()
|
||||
if (sibling) {
|
||||
sibling.owesDiscoveryStop = true
|
||||
root.owesDiscoveryStop = false
|
||||
return
|
||||
}
|
||||
attempts += 1
|
||||
if (attempts > 3) { root.owesDiscoveryStop = false; return }
|
||||
root.adapter.discovering = false
|
||||
}
|
||||
}
|
||||
|
||||
// The debt is settled the moment BlueZ reports discovery down — whether
|
||||
// because the stop above landed or the session ended some other way — so a
|
||||
// stale claim never touches a scan another client starts later. While the
|
||||
// panel is open, discoveryRetry re-incurs it as it restarts the scan.
|
||||
Connections {
|
||||
target: root.adapter
|
||||
function onDiscoveringChanged() {
|
||||
if (!root.adapter.discovering) root.owesDiscoveryStop = false
|
||||
}
|
||||
}
|
||||
|
||||
// A destroyed instance cannot wait for BlueZ confirmations, so it hands any
|
||||
// debt to a surviving sibling — whose declarative stop catches even a start
|
||||
// confirmed after this object is gone — and only writes the stop directly
|
||||
// when it is the last one standing.
|
||||
Component.onDestruction: {
|
||||
if (!owesDiscoveryStop) return
|
||||
var items = bar && typeof bar.moduleWidgets === "function" ? bar.moduleWidgets(moduleName) : []
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
if (items[i] && items[i] !== root) { items[i].owesDiscoveryStop = true; return }
|
||||
}
|
||||
if (adapter !== null && adapter.discovering) adapter.discovering = false
|
||||
}
|
||||
|
||||
Timer {
|
||||
|
||||
Reference in New Issue
Block a user