aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt1
-rw-r--r--src/CMakeLists.txt9
-rw-r--r--src/agent.vala249
-rw-r--r--src/bluetooth.vala12
-rw-r--r--src/bluez.vala66
-rw-r--r--src/device.vala5
-rw-r--r--src/service.vala54
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)
{