#include "connection/dbus/bluetooth.hpp" #include "services/notificationController.hpp" #include #include #include const std::string agent_introspection_xml = R"( )"; BluetoothController::BluetoothController() { connect_system_async(sigc::mem_fun(*this, &BluetoothController::onBusConnected)); } sigc::signal &BluetoothController::signalPoweredChanged() { return m_powered_signal; } sigc::signal &BluetoothController::signalDiscoveringChanged() { return m_discovering_signal; } sigc::signal &BluetoothController::signalDeviceAdded() { return m_device_added_signal; } sigc::signal &BluetoothController::signalDeviceRemoved() { return m_device_removed_signal; } sigc::signal &BluetoothController::signalDeviceChanged() { return m_device_changed_signal; } bool BluetoothController::isPowered() const { return m_powered; } bool BluetoothController::isDiscovering() const { return m_discovering; } std::vector BluetoothController::getDevices() const { std::vector result; result.reserve(m_devices.size()); for (const auto &[_, device] : m_devices) { result.push_back(device); } return result; } std::vector BluetoothController::getPairedDevices() const { std::vector result; for (const auto &[_, device] : m_devices) { if (device.paired) { result.push_back(device); } } return result; } void BluetoothController::setPowered(bool powered) { spdlog::info("Bluetooth: setting powered to {}", powered); setDbusProperty(m_adapter_path, "org.bluez.Adapter1", "Powered", Glib::Variant::create(powered)); } void BluetoothController::startDiscovery() { spdlog::info("Bluetooth: starting discovery"); if (m_adapter_proxy) { m_adapter_proxy->call("StartDiscovery"); } } void BluetoothController::stopDiscovery() { spdlog::info("Bluetooth: stopping discovery"); if (m_adapter_proxy) { m_adapter_proxy->call("StopDiscovery"); } } void BluetoothController::pairDevice(const std::string &object_path) { spdlog::info("Bluetooth: pairing device {}", object_path); auto it = m_device_proxies.find(object_path); if (it != m_device_proxies.end() && it->second) { it->second->call("Pair"); } } void BluetoothController::unpairDevice(const std::string &object_path) { spdlog::info("Bluetooth: unpairing device {}", object_path); if (!m_adapter_proxy) return; auto params = Glib::VariantContainerBase::create_tuple( Glib::Variant::create(object_path)); m_adapter_proxy->call("RemoveDevice", params); } void BluetoothController::connectDevice(const std::string &object_path) { spdlog::info("Bluetooth: connecting device {}", object_path); auto it = m_device_proxies.find(object_path); if (it != m_device_proxies.end() && it->second) { it->second->call("Connect"); } } void BluetoothController::disconnectDevice(const std::string &object_path) { spdlog::info("Bluetooth: disconnecting device {}", object_path); auto it = m_device_proxies.find(object_path); if (it != m_device_proxies.end() && it->second) { it->second->call("Disconnect"); } } void BluetoothController::trustDevice(const std::string &object_path, bool trusted) { spdlog::info("Bluetooth: setting trusted={} for device {}", trusted, object_path); setDbusProperty(object_path, "org.bluez.Device1", "Trusted", Glib::Variant::create(trusted)); } // --------------------------------------------------------------------------- // Bus connection // --------------------------------------------------------------------------- void BluetoothController::onBusConnected(const Glib::RefPtr &result) { if (!result) { spdlog::error("Bluetooth: null async result"); return; } try { connection = Gio::DBus::Connection::get_finish(result); m_object_manager_proxy = Gio::DBus::Proxy::create_sync( connection, "org.bluez", "/", "org.freedesktop.DBus.ObjectManager"); if (m_object_manager_proxy) { m_object_manager_proxy->signal_signal().connect( sigc::mem_fun(*this, &BluetoothController::onObjectManagerSignal)); enumerateObjects(); } registerAgent(); } catch (const Glib::Error &ex) { spdlog::error("Bluetooth DBus Connection Error: {}", ex.what()); } } // --------------------------------------------------------------------------- // Enumerate existing BlueZ objects // --------------------------------------------------------------------------- void BluetoothController::enumerateObjects() { if (!m_object_manager_proxy) return; try { auto result = m_object_manager_proxy->call_sync("GetManagedObjects"); auto objects = Glib::VariantBase::cast_dynamic( result.get_child(0)); for (gsize i = 0; i < objects.get_n_children(); i++) { auto entry = Glib::VariantBase::cast_dynamic( objects.get_child(i)); auto path = static_cast( Glib::VariantBase::cast_dynamic>( entry.get_child(0)) .get()); parseInterfaces(path, entry.get_child(1)); } } catch (const Glib::Error &ex) { spdlog::error("Bluetooth: Error enumerating objects: {}", ex.what()); } } // --------------------------------------------------------------------------- // Parse an interfaces-and-properties dict a{sa{sv}} // --------------------------------------------------------------------------- void BluetoothController::parseInterfaces(const std::string &object_path, const Glib::VariantBase &interfaces_var) { auto ifaces = Glib::VariantBase::cast_dynamic(interfaces_var); for (gsize i = 0; i < ifaces.get_n_children(); i++) { auto entry = Glib::VariantBase::cast_dynamic( ifaces.get_child(i)); auto iface_name = static_cast( Glib::VariantBase::cast_dynamic>( entry.get_child(0)) .get()); if (iface_name != "org.bluez.Device1" && iface_name != "org.bluez.Adapter1") { continue; } auto props = Glib::VariantBase::cast_dynamic>( entry.get_child(1)) .get(); if (iface_name == "org.bluez.Device1") { addDevice(object_path, props); } else { setupAdapter(object_path, props); } } } // --------------------------------------------------------------------------- // ObjectManager signal handler (InterfacesAdded / InterfacesRemoved) // --------------------------------------------------------------------------- void BluetoothController::onObjectManagerSignal( const Glib::ustring &, const Glib::ustring &signal_name, const Glib::VariantContainerBase ¶meters) { if (signal_name == "InterfacesAdded") { // signature: (oa{sa{sv}}) auto path = static_cast( Glib::VariantBase::cast_dynamic>( parameters.get_child(0)) .get()); parseInterfaces(path, parameters.get_child(1)); } else if (signal_name == "InterfacesRemoved") { // signature: (oas) auto path = static_cast( Glib::VariantBase::cast_dynamic>( parameters.get_child(0)) .get()); auto ifaces = Glib::VariantBase::cast_dynamic< Glib::Variant>>(parameters.get_child(1)) .get(); for (const auto &iface : ifaces) { if (iface == "org.bluez.Device1") { removeDevice(path); } else if (iface == "org.bluez.Adapter1" && path == m_adapter_path) { m_adapter_proxy.reset(); m_adapter_path.clear(); m_powered = false; m_discovering = false; m_powered_signal.emit(false); m_discovering_signal.emit(false); spdlog::info("Bluetooth adapter removed"); } } } } // --------------------------------------------------------------------------- // Adapter // --------------------------------------------------------------------------- void BluetoothController::setupAdapter(const std::string &path, const PropertiesMap &properties) { m_adapter_path = path; spdlog::info("Bluetooth adapter found: {}", path); auto it = properties.find("Powered"); if (it != properties.end() && it->second.is_of_type(Glib::VariantType("b"))) { m_powered = Glib::VariantBase::cast_dynamic>(it->second).get(); m_powered_signal.emit(m_powered); } it = properties.find("Discovering"); if (it != properties.end() && it->second.is_of_type(Glib::VariantType("b"))) { m_discovering = Glib::VariantBase::cast_dynamic>(it->second).get(); m_discovering_signal.emit(m_discovering); } try { m_adapter_proxy = Gio::DBus::Proxy::create_sync( connection, "org.bluez", path, "org.bluez.Adapter1"); if (m_adapter_proxy) { m_adapter_proxy->signal_properties_changed().connect( sigc::mem_fun(*this, &BluetoothController::onAdapterPropertiesChanged)); } } catch (const Glib::Error &ex) { spdlog::error("Bluetooth: Error creating adapter proxy: {}", ex.what()); } } void BluetoothController::onAdapterPropertiesChanged( const Gio::DBus::Proxy::MapChangedProperties &changed, const std::vector &) { auto it = changed.find("Powered"); if (it != changed.end() && it->second.is_of_type(Glib::VariantType("b"))) { m_powered = Glib::VariantBase::cast_dynamic>(it->second).get(); spdlog::info("Bluetooth powered: {}", m_powered); m_powered_signal.emit(m_powered); } it = changed.find("Discovering"); if (it != changed.end() && it->second.is_of_type(Glib::VariantType("b"))) { m_discovering = Glib::VariantBase::cast_dynamic>(it->second).get(); spdlog::info("Bluetooth discovering: {}", m_discovering); m_discovering_signal.emit(m_discovering); } } // --------------------------------------------------------------------------- // Device management // --------------------------------------------------------------------------- void BluetoothController::addDevice(const std::string &path, const PropertiesMap &properties) { auto device = parseDeviceProperties(path, properties); m_devices[path] = device; spdlog::info("Bluetooth device added: {} ({}) {}", device.name, device.address, device.icon); try { auto proxy = Gio::DBus::Proxy::create_sync( connection, "org.bluez", path, "org.bluez.Device1"); if (proxy) { proxy->signal_properties_changed().connect( [this, path](const Gio::DBus::Proxy::MapChangedProperties &changed, const std::vector &invalidated) { onDevicePropertiesChanged(path, changed, invalidated); }); m_device_proxies[path] = proxy; } } catch (const Glib::Error &ex) { spdlog::error("Bluetooth: Error creating device proxy for {}: {}", path, ex.what()); } m_device_added_signal.emit(device); } void BluetoothController::removeDevice(const std::string &path) { spdlog::info("Bluetooth device removed: {}", path); m_devices.erase(path); m_device_proxies.erase(path); m_device_removed_signal.emit(path); } void BluetoothController::onDevicePropertiesChanged( const std::string &object_path, const Gio::DBus::Proxy::MapChangedProperties &changed, const std::vector &) { auto it = m_devices.find(object_path); if (it == m_devices.end()) return; auto &device = it->second; for (const auto &[key, value] : changed) { if (key == "Name" && value.is_of_type(Glib::VariantType("s"))) { device.name = static_cast( Glib::VariantBase::cast_dynamic>(value).get()); } else if (key == "Alias" && value.is_of_type(Glib::VariantType("s"))) { // Only use Alias as fallback if Name is still the MAC address auto alias = static_cast( Glib::VariantBase::cast_dynamic>(value).get()); if (device.name == device.address) { device.name = alias; } } else if (key == "Paired" && value.is_of_type(Glib::VariantType("b"))) { device.paired = Glib::VariantBase::cast_dynamic>(value).get(); } else if (key == "Connected" && value.is_of_type(Glib::VariantType("b"))) { device.connected = Glib::VariantBase::cast_dynamic>(value).get(); } else if (key == "Trusted" && value.is_of_type(Glib::VariantType("b"))) { device.trusted = Glib::VariantBase::cast_dynamic>(value).get(); } else if (key == "RSSI" && value.is_of_type(Glib::VariantType("n"))) { device.rssi = Glib::VariantBase::cast_dynamic>(value).get(); } else if (key == "Icon" && value.is_of_type(Glib::VariantType("s"))) { device.icon = static_cast( Glib::VariantBase::cast_dynamic>(value).get()); } } m_device_changed_signal.emit(device); } // --------------------------------------------------------------------------- // Property parsing helper // --------------------------------------------------------------------------- BluetoothDevice BluetoothController::parseDeviceProperties( const std::string &path, const PropertiesMap &properties) { BluetoothDevice device; device.object_path = path; auto getString = [&](const Glib::ustring &key) -> std::string { auto it = properties.find(key); if (it != properties.end() && it->second.is_of_type(Glib::VariantType("s"))) { return static_cast( Glib::VariantBase::cast_dynamic>(it->second).get()); } return ""; }; auto getBool = [&](const Glib::ustring &key) -> bool { auto it = properties.find(key); if (it != properties.end() && it->second.is_of_type(Glib::VariantType("b"))) { return Glib::VariantBase::cast_dynamic>(it->second).get(); } return false; }; device.address = getString("Address"); device.name = getString("Name"); if (device.name.empty()) device.name = getString("Alias"); if (device.name.empty()) device.name = device.address; device.icon = getString("Icon"); device.paired = getBool("Paired"); device.connected = getBool("Connected"); device.trusted = getBool("Trusted"); auto rssi_it = properties.find("RSSI"); if (rssi_it != properties.end() && rssi_it->second.is_of_type(Glib::VariantType("n"))) { device.rssi = Glib::VariantBase::cast_dynamic>(rssi_it->second).get(); } return device; } // --------------------------------------------------------------------------- // D-Bus property setter (calls org.freedesktop.DBus.Properties.Set) // --------------------------------------------------------------------------- void BluetoothController::setDbusProperty(const std::string &object_path, const std::string &interface, const std::string &property, const Glib::VariantBase &value) { if (!connection || object_path.empty()) return; try { auto props_proxy = Gio::DBus::Proxy::create_sync( connection, "org.bluez", object_path, "org.freedesktop.DBus.Properties"); auto wrapped = Glib::Variant::create(value); auto params = Glib::VariantContainerBase::create_tuple({Glib::Variant::create(interface), Glib::Variant::create(property), wrapped}); props_proxy->call_sync("Set", params); } catch (const Glib::Error &ex) { spdlog::error("Error setting {}.{}: {}", interface, property, ex.what()); } } // --------------------------------------------------------------------------- // Agent // --------------------------------------------------------------------------- void BluetoothController::registerAgent() { if (!connection) return; try { m_node_info = Gio::DBus::NodeInfo::create_for_xml(agent_introspection_xml); auto interface_info = m_node_info->lookup_interface("org.bluez.Agent1"); m_interface_vtable = std::make_shared( sigc::mem_fun(*this, &BluetoothController::on_agent_method_call)); m_agent_id = connection->register_object("/org/bluez/bar_agent", interface_info, *m_interface_vtable); auto agent_manager = Gio::DBus::Proxy::create_sync( connection, "org.bluez", "/org/bluez", "org.bluez.AgentManager1"); if (agent_manager) { agent_manager->call_sync( "RegisterAgent", Glib::VariantContainerBase::create_tuple( {Glib::Variant::create("/org/bluez/bar_agent"), Glib::Variant::create("DisplayYesNo")})); agent_manager->call_sync( "RequestDefaultAgent", Glib::VariantContainerBase::create_tuple( {Glib::Variant::create("/org/bluez/bar_agent")})); spdlog::info("Bluetooth Agent registered successfully"); } } catch (const Glib::Error &ex) { spdlog::error("Bluetooth: Failed to register agent: {}", ex.what()); } } void BluetoothController::on_agent_method_call( const Glib::RefPtr &, const Glib::ustring &, const Glib::ustring &, const Glib::ustring &, const Glib::ustring &method_name, const Glib::VariantContainerBase ¶meters, const Glib::RefPtr &invocation) { spdlog::info("Bluetooth Agent method called: {}", method_name.c_str()); if (method_name == "RequestConfirmation") { // Signature: (ou) std::string device_path = static_cast( Glib::VariantBase::cast_dynamic>( parameters.get_child(0)).get()); guint32 passkey = Glib::VariantBase::cast_dynamic>( parameters.get_child(1)).get(); std::string device_name = device_path; if (m_devices.count(device_path)) { device_name = m_devices[device_path].name; if (device_name.empty()) device_name = m_devices[device_path].address; } std::ostringstream ss; ss << std::setw(6) << std::setfill('0') << passkey; std::string passkey_str = ss.str(); NotifyMessage msg; msg.summary = "Bluetooth Pairing Request"; msg.body = "Device '" + device_name + "' wants to pair.\nPasskey: " + passkey_str; msg.app_name = "Bar"; msg.actions = {"Confirm", "Confirm", "Cancel", "Cancel"}; msg.urgency = NotificationUrgency::CRITICAL; msg.expire_timeout = -1; // No timeout, requires user interaction // Capture invocation to respond later // Note: invocation is a RefPtr, so copying it increases ref count msg.on_action = [invocation](const std::string &action) { if (action == "Confirm") { invocation->return_value(Glib::VariantContainerBase()); } else { invocation->return_dbus_error("org.bluez.Error.Rejected", "Rejected by user"); } }; NotificationController::getInstance()->showNotificationOnAllMonitors(msg); } else if (method_name == "RequestAuthorization") { // Signature: (o) std::string device_path = static_cast( Glib::VariantBase::cast_dynamic>( parameters.get_child(0)).get()); std::string device_name = device_path; if (m_devices.count(device_path)) { device_name = m_devices[device_path].name; if (device_name.empty()) device_name = m_devices[device_path].address; } NotifyMessage msg; msg.summary = "Bluetooth Authorization Request"; msg.body = "Device '" + device_name + "' requests authorization to connect."; msg.app_name = "Bar"; msg.actions = {"Allow", "Allow", "Deny", "Deny"}; msg.urgency = NotificationUrgency::LOW; msg.expire_timeout = -1; msg.on_action = [invocation](const std::string &action) { if (action == "Allow") { invocation->return_value(Glib::VariantContainerBase()); } else { invocation->return_dbus_error("org.bluez.Error.Rejected", "Rejected by user"); } }; NotificationController::getInstance()->showNotificationOnAllMonitors(msg); } else if (method_name == "AuthorizeService") { // Signature: (os) std::string device_path = static_cast( Glib::VariantBase::cast_dynamic>( parameters.get_child(0)).get()); std::string uuid = static_cast( Glib::VariantBase::cast_dynamic>( parameters.get_child(1)).get()); std::string device_name = device_path; if (m_devices.count(device_path)) { device_name = m_devices[device_path].name; if (device_name.empty()) device_name = m_devices[device_path].address; } NotifyMessage msg; msg.summary = "Bluetooth Service Authorization"; msg.body = "Device '" + device_name + "' wants to access service: " + uuid; msg.app_name = "Bar"; msg.actions = {"Allow", "Allow", "Deny", "Deny"}; msg.urgency = NotificationUrgency::CRITICAL; msg.expire_timeout = -1; msg.on_action = [invocation](const std::string &action) { if (action == "Allow") { invocation->return_value(Glib::VariantContainerBase()); } else { invocation->return_dbus_error("org.bluez.Error.Rejected", "Rejected by user"); } }; NotificationController::getInstance()->showNotificationOnAllMonitors(msg); } else if (method_name == "DisplayPinCode") { // Signature: (os) std::string device_path = static_cast( Glib::VariantBase::cast_dynamic>( parameters.get_child(0)).get()); std::string pincode = static_cast( Glib::VariantBase::cast_dynamic>( parameters.get_child(1)).get()); std::string device_name = device_path; if (m_devices.count(device_path)) { device_name = m_devices[device_path].name; if (device_name.empty()) device_name = m_devices[device_path].address; } NotifyMessage msg; msg.summary = "Bluetooth Pin Code"; msg.body = "Pin code for device '" + device_name + "': " + pincode; msg.app_name = "Bar"; msg.actions = {"Dismiss"}; msg.urgency = NotificationUrgency::NORMAL; msg.expire_timeout = 0; NotificationController::getInstance()->showNotificationOnAllMonitors(msg); invocation->return_value(Glib::VariantContainerBase()); } else if (method_name == "DisplayPasskey") { // Signature: (ouq) std::string device_path = static_cast( Glib::VariantBase::cast_dynamic>( parameters.get_child(0)).get()); guint32 passkey = Glib::VariantBase::cast_dynamic>( parameters.get_child(1)).get(); guint16 entered = Glib::VariantBase::cast_dynamic>( parameters.get_child(2)).get(); // Only show on first display (entered == 0) to avoid spam if (entered == 0) { std::string device_name = device_path; if (m_devices.count(device_path)) { device_name = m_devices[device_path].name; if (device_name.empty()) device_name = m_devices[device_path].address; } std::ostringstream ss; ss << std::setw(6) << std::setfill('0') << passkey; std::string passkey_str = ss.str(); NotifyMessage msg; msg.summary = "Bluetooth Passkey"; msg.body = "Type this passkey on '" + device_name + "': " + passkey_str; msg.app_name = "Bar"; msg.actions = {"Dismiss"}; msg.urgency = NotificationUrgency::CRITICAL; msg.expire_timeout = -1; NotificationController::getInstance()->showNotificationOnAllMonitors(msg); } invocation->return_value(Glib::VariantContainerBase()); } else if (method_name == "RequestPinCode") { // Signature: (o) -> s std::string device_path = static_cast( Glib::VariantBase::cast_dynamic>( parameters.get_child(0)).get()); std::string device_name = device_path; if (m_devices.count(device_path)) { device_name = m_devices[device_path].name; if (device_name.empty()) device_name = m_devices[device_path].address; } NotifyMessage msg; msg.summary = "Bluetooth PIN Request"; msg.body = "Enter PIN Code for '" + device_name + "'"; msg.app_name = "Bar"; msg.urgency = NotificationUrgency::CRITICAL; msg.expire_timeout = -1; msg.has_input = true; msg.input_placeholder = "PIN Code"; msg.on_input = [invocation](const std::string &input) { invocation->return_value( Glib::VariantContainerBase::create_tuple( {Glib::Variant::create(input)})); }; NotificationController::getInstance()->showNotificationOnAllMonitors(msg); } else if (method_name == "RequestPasskey") { // Signature: (o) -> u std::string device_path = static_cast( Glib::VariantBase::cast_dynamic>( parameters.get_child(0)).get()); std::string device_name = device_path; if (m_devices.count(device_path)) { device_name = m_devices[device_path].name; if (device_name.empty()) device_name = m_devices[device_path].address; } NotifyMessage msg; msg.summary = "Bluetooth Passkey Request"; msg.body = "Enter Passkey for '" + device_name + "'"; msg.app_name = "Bar"; msg.urgency = NotificationUrgency::CRITICAL; msg.expire_timeout = -1; msg.has_input = true; msg.input_placeholder = "Passkey (0-999999)"; msg.on_input = [invocation](const std::string &input) { try { uint32_t passkey = std::stoul(input); invocation->return_value( Glib::VariantContainerBase::create_tuple( {Glib::Variant::create(passkey)})); } catch (...) { invocation->return_dbus_error("org.bluez.Error.Failed", "Invalid passkey format"); } }; NotificationController::getInstance()->showNotificationOnAllMonitors(msg); } else if (method_name == "Cancel" || method_name == "Release") { // Just return invocation->return_value(Glib::VariantContainerBase()); } else { // Not implemented invocation->return_dbus_error("org.bluez.Error.Rejected", "Not implemented"); } }