Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand Down
9 changes: 9 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ vala_init(ayatana-indicator-bluetooth-service
posix
gio-2.0
gio-unix-2.0
libnotify
AyatanaCommon
OPTIONS
--ccode
Expand Down Expand Up @@ -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
Expand Down
186 changes: 186 additions & 0 deletions src/agent.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
[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");
}

/* 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;

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 ();

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 GLib.DBusError, GLib.IOError
{
}

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, 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
{
sendNotification (bluetooth.get_device_name (object), @"Enter the PIN code $pincode on the other device", false, false);
}

public uint32 RequestPasskey (GLib.ObjectPath object) throws RejectedError, GLib.DBusError, GLib.IOError
{
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
{
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
}
6 changes: 6 additions & 0 deletions src/bluetooth.vala
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,10 @@ public interface Bluetooth: Object
/* 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);

public abstract string get_device_name (ObjectPath path);

public signal void agent_manager_ready ();

public abstract void add_agent (string path);
}
40 changes: 40 additions & 0 deletions src/bluez.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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 ();
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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<unowned Device> get_devices ()
{
return id_to_device.get_values();
Expand Down Expand Up @@ -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), 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")]
Expand All @@ -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;
}
54 changes: 53 additions & 1 deletion src/service.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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 ()
{
Expand All @@ -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));
Expand All @@ -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");
Expand All @@ -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)
{
Expand Down