autoswitch-bluetooth-profile.lua: Refactor and fix issues with saved profiles

This patch improves the BT profile autoswitch logic so that it is simpler and
more robust. Instead of just relying on capture clients that are linked to (or
unlinked from) the BT loopback source node to evaluate whether we have to switch
to a headset profile or not, we now also evaluate the autoswitch every time the
BT loopback source node state changes. This avoids problems with some capture
clients that pause the input stream without closing them.

Apart from this, the patch also fixes some issues with saved profiles if the
user manually switched to a headset profile when no capture streams are present.
The autoswitch logic should restore back the non-headset profile (A2DP) every
time a capture client is disconnected or the BT loopback source node stops
running.

Finally, the 'bluetooth.autoswitch-to-headset-profile' setting will now register
or remove the necessary hooks depending on whether the setting is enabled or
disabled, improving WirePlumber performance if autoswitching is disabled.
This commit is contained in:
Julian Bouzas 2025-09-10 16:51:46 -04:00
parent f196d10e87
commit e94caea3a4

View file

@ -28,43 +28,25 @@
lutils = require ("linking-utils")
cutils = require ("common-utils")
log = Log.open_topic ("s-device")
persistent_storage_hooks_registered = false
autoswitch_hooks_registered = false
state = nil
headset_profiles = nil
local PROFILE_RESTORE_TIMEOUT_MSEC = 2000
local PROFILE_SWITCH_TIMEOUT_MSEC = 500
local profile_restore_timeout_msec = 2000
local profile_switch_timeout_msec = 500
local INVALID = -1
local state = nil
local headset_profiles = {}
local non_headset_profiles = {}
local capture_stream_links = {}
local restore_timeout_source = {}
local switch_timeout_source = {}
local last_profiles = {}
local active_streams = {}
local previous_streams = {}
function handlePersistentSetting (enable)
if enable and state == nil then
-- the state storage
state = Settings.get_boolean ("bluetooth.autoswitch-to-headset-profile")
and State ("bluetooth-autoswitch") or nil
headset_profiles = state and state:load () or Properties()
else
state = nil
headset_profiles = nil
end
end
handlePersistentSetting (Settings.get_boolean ("bluetooth.use-persistent-storage"))
Settings.subscribe ("bluetooth.use-persistent-storage", function ()
handlePersistentSetting (Settings.get_boolean ("bluetooth.use-persistent-storage"))
end)
function saveHeadsetProfile (device, profile_name)
function saveHeadsetProfile (device, profile_name, persistent)
local key = "saved-headset-profile:" .. device.properties ["device.name"]
headset_profiles [key] = profile_name
state:save_after_timeout (headset_profiles)
if state ~= nil and persistent then
state:save_after_timeout (headset_profiles)
end
end
function getSavedHeadsetProfile (device)
@ -72,85 +54,72 @@ function getSavedHeadsetProfile (device)
return headset_profiles [key]
end
function saveLastProfile (device, profile_name)
last_profiles [device.properties ["device.name"]] = profile_name
function saveNonHeadsetProfile (device, profile_name)
non_headset_profiles [device.properties ["device.name"]] = profile_name
end
function getSavedLastProfile (device)
return last_profiles [device.properties ["device.name"]]
end
function isSwitchedToHeadsetProfile (device)
return getSavedLastProfile (device) ~= nil
function getSavedNonHeadsetProfile (device)
return non_headset_profiles [device.properties ["device.name"]]
end
function findProfile (device, index, name)
for p in device:iterate_params ("EnumProfile") do
local profile = cutils.parseParam (p, "EnumProfile")
if not profile then
goto skip_enum_profile
end
log:debug ("Profile name: " .. profile.name .. ", priority: "
.. tostring (profile.priority) .. ", index: " .. tostring (profile.index))
if (index ~= nil and profile.index == index) or
(name ~= nil and profile.name == name) then
return profile.priority, profile.index, profile.name
end
::skip_enum_profile::
end
return INVALID, INVALID, nil
end
function getCurrentProfile (device)
for p in device:iterate_params ("Profile") do
local profile = cutils.parseParam (p, "Profile")
if profile then
return profile.name
if profile ~= nil then
if (index ~= nil and profile.index == index) or
(name ~= nil and profile.name == name) then
return profile
end
end
end
return nil
end
function highestPrioProfileWithInputRoute (device)
local profile_priority = INVALID
local profile_index = INVALID
local profile_name = nil
function getCurrentProfile (device)
for p in device:iterate_params ("Profile") do
local profile = cutils.parseParam (p, "Profile")
if profile then
return profile
end
end
return nil
end
function highestPrioProfileWithInputRoute (device)
local found_profile = nil
for p in device:iterate_params ("EnumRoute") do
local route = cutils.parseParam (p, "EnumRoute")
-- Parse pod
if not route then
goto skip_enum_route
end
if route.direction ~= "Input" then
goto skip_enum_route
end
log:debug ("Route with index: " .. tostring (route.index) .. ", direction: "
.. route.direction .. ", name: " .. route.name .. ", description: "
.. route.description .. ", priority: " .. route.priority)
if route.profiles then
if route ~= nil and route.profiles ~= nil and route.direction == "Input" then
for _, v in pairs (route.profiles) do
local priority, index, name = findProfile (device, v)
if priority ~= INVALID then
if profile_priority < priority then
profile_priority = priority
profile_index = index
profile_name = name
local p = findProfile (device, v)
if p ~= nil then
if found_profile == nil or found_profile.priority < p.priority then
found_profile = p
end
end
end
end
::skip_enum_route::
end
return found_profile
end
return profile_priority, profile_index, profile_name
function highestPrioProfileWithoutInputRoute (device)
local found_profile = nil
for p in device:iterate_params ("EnumRoute") do
local route = cutils.parseParam (p, "EnumRoute")
if route ~= nil and route.profiles ~= nil and route.direction ~= "Input" then
for _, v in pairs (route.profiles) do
local p = findProfile (device, v)
if p ~= nil then
if found_profile == nil or found_profile.priority < p.priority then
found_profile = p
end
end
end
end
end
return found_profile
end
function hasProfileInputRoute (device, profile_index)
@ -177,49 +146,40 @@ function switchDeviceToHeadsetProfile (dev_id, device_om)
return
end
local cur_profile_name = getCurrentProfile (device)
local priority, index, name = findProfile (device, nil, cur_profile_name)
if hasProfileInputRoute (device, index) then
log:info ("Current profile has input route, not switching")
-- Do not switch if the current profile is already a headset profile
local cur_profile = getCurrentProfile (device)
if cur_profile ~= nil and
hasProfileInputRoute (device, cur_profile.index) then
log:info (device,
"Current profile is already a headset profile, no need to switch")
return
end
if isSwitchedToHeadsetProfile (device) then
log:info ("Device with id " .. tostring(dev_id).. " is already switched to HSP/HFP")
return
end
local saved_headset_profile = getSavedHeadsetProfile (device)
index = INVALID
if saved_headset_profile then
priority, index, name = findProfile (device, nil, saved_headset_profile)
if index ~= INVALID and not hasProfileInputRoute (device, index) then
index = INVALID
saveHeadsetProfile (device, nil)
-- Get saved headset profile if any, otherwise find the highest priority one
local profile = nil
local profile_name = getSavedHeadsetProfile (device)
if profile_name ~= nil then
profile = findProfile (device, nil, profile_name)
if profile ~= nil and not hasProfileInputRoute (device, profile.index) then
saveHeadsetProfile (device, nil, false)
end
end
if index == INVALID then
priority, index, name = highestPrioProfileWithInputRoute (device)
if profile == nil then
profile = highestPrioProfileWithInputRoute (device)
end
if index ~= INVALID then
-- Switch if headset profile was found
if profile ~= nil then
local pod = Pod.Object {
"Spa:Pod:Object:Param:Profile", "Profile",
index = index
index = profile.index,
save = false
}
-- store the current profile (needed when restoring)
saveLastProfile (device, cur_profile_name)
-- switch to headset profile
log:info ("Setting profile of '"
.. device.properties ["device.description"]
.. "' from: " .. cur_profile_name
.. " to: " .. name)
log:info (device, "Switching profile from: " .. cur_profile.name
.. " to: " .. profile.name)
device:set_params ("Profile", pod)
else
log:warning ("Got invalid index when switching profile")
log:warning ("Could not find valid headset profile, not switching")
end
end
@ -233,45 +193,40 @@ function restoreProfile (dev_id, device_om)
return
end
if not isSwitchedToHeadsetProfile (device) then
log:info ("Device with id " .. tostring(dev_id).. " is already not switched to HSP/HFP")
-- Do not restore if the current profile is already a non-headset profile
local cur_profile = getCurrentProfile (device)
if cur_profile ~= nil and
not hasProfileInputRoute (device, cur_profile.index) then
log:info (device,
"Current profile is already a non-headset profile, no need to restore")
return
end
local profile_name = getSavedLastProfile (device)
local cur_profile_name = getCurrentProfile (device)
local priority, index, name
if cur_profile_name then
priority, index, name = findProfile (device, nil, cur_profile_name)
if index ~= INVALID and hasProfileInputRoute (device, index) then
log:info ("Setting saved headset profile to: " .. cur_profile_name)
saveHeadsetProfile (device, cur_profile_name)
-- Get saved non-headset profile if any, otherwise find the highest priority one
local profile = nil
local profile_name = getSavedNonHeadsetProfile (device)
if profile_name ~= nil then
profile = findProfile (device, nil, profile_name)
if profile ~= nil and hasProfileInputRoute (device, profile.index) then
saveNonHeadsetProfile (device, nil)
end
end
if profile == nil then
profile = highestPrioProfileWithoutInputRoute (device)
end
if profile_name then
priority, index, name = findProfile (device, nil, profile_name)
if index ~= INVALID then
local pod = Pod.Object {
"Spa:Pod:Object:Param:Profile", "Profile",
index = index
}
-- clear last profile as we will restore it now
saveLastProfile (device, nil)
-- restore previous profile
log:info ("Restoring profile of '"
.. device.properties ["device.description"]
.. "' from: " .. cur_profile_name
.. " to: " .. name)
device:set_params ("Profile", pod)
else
log:warning ("Failed to restore profile")
end
-- Restore if non-headset profile was found
if profile ~= nil then
local pod = Pod.Object {
"Spa:Pod:Object:Param:Profile", "Profile",
index = profile.index,
save = false
}
log:info (device, "Restoring profile from: " .. cur_profile.name
.. " to: " .. profile.name)
device:set_params ("Profile", pod)
else
log:warning ("Could not find valid non-headset profile, not switching")
end
end
@ -280,95 +235,89 @@ function triggerSwitchDeviceToHeadsetProfile (dev_id, device_om)
if restore_timeout_source[dev_id] ~= nil then
restore_timeout_source[dev_id]:destroy ()
restore_timeout_source[dev_id] = nil
log:info ("Cancelled profile restore on device " .. tostring (dev_id))
end
if switch_timeout_source[dev_id] ~= nil then
switch_timeout_source[dev_id]:destroy ()
switch_timeout_source[dev_id] = nil
log:info ("Cancelled profile switch on device " .. tostring (dev_id))
end
-- create new switch callback
switch_timeout_source[dev_id] = Core.timeout_add (profile_switch_timeout_msec, function ()
log:info ("Triggering profile switch on device " .. tostring (dev_id))
switch_timeout_source[dev_id] = Core.timeout_add (PROFILE_SWITCH_TIMEOUT_MSEC, function ()
switch_timeout_source[dev_id] = nil
switchDeviceToHeadsetProfile (dev_id, device_om)
end)
end
function triggerRestoreProfile (dev_id, device_om)
-- we never restore the device profiles if there are active streams
for _, v in pairs (active_streams) do
if v == dev_id then
return
end
end
-- Always clear any pending restore/switch callbacks when triggering a new restore
if switch_timeout_source[dev_id] ~= nil then
switch_timeout_source[dev_id]:destroy ()
switch_timeout_source[dev_id] = nil
log:info ("Cancelled profile switch on device " .. tostring (dev_id))
end
if restore_timeout_source[dev_id] ~= nil then
restore_timeout_source[dev_id]:destroy ()
restore_timeout_source[dev_id] = nil
log:info ("Cancelled profile restore on device " .. tostring (dev_id))
end
-- create new restore callback
restore_timeout_source[dev_id] = Core.timeout_add (profile_restore_timeout_msec, function ()
log:info ("Triggering profile restore on device " .. tostring (dev_id))
restore_timeout_source[dev_id] = Core.timeout_add (PROFILE_RESTORE_TIMEOUT_MSEC, function ()
restore_timeout_source[dev_id] = nil
restoreProfile (dev_id, device_om)
end)
end
-- We consider a Stream of interest if it is linked to a bluetooth loopback
-- source filter
function checkStreamStatus (stream, node_om, visited_link_groups)
-- check if the stream is linked to a bluetooth loopback source
local stream_id = tonumber(stream["bound-id"])
local peer_id = lutils.getNodePeerId (stream_id)
if peer_id ~= nil then
local bt_node = node_om:lookup {
Constraint { "bound-id", "=", peer_id, type = "gobject" },
Constraint { "bluez5.loopback", "=", "true", type = "pw" }
}
if bt_node ~= nil then
local dev_id = bt_node.properties["device.id"]
if dev_id ~= nil then
-- If a stream we previously saw stops running, we consider it
-- inactive, because some applications (Teams) just cork input
-- streams, but don't close them.
if previous_streams [stream.id] == dev_id and
stream.state ~= "running" then
return nil
end
function getLinkedBluetoothLoopbackSourceNodeForStream (stream, node_om, link_om, visited_link_groups)
local stream_id = stream["bound-id"]
return dev_id
-- Make sure the node is linked
local link = link_om:lookup {
Constraint { "link.input.node", "=", stream_id, type = "pw-global"}
}
if link == nil then
return nil
end
local peer_id = link.properties["link.output.node"]
-- If the peer node is the BT loopback source node, return its Id.
-- Otherwise recursively advance in the graph if it is linked to a filter.
local bt_node = node_om:lookup {
Constraint { "media.class", "matches", "Audio/Source", type = "pw-global" },
Constraint { "bound-id", "=", peer_id, type = "gobject" },
Constraint { "bluez5.loopback", "=", "true", type = "pw" }
}
if bt_node ~= nil then
return bt_node
else
local filter_main_node = node_om:lookup {
Constraint { "bound-id", "=", peer_id, type = "gobject" },
Constraint { "node.link-group", "+", type = "pw" }
}
if filter_main_node ~= nil then
local filter_link_group = filter_main_node.properties ["node.link-group"]
if visited_link_groups == nil then
visited_link_groups = {}
end
else
-- Check if it is linked to a filter main node, and recursively advance if so
local filter_main_node = node_om:lookup {
Constraint { "bound-id", "=", peer_id, type = "gobject" },
Constraint { "node.link-group", "+", type = "pw" }
}
if filter_main_node ~= nil then
-- Now check all stream nodes for this filter
local filter_link_group = filter_main_node.properties ["node.link-group"]
if visited_link_groups == nil then
visited_link_groups = {}
end
if visited_link_groups [filter_link_group] then
return nil
else
visited_link_groups [filter_link_group] = true
end
for filter_stream_node in node_om:iterate {
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
Constraint { "stream.monitor", "!", "true", type = "pw" },
Constraint { "bluez5.loopback", "!", "true", type = "pw" },
Constraint { "node.link-group", "=", filter_link_group, type = "pw" }
} do
local dev_id = checkStreamStatus (filter_stream_node, node_om, visited_link_groups)
if dev_id ~= nil then
return dev_id
end
if visited_link_groups [filter_link_group] then
return nil
else
visited_link_groups [filter_link_group] = true
end
for filter_stream_node in node_om:iterate {
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
Constraint { "stream.monitor", "!", "true", type = "pw" },
Constraint { "bluez5.loopback", "!", "true", type = "pw" },
Constraint { "node.link-group", "=", filter_link_group, type = "pw" }
} do
local filter_stream_id = filter_stream_node["bound-id"]
local bt_node = getLinkedBluetoothLoopbackSourceNodeForStream (filter_stream_id, node_om, link_om, visited_link_groups)
if bt_node ~= nil then
return bt_node
end
end
end
@ -377,60 +326,65 @@ function checkStreamStatus (stream, node_om, visited_link_groups)
return nil
end
function handleStream (stream, node_om, device_om)
if not Settings.get_boolean ("bluetooth.autoswitch-to-headset-profile") then
return
end
local dev_id = checkStreamStatus (stream, node_om)
if dev_id ~= nil then
active_streams [stream.id] = dev_id
previous_streams [stream.id] = dev_id
triggerSwitchDeviceToHeadsetProfile (dev_id, device_om)
else
dev_id = active_streams [stream.id]
active_streams [stream.id] = nil
if dev_id ~= nil then
triggerRestoreProfile (dev_id, device_om)
end
end
end
function handleAllStreams (node_om, device_om)
function isBluetoothLoopbackSourceNodeLinkedToStream (bt_node, node_om, link_om)
local bt_node_id = bt_node["bound-id"]
for stream in node_om:iterate {
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
Constraint { "node.link-group", "-", type = "pw" },
Constraint { "stream.monitor", "!", "true", type = "pw" },
Constraint { "bluez5.loopback", "!", "true", type = "pw" }
} do
handleStream (stream, node_om, device_om)
local linked_bt_node = getLinkedBluetoothLoopbackSourceNodeForStream (stream, node_om, link_om)
if linked_bt_node ~= nil then
local linked_bt_node_id = linked_bt_node ["bound-id"]
if tonumber (linked_bt_node_id) == tonumber (bt_node_id) then
return true
end
end
end
return false
end
SimpleEventHook {
name = "node-removed@autoswitch-bluetooth-profile",
local evaluate_bluetooth_profiles_hook = SimpleEventHook {
name = "evaluate-bluetooth-profiles@autoswitch-bluetooth-profile",
interests = {
EventInterest {
Constraint { "event.type", "=", "node-removed" },
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
Constraint { "bluez5.loopback", "!", "true", type = "pw" },
Constraint { "event.type", "=", "evaluate-bluetooth-profiles" },
},
},
execute = function (event)
local stream = event:get_subject ()
local source = event:get_source ()
local node_om = source:call ("get-object-manager", "node")
local device_om = source:call ("get-object-manager", "device")
local link_om = source:call ("get-object-manager", "link")
local dev_id = active_streams[stream.id]
active_streams[stream.id] = nil
previous_streams[stream.id] = nil
if dev_id ~= nil then
triggerRestoreProfile (dev_id, device_om)
-- Evaluate all bluetooth loopback source nodes, and switch to headset
-- profile only if the node is running and linked to a stream that is not a
-- monitor, otherwise just restore the profile.
--
-- If the bluetooth node is linked to a stream that is a monitor, its state
-- will be 'running', so we cannot just rely on the state to know if we
-- have to switch or not, we also need to check if the node is linked to
-- a stream that is not a monitor.
for bt_node in node_om:iterate {
Constraint { "media.class", "matches", "Audio/Source" },
Constraint { "device.id", "+" },
Constraint { "bluez5.loopback", "=", "true", type = "pw" }
} do
local bt_node_state = bt_node["state"]
local bt_dev_id = bt_node.properties ["device.id"]
if bt_node_state == "running" and
isBluetoothLoopbackSourceNodeLinkedToStream (bt_node, node_om, link_om) then
triggerSwitchDeviceToHeadsetProfile (bt_dev_id, device_om)
else
triggerRestoreProfile (bt_dev_id, device_om)
end
end
end
}:register ()
}
SimpleEventHook {
local link_added_hook = SimpleEventHook {
name = "link-added@autoswitch-bluetooth-profile",
interests = {
EventInterest {
@ -438,46 +392,149 @@ SimpleEventHook {
},
},
execute = function (event)
local link = event:get_subject ()
local source = event:get_source ()
local node_om = source:call ("get-object-manager", "node")
local device_om = source:call ("get-object-manager", "device")
local link_props = link.properties
local link = event:get_subject ()
local in_stream_id = link.properties["link.input.node"]
for stream in node_om:iterate {
-- Only evaluate bluetooth profiles if a capture stream was linked
local stream = node_om:lookup {
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
Constraint { "node.link-group", "-", type = "pw" },
Constraint { "stream.monitor", "!", "true", type = "pw" },
Constraint { "bluez5.loopback", "!", "true", type = "pw" }
} do
local in_id = tonumber(link_props["link.input.node"])
local stream_id = tonumber(stream["bound-id"])
if in_id == stream_id then
handleStream (stream, node_om, device_om)
end
Constraint { "bluez5.loopback", "!", "true", type = "pw" },
Constraint { "bound-id", "=", in_stream_id, type = "gobject" },
}
if stream ~= nil then
capture_stream_links [link.id] = true
source:call ("push-event", "evaluate-bluetooth-profiles", nil, nil)
end
end
}:register ()
}
SimpleEventHook {
name = "bluez-device-added@autoswitch-bluetooth-profile",
local link_removed_hook = SimpleEventHook {
name = "link-removed@autoswitch-bluetooth-profile",
interests = {
EventInterest {
Constraint { "event.type", "=", "device-added" },
Constraint { "event.type", "=", "link-removed" },
},
},
execute = function (event)
local source = event:get_source ()
local link = event:get_subject ()
-- Only evaluate bluetooth profiles if a capture stream was unlinked
if capture_stream_links [link.id] then
capture_stream_links [link.id] = nil
source:call ("push-event", "evaluate-bluetooth-profiles", nil, nil)
end
end
}
local state_changed_hook = SimpleEventHook {
name = "bluez-loopback-state-changed@autoswitch-bluetooth-profile",
interests = {
EventInterest {
Constraint { "event.type", "=", "node-state-changed" },
Constraint { "media.class", "matches", "Audio/Source" },
Constraint { "device.id", "+" },
Constraint { "bluez5.loopback", "=", "true", type = "pw" }
},
},
execute = function (event)
local source = event:get_source ()
source:call ("push-event", "evaluate-bluetooth-profiles", nil, nil)
end
}
local node_added_hook = SimpleEventHook {
name = "bluez-loopback-added@autoswitch-bluetooth-profile",
interests = {
EventInterest {
Constraint { "event.type", "=", "node-added" },
Constraint { "media.class", "matches", "Audio/Source" },
Constraint { "device.id", "+" },
Constraint { "bluez5.loopback", "=", "true", type = "pw" }
},
},
execute = function (event)
local source = event:get_source ()
source:call ("push-event", "evaluate-bluetooth-profiles", nil, nil)
end
}
local device_profile_changed_hook = SimpleEventHook {
name = "device/store-user-selected-profile",
interests = {
EventInterest {
Constraint { "event.type", "=", "device-params-changed" },
Constraint { "event.subject.param-id", "=", "Profile" },
Constraint { "device.api", "=", "bluez5" },
},
},
execute = function (event)
local device = event:get_subject ()
local source = event:get_source ()
local node_om = source:call ("get-object-manager", "node")
local device_om = source:call ("get-object-manager", "device")
-- Devices are unswitched initially
saveLastProfile (device, nil)
-- Handle all streams when BT device is added
handleAllStreams (node_om, device_om)
-- Always save the current profile when it changes
local cur_profile = getCurrentProfile (device)
if cur_profile ~= nil then
if hasProfileInputRoute (device, cur_profile.index) then
log:info (device, "Saving headset profile " .. cur_profile.name)
saveHeadsetProfile (device, cur_profile.name, cur_profile.save)
else
log:info (device, "Saving non-headset profile " .. cur_profile.name)
saveNonHeadsetProfile (device, cur_profile.name)
end
end
end
}:register ()
}
function evaluatePersistentStorage ()
if Settings.get_boolean ("bluetooth.use-persistent-storage") and
not persistent_storage_hooks_registered then
state = State ("bluetooth-autoswitch")
headset_profiles = state:load ()
persistent_storage_hooks_registered = true
elseif persistent_storage_hooks_registered then
state = nil
headset_profiles = {}
persistent_storage_hooks_registered = false
end
end
function evaluateAutoswitch ()
if Settings.get_boolean ("bluetooth.autoswitch-to-headset-profile") and
not autoswitch_hooks_registered then
capture_stream_links = {}
restore_timeout_source = {}
switch_timeout_source = {}
evaluate_bluetooth_profiles_hook:register ()
link_added_hook:register ()
link_removed_hook:register ()
state_changed_hook:register ()
node_added_hook:register ()
device_profile_changed_hook:register ()
autoswitch_hooks_registered = true
elseif autoswitch_hooks_registered then
capture_stream_links = nil
restore_timeout_source = nil
switch_timeout_source = nil
evaluate_bluetooth_profiles_hook:remove ()
link_added_hook:remove ()
link_removed_hook:remove ()
state_changed_hook:remove ()
node_added_hook:remove ()
device_profile_changed_hook:remove ()
autoswitch_hooks_registered = false
end
end
Settings.subscribe ("bluetooth.use-persistent-storage", function ()
evaluatePersistentStorage ()
end)
evaluatePersistentStorage ()
Settings.subscribe ("bluetooth.autoswitch-to-headset-profile", function ()
evaluateAutoswitch ()
end)
evaluateAutoswitch ()