From c1ce02f2b8cd198712606888d08b36f1e7aefe39 Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Mon, 5 Aug 2013 16:48:17 -0500 Subject: fully implement the bluez/device backend. in the desktop profile, add menuitems for the devices. --- src/Makefile.am | 8 +- src/bluetooth.vala | 191 +++++------------------- src/bluez.vala | 288 ++++++++++++++++++++++++++++++----- src/desktop.vala | 192 ++++++++++++++++++++++-- src/killswitch.vala | 7 +- src/main.vala | 2 +- src/phone.vala | 2 +- src/profile.vala | 4 +- src/service.vala | 421 ++-------------------------------------------------- 9 files changed, 489 insertions(+), 626 deletions(-) (limited to 'src') diff --git a/src/Makefile.am b/src/Makefile.am index b5aa8f1..87f4231 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -5,6 +5,7 @@ indicator_bluetooth_service_SOURCES = \ bluetooth.vala \ bluez.vala \ desktop.vala \ + device.vala \ main.vala \ phone.vala \ profile.vala \ @@ -15,18 +16,17 @@ indicator_bluetooth_service_VALAFLAGS = \ --ccode \ --vapidir=$(top_srcdir)/vapi/ \ --vapidir=./ \ - --pkg indicator3-0.4 \ - --pkg gnome-bluetooth-1.0 \ --pkg config \ --pkg rfkill \ --pkg posix \ --pkg glib-2.0 \ - --pkg gtk+-3.0 \ - --pkg Dbusmenu-0.4 + --pkg gtk+-3.0 +# -w to disable warnings for vala-generated code indicator_bluetooth_service_CFLAGS = \ -DGETTEXT_PACKAGE=\"$(GETTEXT_PACKAGE)\" \ -DLOCALE_DIR=\"$(datadir)/locale\" \ + -w \ $(INDICATOR_BLUETOOTH_SERVICE_CFLAGS) indicator_bluetooth_service_LDADD = \ diff --git a/src/bluetooth.vala b/src/bluetooth.vala index 1952d1c..98464a4 100644 --- a/src/bluetooth.vala +++ b/src/bluetooth.vala @@ -17,173 +17,64 @@ * Charles Kerr */ + /** - * Base class for the bluetooth backend. + * Abstract interface for the Bluetooth backend. */ -public class Bluetooth: Object +public interface Bluetooth: Object { - /* whether or not our system can be seen by other bluetooth devices */ - public bool discoverable { get; protected set; default = false; } - public virtual void try_set_discoverable (bool b) {} + /* True if there are any bluetooth adapters powered up on the system. + In short, whether or not this system's bluetooth is "on". */ + public abstract bool powered { get; protected set; } - /* whether or not there are any bluetooth adapters powered up on the system */ - public bool powered { get; protected set; default = false; } + /* True if our system can be seen by other bluetooth devices */ + public abstract bool discoverable { get; protected set; } + public abstract void try_set_discoverable (bool discoverable); - /* whether or not bluetooth's been disabled, - either by a software setting or physical hardware switch */ - public bool blocked { get; protected set; default = true; } - public virtual void try_set_blocked (bool b) { - killswitch.try_set_blocked (b); - } + /* True if bluetooth's blocked. This can be soft-blocked by software and + * hard-blocked physically, eg by a laptop's network killswitch */ + public abstract bool blocked { get; protected set; } - public class Device: Object { - public string name { get; construct; } - public bool supports_browsing { get; construct; } - public bool supports_file_transfer { get; construct; } - public Device (string name, - bool supports_browsing, - bool supports_file_transfer) { - Object (name: name, - supports_browsing: supports_browsing, - supports_file_transfer: supports_file_transfer); - } - } - - private static uint16 get_uuid16_from_uuid (string uuid) - { - uint16 uuid16; - - string[] tokens = uuid.split ("-", 1); - if (tokens.length > 0) - uuid16 = (uint16) uint64.parse ("0x"+tokens[0]); - else - uuid16 = 0; - - return uuid16; - } - - protected static bool uuid_supports_file_transfer (string uuid) - { - return get_uuid16_from_uuid (uuid) == 0x1105; // OBEXObjectPush - } - - protected static bool uuid_supports_browsing (string uuid) - { - return get_uuid16_from_uuid (uuid) == 0x1106; // OBEXFileTransfer - } - - public enum DeviceType - { - COMPUTER, - PHONE, - MODEM, - NETWORK, - HEADSET, - HEADPHONES, - VIDEO, - OTHER_AUDIO, - JOYPAD, - KEYPAD, - KEYBOARD, - TABLET, - MOUSE, - PRINTER, - CAMERA - } - - protected static DeviceType class_to_device_type (uint32 c) - { - switch ((c & 0x1f00) >> 8) - { - case 0x01: - return DeviceType.COMPUTER; + /* Try to block/unblock bluetooth. This can fail if it's overridden + by the system, eg by a laptop's network killswitch */ + public abstract void try_set_blocked (bool b); - case 0x02: - switch ((c & 0xfc) >> 2) - { - case 0x01: - case 0x02: - case 0x03: - case 0x05: - return DeviceType.PHONE; + /* Get a list of the Device structs that we know about */ + public abstract List get_devices (); - case 0x04: - return DeviceType.MODEM; - } - break; + /* Emitted when one or more of the devices is added, removed, or changed */ + public signal void devices_changed (); - case 0x03: - return DeviceType.NETWORK; - - case 0x04: - switch ((c & 0xfc) >> 2) - { - case 0x01: - case 0x02: - return DeviceType.HEADSET; - - case 0x06: - return DeviceType.HEADPHONES; - - case 0x0b: // vcr - case 0x0c: // video camera - case 0x0d: // camcorder - return DeviceType.VIDEO; - - default: - return DeviceType.OTHER_AUDIO; - } - //break; - - case 0x05: - switch ((c & 0xc0) >> 6) - { - case 0x00: - switch ((c & 0x1e) >> 2) - { - case 0x01: - case 0x02: - return DeviceType.JOYPAD; - } - break; - - case 0x01: - return DeviceType.KEYBOARD; - - case 0x02: - switch ((c & 0x1e) >> 2) - { - case 0x05: - return DeviceType.TABLET; - - default: - return DeviceType.MOUSE; - } - } - break; - - case 0x06: - if ((c & 0x80) != 0) - return DeviceType.PRINTER; - if ((c & 0x20) != 0) - return DeviceType.CAMERA; - break; - } - - return 0; - } + /* 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); +} - /*** - **** Killswitch Implementation - ***/ +/** + * Base class for Bluetooth objects that use a killswitch to implement + * the 'discoverable' property. + */ +public abstract class KillswitchBluetooth: Object, Bluetooth +{ private KillSwitch killswitch; - public Bluetooth (KillSwitch killswitch) + public KillswitchBluetooth (KillSwitch killswitch) { + // always sync our 'blocked' property with the one in killswitch this.killswitch = killswitch; blocked = killswitch.blocked; killswitch.notify["blocked"].connect (() => blocked = killswitch.blocked ); } + + public bool powered { get; protected set; default = false; } + public bool discoverable { get; protected set; default = false; } + public bool blocked { get; protected set; default = true; } + public void try_set_blocked (bool b) { killswitch.try_set_blocked (b); } + + // empty implementations + public abstract void try_set_discoverable (bool b); + public abstract List get_devices (); + public abstract void set_device_connected (uint device_key, bool connected); } diff --git a/src/bluez.vala b/src/bluez.vala index b0c8761..b4d7836 100644 --- a/src/bluez.vala +++ b/src/bluez.vala @@ -18,90 +18,295 @@ */ /** - * Bluetooth implementaion which uses bluez over dbus + * Bluetooth implementaion which uses org.bluez on DBus */ -public class Bluez: Bluetooth +public class Bluez: KillswitchBluetooth { private org.bluez.Manager manager; private org.bluez.Adapter default_adapter; - private HashTable devices; + private HashTable path_to_proxy; + private HashTable path_to_id; + private HashTable id_to_path; + private HashTable id_to_device; + private uint next_device_id = 1; public Bluez (KillSwitch killswitch) { base (killswitch); - string default_adapter_object_path = null; + string adapter_path = null; - this.devices = new HashTable(str_hash, str_equal); + id_to_path = new HashTable (direct_hash, direct_equal); + id_to_device = new HashTable (direct_hash, direct_equal); + path_to_id = new HashTable (str_hash, str_equal); + path_to_proxy = new HashTable (str_hash, str_equal); try { manager = Bus.get_proxy_sync (BusType.SYSTEM, "org.bluez", "/"); - manager.default_adapter_changed.connect ((object_path) => on_default_adapter_changed (object_path)); - default_adapter_object_path = manager.default_adapter (); + adapter_path = manager.default_adapter (); } catch (Error e) - { - critical ("%s", e.message); - } + { + critical (@"$(e.message)"); + } - on_default_adapter_changed (default_adapter_object_path); + on_default_adapter_changed (adapter_path); } private void on_default_adapter_changed (string? object_path) { if (object_path != null) try { - message ("using default adapter at %s", object_path); + message (@"using default adapter at $object_path"); default_adapter = Bus.get_proxy_sync (BusType.SYSTEM, "org.bluez", object_path); default_adapter.property_changed.connect(() => on_default_adapter_properties_changed()); + default_adapter.device_removed.connect((adapter, path) => { + var id = path_to_id.lookup (path); + path_to_id.remove (path); + id_to_path.remove (id); + id_to_device.remove (id); + devices_changed (); + }); + default_adapter.device_created.connect((adapter, path) => add_device (path)); - default_adapter.device_removed.connect((adapter, path) => devices.remove (path)); foreach (string device_path in default_adapter.list_devices()) add_device (device_path); } catch (Error e) { - critical ("%s", e.message); + critical (@"$(e.message)"); } this.on_default_adapter_properties_changed (); } - private void add_device (string object_path) + private static uint16 get_uuid16_from_uuid_string (string uuid) + { + uint16 uuid16; + + string[] tokens = uuid.split ("-", 1); + if (tokens.length > 0) + uuid16 = (uint16) uint64.parse ("0x"+tokens[0]); + else + uuid16 = 0; + + return uuid16; + } + + /* A device supports file transfer if OBEXObjectPush is in its uuid list */ + private bool device_supports_file_transfer (uint16[] uuids) + { + foreach (uint16 uuid16 in uuids) + if (uuid16 == 0x1105) // OBEXObjectPush + return true; + + return false; + } + + /* A device supports browsing if OBEXFileTransfer is in its uuid list */ + private bool device_supports_browsing (uint16[] uuids) { + foreach (uint16 uuid16 in uuids) + if (uuid16 == 0x1106) // OBEXFileTransfer + return true; + + return false; + } + + /* headsets, audio sinks, and input devices are connectable. + * + * TODO: this duplicates the behavior of the indicator from when it used + * gnome-bluetooth as a backend. Are there other interfaces we care about? */ + private DBusInterfaceInfo[] get_connectable_interfaces (DBusProxy device) + { + DBusInterfaceInfo[] connectable_interfaces = {}; + try { - org.bluez.Device device = Bus.get_proxy_sync (BusType.SYSTEM, "org.bluez", object_path); - message ("got device proxy for %s", object_path); - var properties = device.get_properties (); + var iname = "org.freedesktop.DBus.Introspectable.Introspect"; + var intro = device.call_sync (iname, null, DBusCallFlags.NONE, -1); - Variant v = properties.lookup ("Alias"); - if (v == null) - v = properties.lookup ("Name"); - string name = v == null ? _("Unknown") : v.get_string(); + if ((intro != null) && (intro.n_children() > 0)) + { + string xml = intro.get_child_value(0).get_string(); + var info = new DBusNodeInfo.for_xml (xml); + if (info != null) + { + foreach (DBusInterfaceInfo i in info.interfaces) + { + if ((i.name == "org.bluez.AudioSink") || + (i.name == "org.bluez.Headset") || + (i.name == "org.bluez.Input")) + { + connectable_interfaces += i; + } + } + } + } + } + catch (Error e) + { + critical (@"$(e.message)"); + } - bool supports_browsing = false; - v = properties.lookup ("UUIDs"); - message ("%s", v.print(true)); + return connectable_interfaces; + } - bool supports_file_transfer = false; + private bool device_is_connectable (DBusProxy device) + { + var connectable_interfaces = get_connectable_interfaces (device); + return connectable_interfaces.length > 0; + } - //protected static bool uuid_supports_file_transfer (string uuid) - //protected static bool uuid_supports_browsing (string uuid) + private void device_connect (DBusProxy proxy) + { + var connection = proxy.get_connection (); + var object_path = proxy.get_object_path (); - var dev = new Bluetooth.Device (name, - supports_browsing, - supports_file_transfer); - devices.insert (object_path, dev); - message ("devices.size() is %u", devices.size()); + foreach (var i in get_connectable_interfaces (proxy)) + { + try + { + debug (@"trying to connect to $object_path: $(i.name)"); + connection.call_sync ("org.bluez", + object_path, + i.name, + "Connect", + null, + null, + DBusCallFlags.NONE, + -1); + } + catch (Error e) + { + debug (@"Unable to call $(i.name).Connect() on $(proxy.get_object_path()): $(e.message)"); + } + } + } + + private void add_device (string object_path) + { + if (!path_to_proxy.contains (object_path)) + { + try + { + org.bluez.Device device = Bus.get_proxy_sync (BusType.SYSTEM, "org.bluez", object_path); + path_to_proxy.insert (object_path, device); + device.property_changed.connect(() => update_device (device)); + update_device (device); + } + catch (Error e) + { + critical (@"$(e.message)"); + } + } + } + + private void update_device (org.bluez.Device device_proxy) + { + HashTable properties; + + try { + properties = device_proxy.get_properties (); + } catch (Error e) { + critical (@"$(e.message)"); + return; + } + + // look up our id for this device. + // if we don't have one yet, create one. + var object_path = (device_proxy as DBusProxy).get_object_path(); + var id = path_to_id.lookup (object_path); + if (id == 0) + { + id = next_device_id ++; + id_to_path.insert (id, object_path); + path_to_id.insert (object_path, id); + } + + // look up the device's type + Device.Type type; + var v = properties.lookup ("Class"); + if (v == null) + type = Device.Type.OTHER; + else + type = Device.class_to_device_type (v.get_uint32()); + + // look up the device's human-readable name + v = properties.lookup ("Alias"); + if (v == null) + v = properties.lookup ("Name"); + string name = v == null ? _("Unknown") : v.get_string (); + + // look up the device's bus address + v = properties.lookup ("Address"); + string address = v.get_string (); + + // look up the device's bus address + Icon icon; + v = properties.lookup ("Icon"); + if (v == null) + icon = new ThemedIcon ("unknown"); + else + icon = new ThemedIcon (v.get_string()); + + // derive a Connectable flag for this device + var is_connectable = device_is_connectable (device_proxy as DBusProxy); + + // look up the device's Connected flag + v = properties.lookup ("Connected"); + bool is_connected = (v != null) && v.get_boolean (); + + // derive the uuid-related attributes we care about + v = properties.lookup ("UUIDs"); + string[] uuid_strings = v.dup_strv (); + uint16[] uuids = {}; + foreach (string s in uuid_strings) + uuids += get_uuid16_from_uuid_string (s); + var supports_browsing = device_supports_browsing (uuids); + var supports_file_transfer = device_supports_file_transfer (uuids); + + // update our lookup table with these new attributes + id_to_device.insert (id, new Device (id, + type, + name, + address, + icon, + is_connectable, + is_connected, + supports_browsing, + supports_file_transfer)); + + devices_changed (); + } + + public override void set_device_connected (uint id, bool connected) + { + var device = id_to_device.lookup (id); + var object_path = id_to_path.lookup (id); + var proxy = (object_path != null) ? path_to_proxy.lookup (object_path) : null; + + if ((proxy != null) && (device != null) && (device.is_connected != connected)) + { + if (connected) + { + device_connect (proxy as DBusProxy); + } + else // disconnect + { + try + { + proxy.disconnect (); + } + catch (Error e) + { + critical (@"Unable to disconnect $object_path: $(e.message)"); + } + } } - catch (Error e) - { - critical ("%s", e.message); - } } private void on_default_adapter_properties_changed () @@ -121,7 +326,7 @@ public class Bluez: Bluetooth } catch (Error e) { - critical ("%s", e.message); + critical (@"$(e.message)"); } this.powered = is_powered; @@ -136,7 +341,12 @@ public class Bluez: Bluetooth } catch (Error e) { - critical ("%s", e.message); + critical (@"$(e.message)"); } } + + public override List get_devices () + { + return id_to_device.get_values(); + } } diff --git a/src/desktop.vala b/src/desktop.vala index a7b8995..31fe9ac 100644 --- a/src/desktop.vala +++ b/src/desktop.vala @@ -19,11 +19,14 @@ class Desktop: Profile { + private uint idle_rebuild_id = 0; private Settings settings; private Bluetooth bluetooth; private SimpleAction root_action; private Action[] all_actions; + private Menu device_section; + private HashTable connect_actions; public override void add_actions_to_group (SimpleActionGroup group) { @@ -31,29 +34,136 @@ class Desktop: Profile group.insert (all_actions[i]); } + protected override void dispose () + { + if (idle_rebuild_id != 0) + { + Source.remove (idle_rebuild_id); + idle_rebuild_id = 0; + } + + base.dispose (); + } + public Desktop (Bluetooth bluetooth) { base ("desktop"); this.bluetooth = bluetooth; + connect_actions = new HashTable(direct_hash, direct_equal); + settings = new Settings ("com.canonical.indicator.bluetooth"); - root_action = new SimpleAction.stateful ("root-desktop", null, action_state_for_root()); + root_action = create_root_action (); all_actions = {}; all_actions += root_action; all_actions += create_enabled_action (bluetooth); all_actions += create_discoverable_action (bluetooth); - all_actions += create_settings_action (); all_actions += create_wizard_action (); + all_actions += create_browse_files_action (); + all_actions += create_send_file_action (); + all_actions += create_show_settings_action (); + + build_menu (); - bluetooth.notify.connect (() => update_root_action_state()); settings.changed["visible"].connect (()=> update_root_action_state()); + bluetooth.notify.connect (() => update_root_action_state()); + bluetooth.devices_changed.connect (()=> { + if (idle_rebuild_id == 0) + idle_rebuild_id = Idle.add (() => { + rebuild_device_section(); + idle_rebuild_id = 0; + return false; + }); + }); + } + + /// + /// MenuItems + /// + + MenuItem create_device_connection_menuitem (Device device) + { + var action_name = @"desktop-device-$(device.id)-connected"; + + var item = new MenuItem (_("Connection"), "indicator."+action_name); + item.set_attribute ("x-canonical-type", "s", "com.canonical.indicator.switch"); + + // if this doesn't already have an action, create one + if (!connect_actions.contains (device.id)) + { + debug (@"creating action for $action_name"); + var action = new SimpleAction.stateful (action_name, null, device.is_connected); + action.activate.connect (() => action.set_state (!action.get_state().get_boolean())); + action.notify["state"].connect (() => bluetooth.set_device_connected (device.id, action.get_state().get_boolean())); + connect_actions.insert (device.id, action); + all_actions += action; + } + else + { + debug (@"updating action $(device.id) state to $(device.is_connected)"); + var action = connect_actions.lookup (device.id); + action.set_state (device.is_connected); + } + + return item; + } + + void rebuild_device_section () + { + device_section.remove_all (); + + foreach (var device in bluetooth.get_devices()) + { + Menu submenu = new Menu (); + MenuItem item; + + if (device.is_connectable) + submenu.append_item (create_device_connection_menuitem (device)); + + if (device.supports_browsing) + submenu.append (_("Browse files…"), @"indicator.desktop-browse-files::$(device.address)"); + + if (device.supports_file_transfer) + submenu.append (_("Send files…"), @"indicator.desktop-send-file::$(device.address)"); + + switch (device.device_type) + { + case Device.Type.KEYBOARD: + submenu.append (_("Keyboard Settings…"), "indicator.desktop-show-settings::keyboard"); + break; + case Device.Type.MOUSE: + case Device.Type.TABLET: + submenu.append (_("Mouse and Touchpad Settings…"), "indicator.desktop-show-settings::mouse"); + break; + + case Device.Type.HEADSET: + case Device.Type.HEADPHONES: + case Device.Type.OTHER_AUDIO: + submenu.append (_("Sound Settings…"), "indicator.desktop-show-settings::sound"); + break; + } + + /* only show the device if it's got actions that we can perform on it */ + if (submenu.get_n_items () > 0) + { + item = new MenuItem (device.name, null); + item.set_attribute_value ("icon", device.icon.serialize()); + item.set_submenu (submenu); + device_section.append_item (item); + } + } + } + + void build_menu () + { Menu section; MenuItem item; + // quick toggles section section = new Menu (); item = new MenuItem ("Bluetooth", "indicator.desktop-enabled"); item.set_attribute ("x-canonical-type", "s", "com.canonical.indicator.switch"); @@ -63,12 +173,40 @@ class Desktop: Profile section.append_item (item); menu.append_section (null, section); + // devices section + device_section = new Menu (); + rebuild_device_section (); + menu.append_section (null, device_section); + + // settings section section = new Menu (); section.append (_("Set Up New Device…"), "indicator.desktop-wizard"); - section.append (_("Bluetooth Settings…"), "indicator.desktop-settings"); + section.append (_("Bluetooth Settings…"), "indicator.desktop-show-settings::bluetooth"); menu.append_section (null, section); } + /// + /// Action Helpers + /// + + void spawn_command_line_async (string command) + { + try { + Process.spawn_command_line_async (command); + } catch (Error e) { + warning ("unable to launch '$command': $(e.message)"); + } + } + + void show_control_center (string panel) + { + spawn_command_line_async ("gnome-control-center " + panel); + } + + /// + /// Actions + /// + Action create_enabled_action (Bluetooth bluetooth) { var action = new SimpleAction.stateful ("desktop-enabled", null, !bluetooth.blocked); @@ -89,15 +227,6 @@ class Desktop: Profile return action; } - void spawn_command_line_async (string command) - { - try { - Process.spawn_command_line_async (command); - } catch (Error e) { - warning ("unable to launch '%s': %s", command, e.message); - } - } - Action create_wizard_action () { var action = new SimpleAction ("desktop-wizard", null); @@ -105,10 +234,36 @@ class Desktop: Profile return action; } - Action create_settings_action () + Action create_browse_files_action () { - var action = new SimpleAction ("desktop-settings", null); - action.activate.connect (() => spawn_command_line_async ("gnome-control-center bluetooth")); + var action = new SimpleAction ("desktop-browse-files", VariantType.STRING); + action.activate.connect ((action, address) => { + var uri = @"obex://[$(address.get_string())]/"; + var file = File.new_for_uri (uri); + file.mount_enclosing_volume.begin (MountMountFlags.NONE, null, null, (obj, res) => { + try { + AppInfo.launch_default_for_uri (uri, null); + } catch (Error e) { + warning ("unable to launch '$uri': $(e.message)"); + } + }); + }); + return action; + } + + Action create_send_file_action () + { + var action = new SimpleAction ("desktop-send-file", VariantType.STRING); + action.activate.connect ((action, address) => { + spawn_command_line_async ("bluetooth-sendto --device=$(address.get_string())"); + }); + return action; + } + + Action create_show_settings_action () + { + var action = new SimpleAction ("desktop-show-settings", VariantType.STRING); + action.activate.connect ((action, panel) => show_control_center (panel.get_string())); return action; } @@ -143,6 +298,11 @@ class Desktop: Profile return builder.end (); } + SimpleAction create_root_action () + { + return new SimpleAction.stateful ("root-desktop", null, action_state_for_root()); + } + void update_root_action_state () { root_action.set_state (action_state_for_root ()); diff --git a/src/killswitch.vala b/src/killswitch.vala index cf5c6c8..fc7978c 100644 --- a/src/killswitch.vala +++ b/src/killswitch.vala @@ -22,8 +22,7 @@ * either by software (e.g., a session configuration setting) * or by hardware (e.g., user disabled it via a physical switch on her laptop). * - * The Bluetooth class uses this as a backend for its 'blocked' property. - * Other code can't even see this, so use Bluetooth.blocked instead. :) + * KillswitchBluetooth uses this as a backend for its Bluetooth.blocked property. */ public class KillSwitch: Object { @@ -50,7 +49,7 @@ public class RfKillSwitch: KillSwitch var bwritten = Posix.write (fd, &event, sizeof(Linux.RfKillEvent)); if (bwritten == -1) - warning ("Could not write rfkill event: %s", strerror(errno)); + warning (@"Could not write rfkill event: $(strerror(errno))"); } private class Entry @@ -90,7 +89,7 @@ public class RfKillSwitch: KillSwitch message ("fd is %d", fd); if (fd == -1) { - warning (@"Can't open $path: $(strerror(errno)); KillSwitch disable"); + warning (@"Can't open $path for use as a killswitch backend: $(strerror(errno))"); } else { diff --git a/src/main.vala b/src/main.vala index b88e94e..824e1d5 100644 --- a/src/main.vala +++ b/src/main.vala @@ -12,7 +12,7 @@ main (string[] args) var bluetooth = new Bluez (new RfKillSwitch ()); // start the service - var service = new BluetoothIndicator (bluetooth); + var service = new Service (bluetooth); service.run (); return Posix.EXIT_SUCCESS; diff --git a/src/phone.vala b/src/phone.vala index 8c9c816..34c10ab 100644 --- a/src/phone.vala +++ b/src/phone.vala @@ -48,7 +48,7 @@ class Phone: Profile try { Process.spawn_command_line_async ("system-settings bluetooth"); } catch (Error e) { - warning ("unable to launch settings: %s", e.message); + warning (@"unable to launch settings: $(e.message)"); } }); diff --git a/src/profile.vala b/src/profile.vala index 6dd5f52..360722d 100644 --- a/src/profile.vala +++ b/src/profile.vala @@ -43,12 +43,12 @@ class Profile: Object { try { - message ("exporting '%s' on %s", name, object_path); + debug (@"exporting '$name' on $object_path"); connection.export_menu_model (object_path, this.root); } catch (Error e) { - critical ("%s", e.message); + critical (@"Unable to export menu on $object_path: $(e.message)"); } } } diff --git a/src/service.vala b/src/service.vala index 10f7f25..0cece83 100644 --- a/src/service.vala +++ b/src/service.vala @@ -8,27 +8,13 @@ * See http://www.gnu.org/copyleft/gpl.html the full text of the license. */ -public class BluetoothIndicator +public class Service: Object { private MainLoop loop; private SimpleActionGroup actions; private HashTable profiles; - private DBusConnection bus; - private Indicator.Service indicator_service; - private Dbusmenu.Server menu_server; - private BluetoothService bluetooth_service; - private GnomeBluetooth.Client client; - private GnomeBluetooth.Killswitch killswitch; - private bool updating_killswitch = false; - private Dbusmenu.Menuitem enable_item; - private Dbusmenu.Menuitem visible_item; - private bool updating_visible = false; - private Dbusmenu.Menuitem devices_separator; - private List device_items; - private Dbusmenu.Menuitem menu; - - public BluetoothIndicator (Bluetooth bluetooth) + public Service (Bluetooth bluetooth) { profiles = new HashTable (str_hash, str_equal); profiles.insert ("phone", new Phone (bluetooth)); @@ -39,107 +25,6 @@ public class BluetoothIndicator profile.add_actions_to_group (actions); } - private void init_for_bus (DBusConnection bus) - { - this.bus = bus; - - /// - /// - - indicator_service = new Indicator.Service ("com.canonical.indicator.bluetooth.old"); - menu_server = new Dbusmenu.Server ("/com/canonical/indicator/bluetooth/menu"); - - bluetooth_service = new BluetoothService (); - bus.register_object ("/com/canonical/indicator/bluetooth/service", bluetooth_service); - - killswitch = new GnomeBluetooth.Killswitch (); - killswitch.state_changed.connect (killswitch_state_changed_cb); - - client = new GnomeBluetooth.Client (); - - menu = new Dbusmenu.Menuitem (); - menu_server.set_root (menu); - - enable_item = new Dbusmenu.Menuitem (); - enable_item.property_set (Dbusmenu.MENUITEM_PROP_LABEL, _("Bluetooth")); - enable_item.property_set (Dbusmenu.MENUITEM_PROP_TYPE, "x-canonical-switch"); - enable_item.item_activated.connect (() => - { - if (updating_killswitch) - return; - if (killswitch.state == GnomeBluetooth.KillswitchState.UNBLOCKED) - killswitch.state = GnomeBluetooth.KillswitchState.SOFT_BLOCKED; - else - killswitch.state = GnomeBluetooth.KillswitchState.UNBLOCKED; - }); - menu.child_append (enable_item); - - visible_item = new Dbusmenu.Menuitem (); - visible_item.property_set (Dbusmenu.MENUITEM_PROP_LABEL, _("Visible")); - visible_item.property_set (Dbusmenu.MENUITEM_PROP_TYPE, "x-canonical-switch"); - bool discoverable; - client.get ("default-adapter-discoverable", out discoverable); - visible_item.property_set_int (Dbusmenu.MENUITEM_PROP_TOGGLE_STATE, discoverable ? Dbusmenu.MENUITEM_TOGGLE_STATE_CHECKED : Dbusmenu.MENUITEM_TOGGLE_STATE_UNCHECKED); - client.notify["default-adapter-discoverable"].connect (() => - { - updating_visible = true; - bool is_discoverable; - client.get ("default-adapter-discoverable", out is_discoverable); - visible_item.property_set_int (Dbusmenu.MENUITEM_PROP_TOGGLE_STATE, is_discoverable ? Dbusmenu.MENUITEM_TOGGLE_STATE_CHECKED : Dbusmenu.MENUITEM_TOGGLE_STATE_UNCHECKED); - updating_visible = false; - }); - visible_item.item_activated.connect (() => - { - if (updating_visible) - return; - client.set ("default-adapter-discoverable", visible_item.property_get_int (Dbusmenu.MENUITEM_PROP_TOGGLE_STATE) != Dbusmenu.MENUITEM_TOGGLE_STATE_CHECKED); - }); - menu.child_append (visible_item); - - devices_separator = new Dbusmenu.Menuitem (); - devices_separator.property_set (Dbusmenu.MENUITEM_PROP_TYPE, Dbusmenu.CLIENT_TYPES_SEPARATOR); - menu.child_append (devices_separator); - - device_items = new List (); - - client.model.row_inserted.connect (device_changed_cb); - client.model.row_changed.connect (device_changed_cb); - client.model.row_deleted.connect (device_removed_cb); - Gtk.TreeIter iter; - var have_iter = client.model.get_iter_first (out iter); - while (have_iter) - { - Gtk.TreeIter child_iter; - var have_child_iter = client.model.iter_children (out child_iter, iter); - while (have_child_iter) - { - device_changed_cb (null, child_iter); - have_child_iter = client.model.iter_next (ref child_iter); - } - have_iter = client.model.iter_next (ref iter); - } - - var sep = new Dbusmenu.Menuitem (); - sep.property_set (Dbusmenu.MENUITEM_PROP_TYPE, Dbusmenu.CLIENT_TYPES_SEPARATOR); - menu.child_append (sep); - - var item = new Dbusmenu.Menuitem (); - item.property_set (Dbusmenu.MENUITEM_PROP_LABEL, _("Set Up New Device…")); - item.item_activated.connect (() => { set_up_new_device (); }); - menu.child_append (item); - - item = new Dbusmenu.Menuitem (); - item.property_set (Dbusmenu.MENUITEM_PROP_LABEL, _("Bluetooth Settings…")); - item.item_activated.connect (() => { show_control_center ("bluetooth"); }); - menu.child_append (item); - - killswitch_state_changed_cb (killswitch.state); - - client.adapter_model.row_inserted.connect (update_visible); - client.adapter_model.row_deleted.connect (update_visible); - update_visible (); - } - public int run () { if (this.loop != null) @@ -162,310 +47,28 @@ public class BluetoothIndicator void on_bus_acquired (DBusConnection connection, string name) { - stdout.printf ("bus acquired: %s\n", name); - - init_for_bus (connection); + debug (@"bus acquired: $name"); + var object_path = "/com/canonical/indicator/bluetooth"; try { - connection.export_action_group ("/com/canonical/indicator/bluetooth", this.actions); + connection.export_action_group (object_path, this.actions); } catch (Error e) { - critical ("%s", e.message); + critical (@"Unable to export actions on $object_path: $(e.message)"); } - this.profiles.@foreach ((name,profile) => profile.export_menu (connection, @"/com/canonical/indicator/bluetooth/$name")); + this.profiles.for_each ((name,profile) => { + var path = @"/com/canonical/indicator/bluetooth/$name"; + message (@"exporting menu '$path'"); + profile.export_menu (connection, path); + }); } void on_name_lost (DBusConnection connection, string name) { - stdout.printf ("name lost: %s\n", name); + debug (@"name lost: $name"); this.loop.quit (); } - - - private BluetoothMenuItem? find_menu_item (string address) - { - foreach (var item in device_items) - if (item.address == address) - return item; - - return null; - } - - private void device_changed_cb (Gtk.TreePath? path, Gtk.TreeIter iter) - { - /* Ignore adapters */ - Gtk.TreeIter parent_iter; - if (!client.model.iter_parent (out parent_iter, iter)) - return; - - DBusProxy proxy; - string address; - string alias; - GnomeBluetooth.Type type; - string icon; - bool connected; - HashTable services; - string[] uuids; - client.model.get (iter, - GnomeBluetooth.Column.PROXY, out proxy, - GnomeBluetooth.Column.ADDRESS, out address, - GnomeBluetooth.Column.ALIAS, out alias, - GnomeBluetooth.Column.TYPE, out type, - GnomeBluetooth.Column.ICON, out icon, - GnomeBluetooth.Column.CONNECTED, out connected, - GnomeBluetooth.Column.SERVICES, out services, - GnomeBluetooth.Column.UUIDS, out uuids); - - /* Skip if haven't actually got any information yet */ - if (proxy == null) - return; - - /* Find or create menu item */ - var item = find_menu_item (address); - if (item == null) - { - item = new BluetoothMenuItem (client, address); - item.property_set_bool (Dbusmenu.MENUITEM_PROP_VISIBLE, killswitch.state == GnomeBluetooth.KillswitchState.UNBLOCKED); - var last_item = devices_separator as Dbusmenu.Menuitem; - if (device_items != null) - last_item = device_items.last ().data; - device_items.append (item); - menu.child_add_position (item, last_item.get_position (menu) + 1); - } - - item.update (type, proxy, alias, icon, connected, services, uuids); - } - - private void update_visible () - { - bluetooth_service._visible = client.adapter_model.iter_n_children (null) > 0;// && settings.get_boolean ("visible"); - var builder = new VariantBuilder (VariantType.ARRAY); - builder.add ("{sv}", "Visible", new Variant.boolean (bluetooth_service._visible)); - try - { - var properties = new Variant ("(sa{sv}as)", "com.canonical.indicator.bluetooth.service", builder, null); - bus.emit_signal (null, - "/com/canonical/indicator/bluetooth/service", - "org.freedesktop.DBus.Properties", - "PropertiesChanged", - properties); - } - catch (Error e) - { - warning ("Failed to emit signal: %s", e.message); - } - } - - private void device_removed_cb (Gtk.TreePath path) - { - Gtk.TreeIter iter; - if (!client.model.get_iter (out iter, path)) - return; - - string address; - client.model.get (iter, GnomeBluetooth.Column.ADDRESS, out address); - - var item = find_menu_item (address); - if (item == null) - return; - - device_items.remove (item); - menu.child_delete (item); - } - - private void killswitch_state_changed_cb (GnomeBluetooth.KillswitchState state) - { - updating_killswitch = true; - - var enabled = state == GnomeBluetooth.KillswitchState.UNBLOCKED; - - bluetooth_service._icon_name = enabled ? "bluetooth-active" : "bluetooth-disabled"; - bluetooth_service._accessible_description = enabled ? _("Bluetooth: On") : _("Bluetooth: Off"); - - var builder = new VariantBuilder (VariantType.ARRAY); - builder.add ("{sv}", "IconName", new Variant.string (bluetooth_service._icon_name)); - builder.add ("{sv}", "AccessibleDescription", new Variant.string (bluetooth_service._accessible_description)); - try - { - var properties = new Variant ("(sa{sv}as)", "com.canonical.indicator.bluetooth.service", builder, null); - bus.emit_signal (null, - "/com/canonical/indicator/bluetooth/service", - "org.freedesktop.DBus.Properties", - "PropertiesChanged", - properties); - } - catch (Error e) - { - warning ("Failed to emit signal: %s", e.message); - } - - enable_item.property_set_int (Dbusmenu.MENUITEM_PROP_TOGGLE_STATE, enabled ? Dbusmenu.MENUITEM_TOGGLE_STATE_CHECKED : Dbusmenu.MENUITEM_TOGGLE_STATE_UNCHECKED); - - /* Disable devices when locked */ - visible_item.property_set_bool (Dbusmenu.MENUITEM_PROP_VISIBLE, enabled); - devices_separator.property_set_bool (Dbusmenu.MENUITEM_PROP_VISIBLE, enabled); - foreach (var item in device_items) - item.property_set_bool (Dbusmenu.MENUITEM_PROP_VISIBLE, enabled && item.get_children () != null); - - updating_killswitch = false; - } -} - -private class BluetoothMenuItem : Dbusmenu.Menuitem -{ - private GnomeBluetooth.Client client; - public string address; - private Dbusmenu.Menuitem? connect_item = null; - private bool make_submenu = false; - - public BluetoothMenuItem (GnomeBluetooth.Client client, string address) - { - this.client = client; - this.address = address; - } - - public void update (GnomeBluetooth.Type type, DBusProxy proxy, string alias, string icon, bool connected, HashTable? services, string[] uuids) - { - property_set (Dbusmenu.MENUITEM_PROP_LABEL, alias); - property_set (Dbusmenu.MENUITEM_PROP_ICON_NAME, icon); - if (connect_item != null) - connect_item.property_set_int (Dbusmenu.MENUITEM_PROP_TOGGLE_STATE, connected ? Dbusmenu.MENUITEM_TOGGLE_STATE_CHECKED : Dbusmenu.MENUITEM_TOGGLE_STATE_UNCHECKED); - - /* FIXME: Not sure if the GUI elements below can change over time */ - if (make_submenu) - return; - make_submenu = true; - - if (services != null) - { - connect_item = new Dbusmenu.Menuitem (); - connect_item.property_set (Dbusmenu.MENUITEM_PROP_LABEL, _("Connection")); - connect_item.property_set (Dbusmenu.MENUITEM_PROP_TYPE, "x-canonical-switch"); - connect_item.property_set_int (Dbusmenu.MENUITEM_PROP_TOGGLE_STATE, connected ? Dbusmenu.MENUITEM_TOGGLE_STATE_CHECKED : Dbusmenu.MENUITEM_TOGGLE_STATE_UNCHECKED); - connect_item.item_activated.connect (() => { connect_service (proxy.get_object_path (), connect_item.property_get_int (Dbusmenu.MENUITEM_PROP_TOGGLE_STATE) != Dbusmenu.MENUITEM_TOGGLE_STATE_CHECKED); }); - child_append (connect_item); - } - - var can_send = false; - var can_browse = false; - if (uuids != null) - { - for (var i = 0; uuids[i] != null; i++) - { -message ("alias %s uuid #%d: %s", alias, i, uuids[i]); - if (uuids[i] == "OBEXObjectPush") - can_send = true; - if (uuids[i] == "OBEXFileTransfer") - can_browse = true; - } - } - - if (can_send) - { - var send_item = new Dbusmenu.Menuitem (); - send_item.property_set (Dbusmenu.MENUITEM_PROP_LABEL, _("Send files…")); - send_item.item_activated.connect (() => { GnomeBluetooth.send_to_address (address, alias); }); - child_append (send_item); - } - - if (can_browse) - { - var browse_item = new Dbusmenu.Menuitem (); - browse_item.property_set (Dbusmenu.MENUITEM_PROP_LABEL, _("Browse files…")); - browse_item.item_activated.connect (() => { GnomeBluetooth.browse_address (null, address, Gdk.CURRENT_TIME, null); }); - child_append (browse_item); - } - - switch (type) - { - case GnomeBluetooth.Type.KEYBOARD: - var keyboard_item = new Dbusmenu.Menuitem (); - keyboard_item.property_set (Dbusmenu.MENUITEM_PROP_LABEL, _("Keyboard Settings…")); - keyboard_item.item_activated.connect (() => { show_control_center ("keyboard"); }); - child_append (keyboard_item); - break; - - case GnomeBluetooth.Type.MOUSE: - case GnomeBluetooth.Type.TABLET: - var mouse_item = new Dbusmenu.Menuitem (); - mouse_item.property_set (Dbusmenu.MENUITEM_PROP_LABEL, _("Mouse and Touchpad Settings…")); - mouse_item.item_activated.connect (() => { show_control_center ("mouse"); }); - child_append (mouse_item); - break; - - case GnomeBluetooth.Type.HEADSET: - case GnomeBluetooth.Type.HEADPHONES: - case GnomeBluetooth.Type.OTHER_AUDIO: - var sound_item = new Dbusmenu.Menuitem (); - sound_item.property_set (Dbusmenu.MENUITEM_PROP_LABEL, _("Sound Settings…")); - sound_item.item_activated.connect (() => { show_control_center ("sound"); }); - child_append (sound_item); - break; - } - - property_set_bool (Dbusmenu.MENUITEM_PROP_VISIBLE, get_children () != null); - } - - private void connect_service (string device, bool connect) - { - client.connect_service.begin (device, connect, null, (object, result) => - { - var connected = false; - try - { - connected = client.connect_service.end (result); - } - catch (Error e) - { - warning ("Failed to connect service: %s", e.message); - } - }); - } -} - -private void set_up_new_device () -{ - try - { - Process.spawn_command_line_async ("bluetooth-wizard"); - } - catch (GLib.SpawnError e) - { - warning ("Failed to open bluetooth-wizard: %s", e.message); - } -} - -private void show_control_center (string panel) -{ - try - { - Process.spawn_command_line_async ("gnome-control-center %s".printf (panel)); - } - catch (GLib.SpawnError e) - { - warning ("Failed to open control center: %s", e.message); - } -} - -[DBus (name = "com.canonical.indicator.bluetooth.service")] -private class BluetoothService : Object -{ - internal bool _visible = false; - public bool visible - { - get { return _visible; } - } - internal string _icon_name = "bluetooth-active"; - public string icon_name - { - get { return _icon_name; } - } - internal string _accessible_description = _("Bluetooth"); - public string accessible_description - { - get { return _accessible_description; } - } } -- cgit v1.2.3