diff options
| -rw-r--r-- | CMakeLists.txt | 1 | ||||
| -rw-r--r-- | src/CMakeLists.txt | 9 | ||||
| -rw-r--r-- | src/agent.vala | 249 | ||||
| -rw-r--r-- | src/bluetooth.vala | 12 | ||||
| -rw-r--r-- | src/bluez.vala | 66 | ||||
| -rw-r--r-- | src/device.vala | 5 | ||||
| -rw-r--r-- | src/service.vala | 54 |
7 files changed, 394 insertions, 2 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 49c6bb3..e954ecd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,6 +41,7 @@ pkg_check_modules( glib-2.0>=${GLIB_2_0_REQUIRED_VERSION} gio-unix-2.0>=${GIO_2_0_REQUIRED_VERSION} libayatana-common>=0.9.3 + libnotify ) include_directories(${BLUETOOTHSERVICE_INCLUDE_DIRS}) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6b0a1e2..d616baf 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -14,6 +14,7 @@ vala_init(ayatana-indicator-bluetooth-service posix gio-2.0 gio-unix-2.0 + libnotify AyatanaCommon OPTIONS --ccode @@ -94,6 +95,14 @@ vala_add(ayatana-indicator-bluetooth-service phone desktop greeter + agent +) + +vala_add(ayatana-indicator-bluetooth-service + agent.vala + DEPENDS + bluetooth + device ) vala_finish(ayatana-indicator-bluetooth-service diff --git a/src/agent.vala b/src/agent.vala new file mode 100644 index 0000000..7f517f0 --- /dev/null +++ b/src/agent.vala @@ -0,0 +1,249 @@ +[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; + + 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"); + } + + /* 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 = !have_actions; + + string header = (_("Pair with %s?").printf (device_name)); + notification = new Notify.Notification (header, body, "bluetooth-active"); + notification.closed.connect (() => { + accepted = false; + notification = null; + + if (loop.is_running ()) { + loop.quit (); + } + }); + + bool is_lomiri = 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 (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 ()); + } + } + + 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 { + notification.show (); + } + catch (Error e) { + warning ("Panic: Failed showing notification: %s", e.message); + } + + if (have_actions) { + loop.run (); + } + + return accepted; + } + + 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 + { + string body = (_("Are you sure you want to pair with PIN %06u?").printf (passkey)); + bool confirmed = sendNotification (bluetooth.get_device_name (object), body, false, true); + + 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?")), false, true); + + if (!authorized) { + throw new RejectedError.ERROR ("Rejected by user"); + } + } + + public string RequestPinCode (GLib.ObjectPath object) throws RejectedError, GLib.DBusError, GLib.IOError + { + 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 + { + string body = (_("Enter the PIN code %s on the other device").printf (pincode)); + sendNotification (bluetooth.get_device_name (object), body, false, false); + } + + public uint32 RequestPasskey (GLib.ObjectPath object) throws RejectedError, GLib.DBusError, GLib.IOError + { + 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.to_int (); + } + + public void DisplayPasskey (GLib.ObjectPath object, uint32 passkey, uint16 entered) throws GLib.DBusError, GLib.IOError + { + string body = (_("Enter the PIN %06u on the other device").printf (passkey)); + sendNotification (bluetooth.get_device_name (object), body, false, false); + } + + public void Cancel () throws GLib.DBusError, GLib.IOError + { + if (loop.is_running ()) { + loop.quit (); + } + + if (notification != null) { + notification.close (); + notification = null; + } + } + + public void Release () throws GLib.DBusError, GLib.IOError + { + } +} + +[DBus (name = "org.bluez.Error.Cancelled")] +public errordomain CancelledError { + ERROR +} + +[DBus (name = "org.bluez.Error.Rejected")] +public errordomain RejectedError { + ERROR +} diff --git a/src/bluetooth.vala b/src/bluetooth.vala index 0cc5432..47c6f8e 100644 --- a/src/bluetooth.vala +++ b/src/bluetooth.vala @@ -46,10 +46,22 @@ public interface Bluetooth: Object /* Get a list of the Device structs that we know about */ public abstract List<unowned Device> 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 (); /* Try to connect/disconnect a particular device. 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 (); + + public abstract void add_agent (string path); } diff --git a/src/bluez.vala b/src/bluez.vala index 16e237a..353b1a2 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<uint,Device> 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"); + agent_manager_ready (); // Find the adapters and watch for changes manager.interfaces_added.connect ((object_path, interfaces_and_properties) => { @@ -321,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 = {}; @@ -340,6 +348,7 @@ public class Bluez: Bluetooth, Object icon, true, is_connected, + is_trusted, supports_browsing, supports_file_transfer)); @@ -411,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) @@ -425,11 +447,22 @@ 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<unowned Device> get_devices () { 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; } @@ -453,6 +486,27 @@ public class Bluez: Bluetooth, Object DBusCallFlags.NONE, -1); } } + + public void add_agent(string path) + { + try + { + agent_manager.register_agent (new GLib.ObjectPath(path), AyatanaCommon.utils_is_lomiri() ? "KeyboardDisplay" : "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")] @@ -478,4 +532,16 @@ 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")] +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; } 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); } diff --git a/src/service.vala b/src/service.vala index 80ccea6..cff90d3 100644 --- a/src/service.vala +++ b/src/service.vala @@ -29,9 +29,16 @@ public class Service: Object private MainLoop loop; private SimpleActionGroup actions; private HashTable<string,Profile> 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 () { @@ -46,12 +53,28 @@ 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; + } } } - public Service (Bluetooth bluetooth) + public Service (Bluetooth bluetooth_service) { 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<string,Profile> (str_hash, str_equal); profiles.insert ("phone", new Phone (bluetooth, actions)); @@ -74,15 +97,39 @@ 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); + + bluetooth.agent_manager_ready.connect (() => { + bluetooth.add_agent (AGENT_OBJECT_PATH); + }); + 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_OBJECT_PATH, agent); + } + 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"); @@ -93,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) { |
