/* * 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 General Public License for more details. * * You should have received a copy of the GNU 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 name_watch_id = 0; uint next_device_id = 1; ObjectManager manager; const string BLUEZ_BUSNAME = "org.bluez"; private bool _powered = false; private bool powered { get { return _powered; } set { _powered = value; update_enabled(); } } private KillSwitch killswitch = new RfKillSwitch (); private DBusConnection bus = null; /* maps an org.bluez.Adapter1's object_path to the BluezAdapter proxy */ private HashTable path_to_adapter_proxy; /* maps an org.bluez.Device1's object_path to the BluezDevice proxy */ private HashTable path_to_device_proxy; /* maps an org.bluez.Device1's object_path to our arbitrary unique id */ private HashTable path_to_id; /* maps our arbitrary unique id to an org.bluez.Device's object path */ private HashTable id_to_path; /* maps our arbitrary unique id to a Bluetooth.Device struct for public consumption */ private HashTable id_to_device; public Bluez (KillSwitch? killswitch) { init_bluez_state_vars (); if ((killswitch != null) && (killswitch.is_valid())) { this.killswitch = killswitch; killswitch.notify["blocked"].connect (() => update_enabled()); update_enabled (); } name_watch_id = Bus.watch_name(BusType.SYSTEM, BLUEZ_BUSNAME, BusNameWatcherFlags.AUTO_START, on_bluez_appeared, on_bluez_vanished); } ~Bluez() { Bus.unwatch_name(name_watch_id); } private void on_bluez_appeared (DBusConnection connection, string name, string name_owner) { debug(@"$name owned by $name_owner, setting up bluez proxies"); bus = connection; init_bluez_state_vars(); reset_manager(); } private void on_bluez_vanished (DBusConnection connection, string name) { debug(@"$name vanished from the bus"); reset_bluez(); } private void init_bluez_state_vars () { 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_adapter_proxy = new HashTable (str_hash, str_equal); path_to_device_proxy = new HashTable (str_hash, str_equal); } private void reset_bluez () { init_bluez_state_vars (); devices_changed (); update_combined_adapter_state (); update_connected (); update_enabled (); } private void reset_manager() { try { manager = bus.get_proxy_sync (BLUEZ_BUSNAME, "/"); // Find the adapters and watch for changes manager.interfaces_added.connect ((object_path, interfaces_and_properties) => { var iter = HashTableIter> (interfaces_and_properties); string name; while (iter.next (out name, null)) { if (name == "org.bluez.Adapter1") update_adapter (object_path); if (name == "org.bluez.Device1") update_device (object_path); } }); manager.interfaces_removed.connect ((object_path, interfaces) => { foreach (var interface in interfaces) { if (interface == "org.bluez.Adapter1") adapter_removed (object_path); if (interface == "org.bluez.Device1") device_removed (object_path); } }); var objects = manager.get_managed_objects (); var object_iter = HashTableIter>> (objects); ObjectPath object_path; HashTable> interfaces_and_properties; while (object_iter.next (out object_path, out interfaces_and_properties)) { var iter = HashTableIter> (interfaces_and_properties); string name; while (iter.next (out name, null)) { if (name == "org.bluez.Adapter1") update_adapter (object_path); if (name == "org.bluez.Device1") update_device (object_path); } } } catch (Error e) { critical (@"$(e.message)"); } } //// //// Adapter Upkeep //// private void update_adapter (ObjectPath object_path) { debug(@"bluez5 calling update_adapter for $object_path"); // Create a proxy if we don't have one var adapter_proxy = path_to_adapter_proxy.lookup (object_path); if (adapter_proxy == null) { try { adapter_proxy = bus.get_proxy_sync (BLUEZ_BUSNAME, object_path); } catch (Error e) { critical (@"$(e.message)"); return; } path_to_adapter_proxy.insert (object_path, adapter_proxy); adapter_proxy.g_properties_changed.connect(() => update_adapter (object_path)); } update_combined_adapter_state (); } private void adapter_removed (ObjectPath object_path) { path_to_adapter_proxy.remove (object_path); update_combined_adapter_state (); } private void update_combined_adapter_state () { var is_discoverable = false; var is_powered = false; var is_supported = false; var iter = HashTableIter (path_to_adapter_proxy); BluezAdapter adapter_proxy; while (iter.next (null, out adapter_proxy)) { var v = adapter_proxy.get_cached_property ("Discoverable"); if (!is_discoverable) is_discoverable = (v != null) && v.get_boolean (); v = adapter_proxy.get_cached_property ("Powered"); if (!is_powered) is_powered = (v != null) && v.get_boolean (); is_supported = true; } discoverable = is_discoverable; powered = is_powered; supported = is_supported; } //// //// 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; } //// //// Device Upkeep //// /* 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 (ObjectPath object_path) { debug(@"bluez5 calling update_device for $object_path"); // Create a proxy if we don't have one var device_proxy = path_to_device_proxy.lookup (object_path); if (device_proxy == null) { try { device_proxy = bus.get_proxy_sync (BLUEZ_BUSNAME, object_path); } catch (Error e) { critical (@"$(e.message)"); return; } path_to_device_proxy.insert (object_path, device_proxy); device_proxy.g_properties_changed.connect(() => update_device (object_path)); } // look up our id for this device. // if we don't have one yet, create one. 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 = device_proxy.get_cached_property ("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 = device_proxy.get_cached_property ("Alias"); if (v == null) v = device_proxy.get_cached_property ("Name"); var name = v == null ? _("Unknown") : v.get_string (); // look up the device's bus address v = device_proxy.get_cached_property ("Address"); var address = v == null ? null : v.get_string (); // look up the device's bus address v = device_proxy.get_cached_property ("Icon"); var icon = new ThemedIcon (v != null ? v.get_string() : "unknown"); // look up the device's Connected flag v = device_proxy.get_cached_property ("Connected"); var is_connected = (v != null) && v.get_boolean (); // derive the uuid-related attributes we care about v = device_proxy.get_cached_property ("UUIDs"); uint16[] uuids = {}; if (v != null) { string[] uuid_strings = v.dup_strv (); 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); id_to_device.insert (id, new Device (id, type, name, address, icon, true, is_connected, supports_browsing, supports_file_transfer)); devices_changed (); update_connected (); } private void device_removed (ObjectPath 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 (); } /* 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_device_proxy.lookup (path) : null; if ((device != null) && (device.is_connected != connected)) { try { if (connected) proxy.connect_ (); else proxy.disconnect_ (); } catch (Error e) { debug (@"$path org.bluez.Device1 connect/disconnect failed: $(e.message)"); } update_connected (); } } public void try_set_discoverable (bool b) { if (discoverable != b) { var iter = HashTableIter (path_to_adapter_proxy); ObjectPath object_path; BluezAdapter adapter_proxy; while (iter.next (out object_path, out adapter_proxy)) adapter_proxy.call.begin ("org.freedesktop.DBus.Properties.Set", new Variant ("(ssv)", "org.bluez.Adapter1", "Discoverable", new Variant.boolean (b)), DBusCallFlags.NONE, -1); } } 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 { var iter = HashTableIter (path_to_adapter_proxy); ObjectPath object_path; BluezAdapter adapter_proxy; while (iter.next (out object_path, out adapter_proxy)) adapter_proxy.call.begin ("org.freedesktop.DBus.Properties.Set", new Variant ("(ssv)", "org.bluez.Adapter1", "Powered", new Variant.boolean (b)), DBusCallFlags.NONE, -1); } } } [DBus (name = "org.freedesktop.DBus.ObjectManager")] private interface ObjectManager : Object { [DBus (name = "GetManagedObjects")] public abstract HashTable>> get_managed_objects() throws DBusError, IOError; [DBus (name = "InterfacesAdded")] public signal void interfaces_added(ObjectPath object_path, HashTable> interfaces_and_properties); [DBus (name = "InterfacesRemoved")] public signal void interfaces_removed(ObjectPath object_path, string[] interfaces); } [DBus (name = "org.bluez.Adapter1")] private interface BluezAdapter : DBusProxy { } [DBus (name = "org.bluez.Device1")] private interface BluezDevice : DBusProxy { [DBus (name = "Connect")] public abstract void connect_() throws DBusError, IOError; [DBus (name = "Disconnect")] public abstract void disconnect_() throws DBusError, IOError; }