/* * Copyright 2013 Canonical Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . * * Authors: * Charles Kerr * Robert Ancell */ /** * Bluetooth implementaion which uses org.bluez on DBus */ public class Bluez: Bluetooth, Object { uint next_device_id = 1; org.bluez.Manager manager; org.bluez.Adapter default_adapter; private bool _powered = false; private bool powered { get { return _powered; } set { _powered = value; update_enabled(); } } private KillSwitch killswitch = null; private string adapter_path = null; private DBusConnection bus = null; /* maps an org.bluez.Device's object_path to the org.bluez.Device proxy */ HashTable path_to_proxy; /* maps an org.bluez.Device's object_path to our arbitrary unique id */ HashTable path_to_id; /* maps our arbitrary unique id to an org.bluez.Device's object path */ HashTable id_to_path; /* maps our arbitrary unique id to a Bluetooth.Device struct for public consumption */ HashTable id_to_device; public Bluez (KillSwitch? killswitch) { try { bus = Bus.get_sync (BusType.SYSTEM); } catch (Error e) { critical (@"$(e.message)"); } if ((killswitch != null) && (killswitch.is_valid())) { this.killswitch = killswitch; killswitch.notify["blocked"].connect (() => update_enabled()); update_enabled (); } 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); reset_manager (); } private void reset_manager () { string new_adapter_path = null; try { manager = bus.get_proxy_sync ("org.bluez", "/"); // if the default adapter changes, update our connections manager.default_adapter_changed.connect ((object_path) => on_default_adapter_changed (object_path)); // if the current adapter disappears, call clear_adapter() manager.adapter_removed.connect ((object_path) => { if (object_path == adapter_path) clear_adapter (); }); // get the current default adapter & watch for future default adapters new_adapter_path = manager.default_adapter (); } catch (Error e) { critical (@"$(e.message)"); } on_default_adapter_changed (new_adapter_path); } private void clear_adapter () { if (adapter_path != null) debug (@"clearing adapter; was using $adapter_path"); path_to_proxy.remove_all (); path_to_id.remove_all (); id_to_path.remove_all (); id_to_device.remove_all (); default_adapter = null; adapter_path = null; discoverable = false; powered = false; } void on_default_adapter_changed (string? object_path) { clear_adapter (); if (object_path != null) try { adapter_path = object_path; default_adapter = Bus.get_proxy_sync (BusType.SYSTEM, "org.bluez", adapter_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)); foreach (var device_path in default_adapter.list_devices ()) add_device (device_path); } catch (Error e) { critical (@"$(e.message)"); } on_default_adapter_properties_changed (); } /* When the default adapter's properties change, update our own properties "powered" and "discoverable" */ private void on_default_adapter_properties_changed () { bool is_discoverable = false; bool is_powered = false; if (default_adapter != null) try { var properties = default_adapter.get_properties (); var v = properties.lookup ("Discoverable"); is_discoverable = (v != null) && v.get_boolean (); v = properties.lookup ("Powered"); is_powered = (v != null) && v.get_boolean (); } catch (Error e) { critical (@"$(e.message)"); } powered = is_powered; discoverable = is_discoverable; } //// //// bluetooth device UUIDs //// 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 (var 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 (var uuid16 in uuids) if (uuid16 == 0x1106) // OBEXFileTransfer return true; return false; } //// //// Connectable Interfaces //// /* Headsets, Audio Sinks, and Input devices are connectable. * * This continues the behavior of the old gnome-bluetooth indicator. * But are there other interfaces we care about? */ private DBusInterfaceInfo[] get_connectable_interfaces (DBusProxy device) { DBusInterfaceInfo[] connectable_interfaces = {}; try { var iname = "org.freedesktop.DBus.Introspectable.Introspect"; var intro = device.call_sync (iname, null, DBusCallFlags.NONE, -1); if ((intro != null) && (intro.n_children() > 0)) { var xml = intro.get_child_value(0).get_string(); var info = new DBusNodeInfo.for_xml (xml); if (info != null) { foreach (var 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)"); } return connectable_interfaces; } private bool device_is_connectable (DBusProxy device) { return get_connectable_interfaces (device).length > 0; } // call "Connect" on the specified interface private void device_connect_on_interface (DBusProxy proxy, string interface_name) { var object_path = proxy.get_object_path (); debug (@"trying to connect to $object_path: $(interface_name)"); try { bus.call_sync ("org.bluez", object_path, interface_name, "Connect", null, null, DBusCallFlags.NONE, -1); } catch (Error e) { debug (@"$object_path $interface_name.Connect() failed: $(e.message)"); } } private void device_connect (org.bluez.Device device) { DBusProxy proxy = device as DBusProxy; // call "Connect" on all the interfaces that support it foreach (var i in get_connectable_interfaces (proxy)) device_connect_on_interface (proxy, i.name); } private void device_disconnect (org.bluez.Device device) { try { device.disconnect (); } catch (Error e) { var object_path = (device as DBusProxy).get_object_path (); critical (@"Unable to disconnect $object_path: $(e.message)"); } } //// //// Device Upkeep //// 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)"); } } } /* Update our public Device struct from the org.bluez.Device's properties. * * This is called when we first walk through bluez' Devices on startup, * when the org.bluez.Adapter gets a new device, * and when a device's properties change s.t. we need to rebuild the proxy. */ 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"); var name = v == null ? _("Unknown") : v.get_string (); // look up the device's bus address v = properties.lookup ("Address"); var address = v.get_string (); // look up the device's bus address v = properties.lookup ("Icon"); var icon = new ThemedIcon (v != null ? v.get_string() : "unknown"); // 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"); var 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 (var 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 (); update_connected (); } /* update the 'enabled' property by looking at the killswitch state and the 'powered' property state */ void update_enabled () { var blocked = (killswitch != null) && killswitch.blocked; debug (@"in upate_enabled, powered is $powered, blocked is $blocked"); enabled = powered && !blocked; } private bool have_connected_device () { var devices = get_devices(); foreach (var device in devices) if (device.is_connected) return true; return false; } private void update_connected () { connected = have_connected_device (); } //// //// Public API //// public void set_device_connected (uint id, bool connected) { var device = id_to_device.lookup (id); var path = id_to_path.lookup (id); var proxy = (path != null) ? path_to_proxy.lookup (path) : null; if ((proxy != null) && (device != null) && (device.is_connected != connected)) { if (connected) device_connect (proxy); else device_disconnect (proxy); update_connected (); } } public void try_set_discoverable (bool b) { if (discoverable != b) { default_adapter.set_property.begin ("Discoverable", new Variant.boolean (b)); } } public List get_devices () { return id_to_device.get_values(); } public bool supported { get; protected set; default = false; } public bool discoverable { get; protected set; default = false; } public bool enabled { get; protected set; default = false; } public bool connected { get; protected set; default = false; } public void try_set_enabled (bool b) { if (killswitch != null) { debug (@"setting killswitch blocked to $(!b)"); killswitch.try_set_blocked (!b); } else if (default_adapter != null) { debug (@"setting bluez Adapter's Powered property to $b"); default_adapter.set_property.begin ("Powered", new Variant.boolean (b)); powered = b; } } }