From 5b45748c5dc9c2d1718b6219e30a64e07e5ca313 Mon Sep 17 00:00:00 2001 From: Muhammad Asif Date: Mon, 19 May 2025 22:22:08 +0200 Subject: ayatana-indicator-bluetooth: add initial pairing agent Co-authored-by: Robert Tari Signed-off-by: Muhammad Asif --- src/bluez.vala | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) (limited to 'src/bluez.vala') diff --git a/src/bluez.vala b/src/bluez.vala index 16e237a..fabee5f 100644 --- a/src/bluez.vala +++ b/src/bluez.vala @@ -55,6 +55,8 @@ public class Bluez: Bluetooth, Object /* maps our arbitrary unique id to a Bluetooth.Device struct for public consumption */ private HashTable id_to_device; + private BluezAgentManager agent_manager; + public Bluez (KillSwitch? killswitch) { init_bluez_state_vars (); @@ -119,6 +121,8 @@ public class Bluez: Bluetooth, Object try { manager = bus.get_proxy_sync (BLUEZ_BUSNAME, "/"); + agent_manager = bus.get_proxy_sync (BLUEZ_BUSNAME, "/org/bluez"); + add_agent ("/agent"); // Find the adapters and watch for changes manager.interfaces_added.connect ((object_path, interfaces_and_properties) => { @@ -425,6 +429,12 @@ public class Bluez: Bluetooth, Object } } + public string get_device_name (ObjectPath path) + { + var device = id_to_device.lookup(path_to_id.lookup(path)); + return device.name; + } + public List get_devices () { return id_to_device.get_values(); @@ -453,6 +463,27 @@ public class Bluez: Bluetooth, Object DBusCallFlags.NONE, -1); } } + + public void add_agent(string path) + { + try + { + agent_manager.register_agent (new GLib.ObjectPath(path), "DisplayYesNo"); + } + catch (GLib.Error pError) + { + warning ("Panic: Failed registering pairing agent: %s", pError.message); + } + + try + { + agent_manager.request_default_agent (new GLib.ObjectPath(path)); + } + catch (GLib.Error pError) + { + warning ("Panic: Failed getting default pairing agent: %s", pError.message); + } + } } [DBus (name = "org.freedesktop.DBus.ObjectManager")] @@ -479,3 +510,12 @@ private interface BluezDevice : DBusProxy { [DBus (name = "Disconnect")] public abstract void disconnect_() throws DBusError, IOError; } + +[DBus (name = "org.bluez.AgentManager1")] +private interface BluezAgentManager : DBusProxy { + [DBus (name = "RegisterAgent")] + public abstract void register_agent(GLib.ObjectPath object, string capabilities) throws DBusError, IOError; + + [DBus (name = "RequestDefaultAgent")] + public abstract void request_default_agent(GLib.ObjectPath object) throws DBusError, IOError; +} -- cgit v1.2.3 From 568f6aa32e290b4e37b61dfd9985ae81a08e2892 Mon Sep 17 00:00:00 2001 From: Muhammad Asif Date: Tue, 24 Jun 2025 00:19:48 +0500 Subject: agent: use signals to register agent to BlueZ Signed-off-by: Muhammad Asif --- src/bluetooth.vala | 2 ++ src/bluez.vala | 2 +- src/service.vala | 12 ++++++++---- 3 files changed, 11 insertions(+), 5 deletions(-) (limited to 'src/bluez.vala') diff --git a/src/bluetooth.vala b/src/bluetooth.vala index da7c176..9b2f9d5 100644 --- a/src/bluetooth.vala +++ b/src/bluetooth.vala @@ -55,5 +55,7 @@ public interface Bluetooth: Object public abstract string get_device_name (ObjectPath path); + public signal void agent_manager_ready (); + public abstract void add_agent (string path); } diff --git a/src/bluez.vala b/src/bluez.vala index fabee5f..6d08004 100644 --- a/src/bluez.vala +++ b/src/bluez.vala @@ -122,7 +122,7 @@ public class Bluez: Bluetooth, Object { manager = bus.get_proxy_sync (BLUEZ_BUSNAME, "/"); agent_manager = bus.get_proxy_sync (BLUEZ_BUSNAME, "/org/bluez"); - add_agent ("/agent"); + agent_manager_ready (); // Find the adapters and watch for changes manager.interfaces_added.connect ((object_path, interfaces_and_properties) => { diff --git a/src/service.vala b/src/service.vala index b79056e..e651811 100644 --- a/src/service.vala +++ b/src/service.vala @@ -29,7 +29,7 @@ public class Service: Object private MainLoop loop; private SimpleActionGroup actions; private HashTable profiles; - private Bluetooth bluez; + private Bluetooth bluetooth; private DBusConnection connection; private uint exported_action_id; private const string OBJECT_PATH = "/org/ayatana/indicator/bluetooth"; @@ -50,10 +50,10 @@ public class Service: Object } } - public Service (Bluetooth bluetooth) + public Service (Bluetooth bluetooth_service) { actions = new SimpleActionGroup (); - bluez = bluetooth; + bluetooth = bluetooth_service; profiles = new HashTable (str_hash, str_equal); profiles.insert ("phone", new Phone (bluetooth, actions)); @@ -83,6 +83,10 @@ public class Service: Object null, null); + bluetooth.agent_manager_ready.connect (() => { + bluetooth.add_agent ("/agent"); + }); + loop = new MainLoop (null, false); loop.run (); @@ -97,7 +101,7 @@ public class Service: Object { try { - connection.register_object ("/agent", new Agent (bluez)); + connection.register_object ("/agent", new Agent (bluetooth)); } catch (GLib.IOError pError) { -- cgit v1.2.3 From 6c7c62d7c6d153293c8aea8dd1c71faabdef3b3e Mon Sep 17 00:00:00 2001 From: Muhammad Date: Tue, 22 Jul 2025 21:05:43 +0500 Subject: agent: Add support for PIN and passkeys on Lomiri * Passkey/PIN display is slightly wonky right now, BlueZ doesn't call the Cancel() method once pairing is done, so you have to swipe away the notification manually. But apart from that, everything else works. Signed-off-by: Muhammad --- src/agent.vala | 139 +++++++++++++++++++++++++++++++++++++++++-------------- src/bluez.vala | 2 +- src/service.vala | 30 +++++++++++- 3 files changed, 134 insertions(+), 37 deletions(-) (limited to 'src/bluez.vala') diff --git a/src/agent.vala b/src/agent.vala index beb8f8b..9e568a4 100644 --- a/src/agent.vala +++ b/src/agent.vala @@ -1,48 +1,103 @@ [DBus (name = "org.bluez.Agent1")] public class Agent: Object { + public GLib.Menu menu; + public GLib.SimpleActionGroup actions; + private GLib.SimpleAction pin_action; + public string menu_path; + public string actions_path; + private MainLoop loop; private Bluetooth bluetooth; + private Notify.Notification? notification; + private string passkey; public Agent (Bluetooth bluez) { + // Menu + menu = new GLib.Menu (); + GLib.MenuItem item = new GLib.MenuItem ("", "notifications.pin"); + item.set_attribute_value ("x-canonical-type", new Variant.string ("com.canonical.snapdecision.textfield")); + item.set_attribute_value ("x-echo-mode-password", new Variant.boolean (false)); + menu.append_item (item); + + // Actions + actions = new GLib.SimpleActionGroup (); + pin_action = new GLib.SimpleAction.stateful ("pin", null, new Variant.string ("")); + pin_action.change_state.connect ((value) => { + this.passkey = value.get_string (); + }); + actions.add_action (pin_action); + loop = new MainLoop (null, false); bluetooth = bluez; Notify.init ("ayatana-indicator-bluetooth"); } - private bool sendNotification (string device_name, string body) + /* TODO: Add a better way to differentiate between rejected and cancelled errors, maybe with an enum */ + private bool sendNotification (string device_name, string body, bool need_input, bool have_actions) { - bool accepted = false; + bool accepted = !have_actions; + + notification = new Notify.Notification (@"Pair with $device_name?", body, "bluetooth-active"); + notification.closed.connect (() => { + accepted = false; + notification = null; + + if (loop.is_running ()) { + loop.quit (); + } + }); + + bool is_lomiri = AyatanaCommon.utils_is_lomiri (); - Notify.Notification notification = new Notify.Notification (@"Pair with $device_name?", body, "bluetooth-active"); - bool bLomiri = AyatanaCommon.utils_is_lomiri (); + if (is_lomiri) { + if (have_actions) { + notification.set_hint ("x-lomiri-snap-decisions", true); + notification.set_hint ("x-lomiri-private-affirmative-tint", "true"); + } - if (bLomiri) - { - notification.set_hint ("x-lomiri-snap-decisions", true); - notification.set_hint ("x-lomiri-private-affirmative-tint", "true"); + if (need_input) { + VariantBuilder actions_builder = new VariantBuilder (new VariantType ("a{sv}")); + actions_builder.add ("{sv}", "notifications", new Variant.string (actions_path)); + + VariantBuilder builder = new VariantBuilder (new VariantType ("a{sv}")); + builder.add ("{sv}", "busName", new Variant.string ("org.ayatana.indicator.bluetooth")); + builder.add ("{sv}", "menuPath", new Variant.string (menu_path)); + builder.add ("{sv}", "actions", actions_builder.end ()); + + notification.set_hint ("x-lomiri-private-menu-model", builder.end ()); + } } - notification.add_action("yes_id", "Yes", (notif, action) => { - loop.quit (); - accepted = true; - }); - notification.add_action("no_id", "No", (notif, action) => { - loop.quit (); - accepted = false; - }); + if (have_actions) { + notification.add_action("yes_id", "Yes", (notif, action) => { + loop.quit (); + notification = null; + accepted = true; + }); + notification.add_action("no_id", "No", (notif, action) => { + loop.quit (); + notification = null; + accepted = false; + }); + } + + if (!have_actions && !need_input) { + // Display-only notification. Make sure we don't time out. + notification.set_hint ("urgency", 2); + } - try - { + try { notification.show (); } - catch (Error pError) - { - warning ("Panic: Failed showing notification: %s", pError.message); + catch (Error e) { + warning ("Panic: Failed showing notification: %s", e.message); } - loop.run (); + if (have_actions) { + loop.run (); + } return accepted; } @@ -54,42 +109,53 @@ public class Agent: Object public void RequestConfirmation (GLib.ObjectPath object, uint32 passkey) throws RejectedError, GLib.DBusError, GLib.IOError { string body = "Are you sure you want to pair with passkey %06u?".printf (passkey); - bool confirmed = sendNotification (bluetooth.get_device_name (object), body); + bool confirmed = sendNotification (bluetooth.get_device_name (object), body, false, true); - if (confirmed) { - return; - } else { + if (!confirmed) { throw new RejectedError.ERROR ("Rejected by user"); } } public void RequestAuthorization (GLib.ObjectPath object) throws RejectedError, GLib.DBusError, GLib.IOError { - bool authorized = sendNotification (bluetooth.get_device_name (object), "Are you sure you want to pair with this device?"); + bool authorized = sendNotification (bluetooth.get_device_name (object), "Are you sure you want to pair with this device?", false, true); - if (authorized) { - return; - } else { + if (!authorized) { throw new RejectedError.ERROR ("Rejected by user"); } } - public string RequestPinCode (GLib.ObjectPath object) throws GLib.DBusError, GLib.IOError + public string RequestPinCode (GLib.ObjectPath object) throws RejectedError, GLib.DBusError, GLib.IOError { - return "123456"; + bool accepted = sendNotification (bluetooth.get_device_name (object), "Enter PIN for this device", true, true); + + if (!accepted) { + throw new RejectedError.ERROR ("Rejected by user"); + } + + return passkey; } public void DisplayPinCode (GLib.ObjectPath object, string pincode) throws GLib.DBusError, GLib.IOError { + sendNotification (bluetooth.get_device_name (object), @"Enter the PIN code $pincode on the other device", false, false); } - public uint32 RequestPasskey (GLib.ObjectPath object) throws GLib.DBusError, GLib.IOError + public uint32 RequestPasskey (GLib.ObjectPath object) throws RejectedError, GLib.DBusError, GLib.IOError { - return 123456; + bool accepted = sendNotification (bluetooth.get_device_name (object), "Enter passkey for this device", true, true); + + if (!accepted) { + throw new RejectedError.ERROR ("Rejected by user"); + } + + return passkey.to_int (); } public void DisplayPasskey (GLib.ObjectPath object, uint32 passkey, uint16 entered) throws GLib.DBusError, GLib.IOError { + string body = "Enter the passkey %06u on the other device".printf (passkey); + sendNotification (bluetooth.get_device_name (object), body, false, false); } public void Cancel () throws GLib.DBusError, GLib.IOError @@ -97,6 +163,11 @@ public class Agent: Object if (loop.is_running ()) { loop.quit (); } + + if (notification != null) { + notification.close (); + notification = null; + } } public void Release () throws GLib.DBusError, GLib.IOError diff --git a/src/bluez.vala b/src/bluez.vala index 6d08004..8d481f2 100644 --- a/src/bluez.vala +++ b/src/bluez.vala @@ -468,7 +468,7 @@ public class Bluez: Bluetooth, Object { try { - agent_manager.register_agent (new GLib.ObjectPath(path), "DisplayYesNo"); + agent_manager.register_agent (new GLib.ObjectPath(path), AyatanaCommon.utils_is_lomiri() ? "KeyboardDisplay" : "DisplayYesNo"); } catch (GLib.Error pError) { diff --git a/src/service.vala b/src/service.vala index e651811..cff90d3 100644 --- a/src/service.vala +++ b/src/service.vala @@ -30,9 +30,15 @@ public class Service: Object private SimpleActionGroup actions; private HashTable profiles; private Bluetooth bluetooth; + private Agent agent; private DBusConnection connection; private uint exported_action_id; + + private uint exported_agent_action_id; + private uint exported_agent_menu_id; + private const string OBJECT_PATH = "/org/ayatana/indicator/bluetooth"; + private const string AGENT_OBJECT_PATH = "/org/ayatana/indicator/bluetooth/agent"; private void unexport () { @@ -47,6 +53,18 @@ public class Service: Object connection.unexport_action_group (exported_action_id); exported_action_id = 0; } + + if (exported_agent_menu_id != 0) + { + connection.unexport_menu_model (exported_agent_menu_id); + exported_agent_menu_id = 0; + } + + if (exported_agent_action_id != 0) + { + connection.unexport_action_group (exported_agent_action_id); + exported_agent_action_id = 0; + } } } @@ -54,6 +72,9 @@ public class Service: Object { actions = new SimpleActionGroup (); bluetooth = bluetooth_service; + agent = new Agent (bluetooth); + agent.actions_path = AGENT_OBJECT_PATH; + agent.menu_path = AGENT_OBJECT_PATH; profiles = new HashTable (str_hash, str_equal); profiles.insert ("phone", new Phone (bluetooth, actions)); @@ -84,7 +105,7 @@ public class Service: Object null); bluetooth.agent_manager_ready.connect (() => { - bluetooth.add_agent ("/agent"); + bluetooth.add_agent (AGENT_OBJECT_PATH); }); loop = new MainLoop (null, false); @@ -101,7 +122,7 @@ public class Service: Object { try { - connection.register_object ("/agent", new Agent (bluetooth)); + connection.register_object (AGENT_OBJECT_PATH, agent); } catch (GLib.IOError pError) { @@ -119,6 +140,11 @@ public class Service: Object debug (@"exporting action group '$(OBJECT_PATH)'"); exported_action_id = connection.export_action_group (OBJECT_PATH, actions); + + exported_agent_action_id = connection.export_action_group (AGENT_OBJECT_PATH, + agent.actions); + exported_agent_menu_id = connection.export_menu_model (AGENT_OBJECT_PATH, + agent.menu); } catch (Error e) { -- cgit v1.2.3 From 36d77944f2e7707eefd60ea208c6f4bc55a3f961 Mon Sep 17 00:00:00 2001 From: Muhammad Date: Tue, 10 Mar 2026 01:37:25 +0500 Subject: agent: add proper support for AuthorizeService Signed-off-by: Muhammad --- src/agent.vala | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++- src/bluetooth.vala | 6 ++++++ src/bluez.vala | 26 ++++++++++++++++++++++ src/device.vala | 5 ++++- 4 files changed, 98 insertions(+), 2 deletions(-) (limited to 'src/bluez.vala') diff --git a/src/agent.vala b/src/agent.vala index 9e568a4..16ce6a2 100644 --- a/src/agent.vala +++ b/src/agent.vala @@ -1,9 +1,12 @@ [DBus (name = "org.bluez.Agent1")] public class Agent: Object { + // These actions and menus are exposed on their relevant paths by service.vala public GLib.Menu menu; public GLib.SimpleActionGroup actions; private GLib.SimpleAction pin_action; + + // These paths are set by service.vala public string menu_path; public string actions_path; @@ -102,8 +105,66 @@ public class Agent: Object return accepted; } - public void AuthorizeService (GLib.ObjectPath object, string uuid) throws GLib.DBusError, GLib.IOError + public void AuthorizeService (GLib.ObjectPath object, string uuid) throws RejectedError, GLib.DBusError, GLib.IOError { + bool authorized = false; + bool trusted = false; + + string header = "Allow %s to connect?".printf (bluetooth.get_device_name (object)); + string body = "Allow the Bluetooth device to access a Bluetooth service?"; + + notification = new Notify.Notification (header, body, "bluetooth-active"); + notification.closed.connect (() => { + authorized = false; + notification = null; + + if (loop.is_running ()) { + loop.quit (); + } + }); + + if (AyatanaCommon.utils_is_lomiri ()) { + notification.set_hint ("x-lomiri-snap-decisions", true); + } + + notification.add_action ("trust_and_authorize", "Trust and authorize", (notif, action) => { + loop.quit (); + notification = null; + + trusted = true; + authorized = true; + }); + + notification.add_action ("authorize", "Authorize", (notif, action) => { + loop.quit (); + notification = null; + + authorized = true; + }); + + notification.add_action ("reject", "Do not authorize", (notif, action) => { + loop.quit (); + notification = null; + }); + + try { + notification.show (); + } + catch (Error e) { + warning ("Panic: Failed showing notification: %s", e.message); + } + + loop.run (); + + // Once the loop quits, we can see if we want to set the 'Trusted' property in BlueZ + if (trusted) { + Device device = bluetooth.get_device (object); + bluetooth.set_device_trusted (device.id, trusted); + } + + if (!authorized) { + throw new RejectedError.ERROR ("Rejected by user"); + } } public void RequestConfirmation (GLib.ObjectPath object, uint32 passkey) throws RejectedError, GLib.DBusError, GLib.IOError diff --git a/src/bluetooth.vala b/src/bluetooth.vala index 9b2f9d5..47c6f8e 100644 --- a/src/bluetooth.vala +++ b/src/bluetooth.vala @@ -46,6 +46,9 @@ public interface Bluetooth: Object /* Get a list of the Device structs that we know about */ public abstract List get_devices (); + /* Get a Device from its DBus path */ + public abstract Device get_device (ObjectPath path); + /* Emitted when one or more of the devices is added, removed, or changed */ public signal void devices_changed (); @@ -53,6 +56,9 @@ public interface Bluetooth: Object The device_key argument comes from the Device struct */ public abstract void set_device_connected (uint device_key, bool connected); + /* Sets whether or not a device is trusted (allowed to connect without authorization) */ + public abstract void set_device_trusted (uint device_key, bool trusted); + public abstract string get_device_name (ObjectPath path); public signal void agent_manager_ready (); diff --git a/src/bluez.vala b/src/bluez.vala index 8d481f2..353b1a2 100644 --- a/src/bluez.vala +++ b/src/bluez.vala @@ -325,6 +325,10 @@ public class Bluez: Bluetooth, Object v = device_proxy.get_cached_property ("Connected"); var is_connected = (v != null) && v.get_boolean (); + // look up whether the device is trusted + v = device_proxy.get_cached_property ("Trusted"); + var is_trusted = (v != null) && v.get_boolean (); + // derive the uuid-related attributes we care about v = device_proxy.get_cached_property ("UUIDs"); uint16[] uuids = {}; @@ -344,6 +348,7 @@ public class Bluez: Bluetooth, Object icon, true, is_connected, + is_trusted, supports_browsing, supports_file_transfer)); @@ -415,6 +420,19 @@ public class Bluez: Bluetooth, Object } } + public void set_device_trusted (uint id, bool trusted) + { + var device = id_to_device.lookup (id); + var path = id_to_path.lookup (id); + var proxy = (path != null) ? path_to_device_proxy.lookup (path) : null; + + if ((device != null) + && (device.is_trusted != trusted)) + { + proxy.trusted = trusted; + } + } + public void try_set_discoverable (bool b) { if (discoverable != b) @@ -440,6 +458,11 @@ public class Bluez: Bluetooth, Object return id_to_device.get_values(); } + public Device get_device (ObjectPath path) + { + return id_to_device.lookup(path_to_id.lookup(path)); + } + public bool supported { get; protected set; default = false; } public bool discoverable { get; protected set; default = false; } public bool enabled { get; protected set; default = false; } @@ -509,6 +532,9 @@ private interface BluezDevice : DBusProxy { [DBus (name = "Disconnect")] public abstract void disconnect_() throws DBusError, IOError; + + [DBus (name = "Trusted")] + public abstract bool trusted { get; set; } } [DBus (name = "org.bluez.AgentManager1")] diff --git a/src/device.vala b/src/device.vala index 51bec03..e984a35 100644 --- a/src/device.vala +++ b/src/device.vala @@ -46,10 +46,11 @@ public class Device: Object public Icon icon { get; construct; } public bool is_connectable { get; construct; } public bool is_connected { get; construct; } + public bool is_trusted { get; construct; } public bool supports_browsing { get; construct; } public bool supports_file_transfer { get; construct; } public string print() { - return @"{id:$id, name:$name, address:$address, icon:$(icon.to_string()), device_type:$device_type, is_connectable:$is_connectable, is_connected:$is_connected, supports_browsing:$supports_browsing, supports_file_transfer:$supports_file_transfer}"; + return @"{id:$id, name:$name, address:$address, icon:$(icon.to_string()), device_type:$device_type, is_connectable:$is_connectable, is_connected:$is_connected, is_trusted:$is_trusted, supports_browsing:$supports_browsing, supports_file_transfer:$supports_file_transfer}"; } public Device (uint id, @@ -59,6 +60,7 @@ public class Device: Object Icon icon, bool is_connectable, bool is_connected, + bool is_trusted, bool supports_browsing, bool supports_file_transfer) { @@ -69,6 +71,7 @@ public class Device: Object icon: icon, is_connectable: is_connectable, is_connected: is_connected, + is_trusted: is_trusted, supports_browsing: supports_browsing, supports_file_transfer: supports_file_transfer); } -- cgit v1.2.3