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/service.vala | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) (limited to 'src/service.vala') diff --git a/src/service.vala b/src/service.vala index 80ccea6..b79056e 100644 --- a/src/service.vala +++ b/src/service.vala @@ -29,6 +29,7 @@ public class Service: Object private MainLoop loop; private SimpleActionGroup actions; private HashTable profiles; + private Bluetooth bluez; private DBusConnection connection; private uint exported_action_id; private const string OBJECT_PATH = "/org/ayatana/indicator/bluetooth"; @@ -52,6 +53,7 @@ public class Service: Object public Service (Bluetooth bluetooth) { actions = new SimpleActionGroup (); + bluez = bluetooth; profiles = new HashTable (str_hash, str_equal); profiles.insert ("phone", new Phone (bluetooth, actions)); @@ -74,15 +76,35 @@ public class Service: Object null, on_name_lost); + var system_name_id = Bus.own_name (BusType.SYSTEM, + "org.ayatana.indicator.bluetooth", + BusNameOwnerFlags.NONE, + on_system_bus_acquired, + null, + null); + loop = new MainLoop (null, false); loop.run (); // cleanup unexport (); Bus.unown_name (own_name_id); + Bus.unown_name (system_name_id); return Posix.EXIT_SUCCESS; } + void on_system_bus_acquired (DBusConnection connection, string name) + { + try + { + connection.register_object ("/agent", new Agent (bluez)); + } + catch (GLib.IOError pError) + { + warning ("Panic: Failed registering pairing agent: %s", pError.message); + } + } + void on_bus_acquired (DBusConnection connection, string name) { debug (@"bus acquired: $name"); -- 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/service.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/service.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