diff options
author | Matthieu Bouron <matthieu.bouron@collabora.com> | 2012-08-21 16:49:44 +0530 |
---|---|---|
committer | Philip Withnall <philip.withnall@collabora.co.uk> | 2013-10-31 20:04:38 +0000 |
commit | 659a5c4539912bb3b31f7e55e48e6fd4215257f8 (patch) | |
tree | 2ba56888cd6064f59d0cbd49c40dc6caafb3f57a | |
parent | 1af18e6813cf1cb2e6319f03b7f5dd9bf22587bd (diff) |
bluez: Add a Bluetooth Phonebook Access Profile backend using BlueZ 5
This pulls contacts out of a paired Bluetooth device and dumps them in
folks.
No test cases are included.
https://bugzilla.gnome.org/show_bug.cgi?id=685848
This bumps the Vala and GLib dependencies of folks, needed for the following
two fixes.
• https://bugzilla.gnome.org/show_bug.cgi?id=710643
• https://bugzilla.gnome.org/show_bug.cgi?id=710726
https://bugzilla.gnome.org/show_bug.cgi?id=685848
-rw-r--r-- | NEWS | 5 | ||||
-rw-r--r-- | backends/Makefile.am | 5 | ||||
-rw-r--r-- | backends/bluez/Makefile.am | 41 | ||||
-rw-r--r-- | backends/bluez/bluez-backend-factory.vala | 73 | ||||
-rw-r--r-- | backends/bluez/bluez-backend.vala | 636 | ||||
-rw-r--r-- | backends/bluez/bluez-persona-store.vala | 843 | ||||
-rw-r--r-- | backends/bluez/bluez-persona.vala | 312 | ||||
-rw-r--r-- | backends/bluez/org-bluez-obex-client.vala | 99 | ||||
-rw-r--r-- | backends/bluez/org-bluez.vala | 116 | ||||
-rw-r--r-- | configure.ac | 28 | ||||
-rw-r--r-- | folks/build-conf.vapi | 3 | ||||
-rw-r--r-- | po/POTFILES.in | 2 | ||||
-rw-r--r-- | po/POTFILES.skip | 2 |
13 files changed, 2162 insertions, 3 deletions
@@ -2,9 +2,11 @@ Overview of changes from libfolks 0.9.5 to libfolks 0.9.6 ========================================================= Dependencies: -• GLib ≥ 2.37.6 +• GLib ≥ 2.39.0 +• Vala ≥ 0.22.0.28-9090 Major changes: +• Add a BlueZ backend Bugs fixed: • Bug 706683 — fails to build with Vala 0.20 @@ -26,6 +28,7 @@ Bugs fixed: • Bug 710869 — Disable some GCC warnings for generated C code • Bug 708059 — build failure: fatal error: folks/folks.h: No such file or directory +• Bug 685848 — Add a folks backend for bluez phonebook access API changes: diff --git a/backends/Makefile.am b/backends/Makefile.am index 0da47738..f3d5e18d 100644 --- a/backends/Makefile.am +++ b/backends/Makefile.am @@ -22,7 +22,12 @@ if ENABLE_OFONO SUBDIRS += ofono endif +if ENABLE_BLUEZ +SUBDIRS += bluez +endif + DIST_SUBDIRS = \ + bluez \ eds \ key-file \ libsocialweb \ diff --git a/backends/bluez/Makefile.am b/backends/bluez/Makefile.am new file mode 100644 index 00000000..d1a39647 --- /dev/null +++ b/backends/bluez/Makefile.am @@ -0,0 +1,41 @@ +BACKEND_NAME = "bluez" + +backenddir = $(BACKEND_DIR)/bluez +backend_LTLIBRARIES = bluez.la + +bluez_la_VALAFLAGS = \ + $(backend_valaflags) \ + --pkg libebook-1.2 \ + $(NULL) + +bluez_la_SOURCES = \ + $(backend_sources) \ + bluez-backend.vala \ + bluez-backend-factory.vala \ + bluez-persona.vala \ + bluez-persona-store.vala \ + org-bluez-obex-client.vala \ + org-bluez.vala \ + $(NULL) + +bluez_la_CPPFLAGS = \ + $(backend_cppflags) \ + $(NULL) + +bluez_la_CFLAGS = \ + $(backend_cflags) \ + $(EBOOK_CFLAGS) \ + $(NULL) + +bluez_la_LIBADD = \ + $(backend_libadd) \ + $(EBOOK_LIBS) \ + $(NULL) + +bluez_la_LDFLAGS = \ + -module -avoid-version \ + $(backend_ldflags) \ + $(NULL) + +-include $(top_srcdir)/backends/backend.mk +-include $(top_srcdir)/git.mk diff --git a/backends/bluez/bluez-backend-factory.vala b/backends/bluez/bluez-backend-factory.vala new file mode 100644 index 00000000..5e139683 --- /dev/null +++ b/backends/bluez/bluez-backend-factory.vala @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2009 Zeeshan Ali (Khattak) <zeeshanak@gnome.org>. + * Copyright (C) 2009 Nokia Corporation. + * Copyright (C) 2012-2013 Collabora Ltd. + * + * This library is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 library. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Arun Raghavan <arun.raghavan@collabora.co.uk> + * + * Based on kf-backend-factory.vala by: + * Zeeshan Ali (Khattak) <zeeshanak@gnome.org> + * Travis Reitter <travis.reitter@collabora.co.uk> + * Philip Withnall <philip.withnall@collabora.co.uk> + */ + +using Folks; +using Folks.Backends.BlueZ; + +private BackendFactory _backend_factory = null; + +/** + * The backend module entry point. + * + * @param backend_store the {@link BackendStore} to use in this factory. + * + * @since UNRELEASED + */ +public void module_init (BackendStore backend_store) +{ + _backend_factory = new BackendFactory (backend_store); +} + +/** + * The backend module exit point. + * + * @param backend_store the {@link BackendStore} to use in this factory. + * + * @since UNRELEASED + */ +public void module_finalize (BackendStore backend_store) +{ + _backend_factory = null; +} + +/** + * A backend factory to create a single {@link Backend}. + * + * @since UNRELEASED + */ +public class Folks.Backends.BlueZ.BackendFactory : Object +{ + /** + * {@inheritDoc} + * + * @since UNRELEASED + */ + public BackendFactory (BackendStore backend_store) + { + backend_store.add_backend (new Backend ()); + } +} diff --git a/backends/bluez/bluez-backend.vala b/backends/bluez/bluez-backend.vala new file mode 100644 index 00000000..3f194401 --- /dev/null +++ b/backends/bluez/bluez-backend.vala @@ -0,0 +1,636 @@ +/* + * Copyright (C) 2012-2013 Collabora Ltd. + * + * This library is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 library. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Arun Raghavan <arun.raghavan@collabora.co.uk> + * Jeremy Whiting <jeremy.whiting@collabora.com> + * Simon McVittie <simon.mcvittie@collabora.co.uk> + * Gustavo Padovan <gustavo.padovan@collabora.co.uk> + * Matthieu Bouron <matthieu.bouron@collabora.com> + * Philip Withnall <philip.withnall@collabora.co.uk> + * + * Based on kf-backend.vala by: + * Philip Withnall <philip.withnall@collabora.co.uk> + */ + +using GLib; +using Gee; +using Folks; +using Folks.Backends.BlueZ; +using org.bluez; + +extern const string BACKEND_NAME; + +/** + * Errors from the BlueZ {@link Backend}. + * + * @since UNRELEASED + */ +public errordomain Folks.Backends.BlueZ.BackendError +{ + /** + * A required D-Bus service couldn’t be connected to. + * + * @since UNRELEASED + */ + NO_DBUS_SERVICE +} + +/** + * A backend which loads {@link Persona}s from paired Bluetooth + * devices using the Phonebook Access Protocol (PBAP) and presents them + * using one {@link PersonaStore} per device. + * + * Each device can be in four states: + * - Unpaired and unconnected + * - Unpaired but connected + * - Paired but unconnected + * - Paired and connected + * + * The default state for a device is unpaired. The user must explicitly pair + * their device before folks will begin to use it — folks ignores unpaired + * devices. Once a device is paired, folks will attempt to do an OBEX PBAP + * transfer to copy the device’s address book; this will automatically connect + * the device. After the transfer is complete, the device will go back to being + * paired and unconnected. + * + * Every time the user explicitly connects to the device, folks will re-download + * its address book. Currently, folks will not otherwise re-download it (i.e. + * there are no change notifications and no polling). + * + * If a transfer is started from an unpaired device, the device will move to the + * unpaired but connected state, and will pop up a notification asking the user + * whether they want to pair to the computer. This should be avoided, and is why + * folks ignores all unpaired devices. + * + * If a connection timeout occurs (e.g. because the user took too long to + * approve a pairing request, or explicitly denied it), the device will become + * disconnected again. + * + * If the phone user explicitly denies the phone’s request to share address book + * data with the laptop (which happens after pairing is successful), creating an + * OBEX transfer session will fail with an explicit error, which is handled in + * the {@link PersonaStore}. + * + * No caching is implemented by libfolks at the moment, so the address book + * will be downloaded every time folks starts up. + * + * Each device can be advertised by BlueZ as trusted or untrusted, a property + * which is explicitly set by the user on the laptop (not on the device). Folks + * will set the PersonaStore’s trust level appropriately, fully trusting devices + * marked as trusted, and only partially trusting others. + * + * @since UNRELEASED + */ +public class Folks.Backends.BlueZ.Backend : Folks.Backend +{ + private bool _is_prepared = false; + private bool _prepare_pending = false; /* used for unprepare() too */ + private bool _is_quiescent = false; + /* Map from PersonaStore.id to PersonaStore. */ + private HashMap<string, PersonaStore> _persona_stores; + private Map<string, PersonaStore> _persona_stores_ro; + private DBusObjectManagerClient? _manager; /* null before prepare() */ + private ulong _object_added_handler; + private ulong _object_removed_handler; + private ulong _properties_changed_handler; + /* Map from device D-Bus object path to PersonaStore. */ + private HashMap<string, PersonaStore> _watched_devices; + private org.bluez.obex.Client? _obex_client = null; + + /** + * Whether this Backend has been prepared. + * + * See {@link Folks.Backend.is_prepared}. + * + * @since UNRELEASED + */ + public override bool is_prepared + { + get { return this._is_prepared; } + } + + /** + * Whether this Backend has reached a quiescent state. + * + * See {@link Folks.Backend.is_quiescent}. + * + * @since UNRELEASED + */ + public override bool is_quiescent + { + get { return this._is_quiescent; } + } + + /** + * {@inheritDoc} + * + * @since UNRELEASED + */ + public override string name { get { return BACKEND_NAME; } } + + /** + * {@inheritDoc} + * + * @since UNRELEASED + */ + public override Map<string, PersonaStore> persona_stores + { + get { return this._persona_stores_ro; } + } + + /** + * {@inheritDoc} + * + * This method actually does nothing because the backend can't + * programmatically disable a persona store since it can only + * be disabled if the corresponding device is unpaired by the + * user. + * + * @since UNRELEASED + */ + public override void disable_persona_store (Folks.PersonaStore store) + { + } + + /** + * {@inheritDoc} + * + * This method actually does nothing because the backend can't + * programmatically add a new persona store since it depends + * on new paired devices. + * + * @since UNRELEASED + */ + public override void enable_persona_store (Folks.PersonaStore store) + { + } + + /** + * {@inheritDoc} + * + * This method actually does nothing because the backend can't + * programmatically add or remove persona stores since it depends + * on paired/unpaired devices. + * + * @since UNRELEASED + */ + public override void set_persona_stores (Set<string>? storeids) + { + } + + /** + * {@inheritDoc} + * + * @since UNRELEASED + */ + public Backend () + { + Object (); + } + + construct + { + this._persona_stores = new HashMap<string, PersonaStore> (); + this._persona_stores_ro = this._persona_stores.read_only_view; + this._watched_devices = new HashMap<string, PersonaStore> (); + } + + /** + * Callback executed when a device property has changed. + * + * The callback is executed when a PropertiesChanged signal is received + * on device. If the device is seen as connected it tries to update the + * Persona store associated with it. If the device is seen as disconnected, + * the OBEX session used by the {@link PersonaStore} is removed. + * + * @param obj_proxy D-Bus proxy for the object + * @param iface_proxy D-Bus proxy for the interface on which the property + * changed + * @param changed the list of properties that have changed + * @param invalidated the list of properties that have been invalidated + * + * @since UNRELEASED + */ + private void _device_properties_changed_cb (DBusObjectProxy obj_proxy, + DBusProxy iface_proxy, Variant changed, string[] invalidated) + { + debug ("Properties changed on interface ‘%s’ of object ‘%s’:", + iface_proxy.g_interface_name, obj_proxy.g_object_path); + var iter = changed.iterator (); + string key; + Variant variant; + while (iter.next ("{sv}", out key, out variant) == true) + debug (" %s", key); + + if (iface_proxy.g_interface_name != "org.bluez.Device1") + return; + + var device = (Device) iface_proxy; + + /* UUIDs and Paired properties. Both affect whether we add or remove a + * device/persona store. */ + var uuids = changed.lookup_value ("UUIDs", null); + var paired = changed.lookup_value ("Paired", VariantType.BOOLEAN); + if (uuids != null || paired != null) + { + /* Sometimes the UUIDs property only changes a second or two after + * the device first appears, so try adding the device again. */ + if (device.paired == true && this._device_supports_pbap_pse (device)) + { + this._add_device.begin (obj_proxy, (o, r) => + { + this._add_device.end (r); + }); + } + else + { + this._remove_device.begin (obj_proxy, (o, r) => + { + this._remove_device.end (r); + }); + } + } + + var store = this._persona_stores.get (device.address); + + if (store == null) + return; + + /* Connected property. */ + var connected = changed.lookup_value ("Connected", VariantType.BOOLEAN); + if (connected != null) + { + store.set_connection_state.begin (connected.get_boolean (), (o, r) => + { + try + { + store.set_connection_state.end (r); + } + catch (IOError e1) + { + debug ("Changing connection state for device ‘%s’ (%s) " + + "was cancelled.", device.alias, device.address); + } + catch (PersonaStoreError e2) + { + warning ("Error changing connection state for device " + + "‘%s’ (%s): %s", device.alias, device.address, + e2.message); + } + }); + } + + /* Trust level. */ + var trusted = changed.lookup_value ("Trusted", VariantType.BOOLEAN); + if (trusted != null) + { + store.set_is_trusted (trusted.get_boolean ()); + } + + /* Alias. */ + var alias = changed.lookup_value ("Alias", VariantType.STRING); + if (alias != null) + { + store.set_alias (alias.get_string ()); + } + } + + /** + * Add a new Persona store to this backend. + * + * Add a new Persona store associated with a device identified by + * its address and alias. The function takes care of creating all + * the D-Bus object and path required by the Personna store. + * + * @param device the D-Bus object for the Bluetooth device + * @param path the path of the D-Bus device object. + * + * @since UNRELEASED + */ + private async void _add_persona_store (Device device, string path) + { + PersonaStore store = + new BlueZ.PersonaStore (device, path, this._obex_client); + + this._watched_devices[path] = store; + this._persona_stores.set (store.id, store); + + store.removed.connect (this._persona_store_removed_cb); + this.persona_store_added (store); + this.notify_property ("persona-stores"); + } + + private void _remove_persona_store (PersonaStore store) + { + store.removed.disconnect (this._persona_store_removed_cb); + + this.persona_store_removed (store); + + this._persona_stores.unset (store.id); + this._watched_devices.unset (store.object_path); + + this.notify_property ("persona-stores"); + } + + /** + * Check if a device supports PSE (Phone Book Server Equipment. + * + * We assume that UUIDs won’t change after we initially see the device, so + * don’t listen for changes to it. + * + * @param device the D-Bus device object + * @return ``true`` if the device supports PSE, ``false`` otherwise. + * + * @since UNRELEASED + */ + private bool _device_supports_pbap_pse (Device device) + { + string[]? uuids = device.uuids; + + /* The UUIDs property is optional; if unset, it’s null. */ + if (uuids == null) + return false; + + foreach (var uuid in (!) uuids) + { + /* Phonebook Access - PSE (Phone Book Server Equipment). + * 0x112F is the pse part. */ + if (uuid == "0000112f-0000-1000-8000-00805f9b34fb") + return true; + } + + return false; + } + + /** + * Add a device to the backend. + * + * @param _obj the device's D-Bus object + * + * @since UNRELEASED + */ + private async void _add_device (DBusObject obj) + { + debug ("Adding device at path ‘%s’.", obj.get_object_path ()); + + var device = obj.get_interface ("org.bluez.Device1") as Device; + if (device == null) + { + debug (" Device doesn’t implement org.bluez.Device1 " + + "interface. Ignoring."); + return; + } + + var path = obj.get_object_path (); + + if (this._watched_devices.has_key (path)) + { + debug (" Device already watched. Ignoring."); + return; + } + + if (device.paired == false) + { + debug (" Device isn’t paired. Ignoring. Manually pair the device" + + " to start downloading contacts."); + return; + } + + if (!this._device_supports_pbap_pse (device)) + { + debug (" Doesn’t support PBAP PSE. Ignoring."); + return; + } + + yield this._add_persona_store (device, path); + } + + /** + * Remove a device from the backend. + * + * @param obj the device's D-Bus object + * + * @since UNRELEASED + */ + private async void _remove_device (DBusObject obj) + { + var path = obj.get_object_path (); + PersonaStore? store = null; + + debug ("Removing device at ‘%s’.", path); + + if (this._watched_devices.unset (path, out store) == true) + { + debug ("Device ‘%s’ removed", path); + this._remove_persona_store (store); + } + } + + private delegate Type TypeFunc (); + + /** + * {@inheritDoc} + * + * @since UNRELEASED + */ + public override async void prepare () throws BackendError + { + Internal.profiling_start ("preparing BlueZ.Backend"); + + if (this._is_prepared || this._prepare_pending) + { + return; + } + + /* In brief, this function: + * 1. Connects to org.bluez. If that’s not available, we assume BlueZ + * is not installed (or is not version 5), and throw an error, leaving + * the store unprepared. + * 2. Connects to org.bluez.obex. Similarly, if that’s not available, + * we throw an error and leave the store unprepared. + * 3. Connects to loads of signals and enumerates all the existing + * devices known to BlueZ. This cannot fail. + */ + try + { + this._prepare_pending = true; + + try + { + this._manager = + yield DBusObjectManagerClient.new_for_bus (BusType.SYSTEM, + DBusObjectManagerClientFlags.NONE, "org.bluez", "/", + /* DBusProxyTypeFunc: */ + (manager, path, iface_name) => + { + debug ("DBusProxyTypeFunc for path ‘%s’ and " + + "interface ‘%s’.", path, iface_name); + + Type retval; + + /* FIXME: Horrible hack to grab the proxy object for + * org.bluez.Device (rather than the interface itself) + * from Vala. Vala generates C code for both, but we + * can’t normally access the proxy object. + * + * See: + * https://bugzilla.gnome.org/show_bug.cgi?id=710817 + */ + if (iface_name == "org.bluez.Device1") + { + var q = + Quark.from_string ("vala-dbus-proxy-type"); + var dev_type = typeof (org.bluez.Device); + retval = ((TypeFunc) (dev_type.get_qdata (q))) (); + } + /* Fallback. */ + else if (iface_name == null) + retval = typeof (DBusObjectProxy); + else + retval = typeof (DBusProxy); + + debug (" Returning: %s", retval.name ()); + + return retval; + }); + } + catch (GLib.Error e1) + { + throw new BackendError.NO_DBUS_SERVICE ( + _("No BlueZ 5 object manager running, so the BlueZ " + + "backend will be inactive. Either your BlueZ " + + "installation is too old (only version 5 is supported) " + + "or the service can’t be started.")); + } + + /* Set up the OBEX client which will be used for all transfers. */ + try + { + this._obex_client = + yield Bus.get_proxy (BusType.SESSION, "org.bluez.obex", + "/org/bluez/obex"); + } + catch (GLib.Error e1) + { + throw new BackendError.NO_DBUS_SERVICE ( + _("Error connecting to OBEX transfer daemon over D-Bus. " + + "Ensure BlueZ and obexd are installed.")); + } + + /* Successfully connected to both D-Bus services. Now connect up some + * signal handlers. */ + this._object_added_handler = + this._manager.object_added.connect ((obj) => + { + this._add_device.begin (obj, (o, r) => + { + this._add_device.end (r); + }); + }); + + this._object_removed_handler = + this._manager.object_removed.connect ((obj) => + { + this._remove_device.begin (obj, (o, r) => + { + this._remove_device.end (r); + }); + }); + + this._properties_changed_handler = + this._manager.interface_proxy_properties_changed.connect ( + this._device_properties_changed_cb); + + /* Add all the existing device objects. */ + var objs = this._manager.get_objects (); + + foreach (var obj in objs) + { + yield this._add_device (obj); + } + + this._is_prepared = true; + this.notify_property ("is-prepared"); + + this._is_quiescent = true; + this.notify_property ("is-quiescent"); + } + finally + { + this._prepare_pending = false; + } + + Internal.profiling_end ("preparing BlueZ.Backend"); + } + + /** + * {@inheritDoc} + * + * @since UNRELEASED + */ + public override async void unprepare () throws GLib.Error + { + if (!this._is_prepared || this._prepare_pending == true) + { + return; + } + + try + { + this._prepare_pending = true; + + if (this._manager != null) + { + this._manager.disconnect (this._object_added_handler); + this._manager.disconnect (this._object_removed_handler); + this._manager.disconnect (this._properties_changed_handler); + this._manager = null; + this._object_added_handler = 0; + this._object_removed_handler = 0; + this._properties_changed_handler = 0; + } + + this._obex_client = null; + + this.freeze_notify (); + + foreach (var persona_store in this._persona_stores.values) + this._remove_persona_store (persona_store); + + this._watched_devices.clear (); + this._persona_stores.clear (); + this.notify_property ("persona-stores"); + + this._is_quiescent = false; + this.notify_property ("is-quiescent"); + + this._is_prepared = false; + this.notify_property ("is-prepared"); + + this.thaw_notify (); + } + finally + { + this._prepare_pending = false; + } + } + + private void _persona_store_removed_cb (Folks.PersonaStore store) + { + this._remove_persona_store ((BlueZ.PersonaStore) store); + } +} diff --git a/backends/bluez/bluez-persona-store.vala b/backends/bluez/bluez-persona-store.vala new file mode 100644 index 00000000..b0b0b226 --- /dev/null +++ b/backends/bluez/bluez-persona-store.vala @@ -0,0 +1,843 @@ +/* + * Copyright (C) 2010-2013 Collabora Ltd. + * + * This library is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 library. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Arun Raghavan <arun.raghavan@collabora.co.uk> + * Jeremy Whiting <jeremy.whiting@collabora.com> + * Simon McVittie <simon.mcvittie@collabora.co.uk> + * Gustavo Padovan <gustavo.padovan@collabora.co.uk> + * Matthieu Bouron <matthieu.bouron@collabora.com> + * Philip Withnall <philip.withnall@collabora.co.uk> + * + * Based on kf-persona-store.vala by: + * Travis Reitter <travis.reitter@collabora.co.uk> + * Philip Withnall <philip.withnall@collabora.co.uk> + */ + +using GLib; +using Gee; +using Folks; +using Folks.Backends.BlueZ; +using org.bluez; + +/** + * A persona store which is associated with a single BlueZ PBAP server (i.e. + * one {@link PersonaStore} per device). It will create a {@link Persona} for + * each contact on the device. + * + * @since UNRELEASED + */ +public class Folks.Backends.BlueZ.PersonaStore : Folks.PersonaStore +{ + private HashMap<string, Persona> _personas; + private Map<string, Persona> _personas_ro; + private bool _is_prepared = false; + private bool _prepare_pending = false; + private bool _is_quiescent = false; + + private static string[] _always_writeable_properties = {}; + + private org.bluez.obex.Client _obex_client; + private HashTable<string, Variant> _phonebook_filter; + private string _object_path; + private Device _device; + private string _display_name; + + /* Non-null iff an _update_contacts() call is in progress. */ + private Cancellable? _update_contacts_cancellable = null; + + /** + * {@inheritDoc} + * + * @since UNRELEASED + */ + public override string type_id { get { return BACKEND_NAME; } } + + /** + * Whether this PersonaStore can add {@link Folks.Persona}s. + * + * See {@link Folks.PersonaStore.can_add_personas}. + * + * @since UNRELEASED + */ + public override MaybeBool can_add_personas + { + get { return MaybeBool.FALSE; } + } + + /** + * Whether this PersonaStore can set the alias of {@link Folks.Persona}s. + * + * See {@link Folks.PersonaStore.can_alias_personas}. + * + * @since UNRELEASED + */ + public override MaybeBool can_alias_personas + { + get { return MaybeBool.FALSE; } + } + + /** + * Whether this PersonaStore can set the groups of {@link Folks.Persona}s. + * + * See {@link Folks.PersonaStore.can_group_personas}. + * + * @since UNRELEASED + */ + public override MaybeBool can_group_personas + { + get { return MaybeBool.FALSE; } + } + + /** + * Whether this PersonaStore can remove {@link Folks.Persona}s. + * + * See {@link Folks.PersonaStore.can_remove_personas}. + * + * @since UNRELEASED + */ + public override MaybeBool can_remove_personas + { + get { return MaybeBool.FALSE; } + } + + /** + * Whether this PersonaStore has been prepared. + * + * See {@link Folks.PersonaStore.is_prepared}. + * + * @since UNRELEASED + */ + public override bool is_prepared + { + get { return this._is_prepared; } + } + + /** + * Whether this PersonaStore has reached a quiescent state. + * + * See {@link Folks.PersonaStore.is_quiescent}. + * + * @since UNRELEASED + */ + public override bool is_quiescent + { + get { return this._is_quiescent; } + } + + /** + * {@inheritDoc} + * + * @since unreleased + */ + public override string[] always_writeable_properties + { + get { return BlueZ.PersonaStore._always_writeable_properties; } + } + + /** + * {@inheritDoc} + * + * @since UNRELEASED + */ + public override Map<string, Persona> personas + { + get { return this._personas_ro; } + } + + /** + * {@inheritDoc} + * + * @since UNRELEASED + */ + public new string display_name + { + get { return this._display_name; } + construct { this._display_name = value; } + } + + /** + * Path of the D-Bus object backing this {@link PersonaStore}. + * + * This is the path of the BlueZ device object on D-Bus which provides the + * contacts in this store. + * + * @since UNRELEASED + */ + public string object_path + { + get { return this._object_path; } + construct { this._object_path = value; } + } + + /** + * Create a new PersonaStore. + * + * Create a new persona store to expose the {@link Persona}s provided by the + * device with the given Bluetooth address. + * + * @param device the D-Bus object for the Bluetooth device. + * @param object_path the D-Bus path of the object for the Bluetooth device + * @param obex_client the D-Bus obex client object. + * + * @since UNRELEASED + */ + public PersonaStore (Device device, string object_path, + org.bluez.obex.Client obex_client) + { + Object (id: device.address, + object_path: object_path, + display_name: device.alias); + + this._device = device; + this._obex_client = obex_client; + + this.set_is_trusted (this._device.trusted); + } + + construct + { + this._personas = new HashMap<string, Persona> (); + this._personas_ro = this._personas.read_only_view; + this._phonebook_filter = new HashTable<string, Variant> (null , null); + this._phonebook_filter.insert ("Format", "Vcard30"); + this._phonebook_filter.insert ("Fields", + new Variant.strv ({ + "N", "FN", "NICKNAME", "TEL", "URL", "EMAIL", "PHOTO" + })); + } + + /** + * Load contacts from a file and update the persona store. + * + * Load contacts from a file identified by its {@link File} and update + * the persona store accordingly. Contacts are stored in the file as a + * sequence of vCards, separated by blank lines. + * + * If this throws an error, it guarantees to leave the store’s internal state + * unchanged. + * + * @param file the file where the contacts are stored + * @param obex_pbap the current OBEX PBAP D-Bus proxy + * @throws IOError if there was an error communicating with D-Bus + * @throws DBusError if an error was returned over the bus + * @throws Error if the given file couldn’t be read + * + * @since UNRELEASED + */ + private async void _update_contacts_from_file (File file, + org.bluez.obex.PhonebookAccess obex_pbap) + throws DBusError, IOError + { + var added_personas = new HashSet<Persona> (); + + /* Get the vCard listing data where every entry + * consists of a pair of strings containing the vCard + * handle and the contact name. For example: + * "0.vcf" : "Me" + * "1.vcf" : "John" + * + * First entry corresponds to the user themselves. + */ + var entries = obex_pbap.list (this._phonebook_filter); + + try + { + var dis = new DataInputStream (file.read ()); + uint i = 0; + string? line = null; + StringBuilder vcard = new StringBuilder (); + + /* For each vCard in the file create a new Persona */ + while ((line = yield dis.read_line_async ()) != null) + { + /* Ignore blank lines between vCards. */ + if (vcard.len == 0 && line.strip () == "") + continue; + + vcard.append (line); + vcard.append_c ('\n'); + if (line.strip () == "END:VCARD") + { + var entry = entries[i]; + + /* The first vCard is always the user themselves. */ + var is_user = (i == 0); + + var persona = new Persona (entry.vcard, entry.name, + vcard.str, this, is_user); + added_personas.add (persona); + + i++; + vcard.erase (); + } + } + } + catch (GLib.Error e1) + { + /* I/O error reading the file. */ + throw new IOError.FAILED ( + /* Translators: the parameter is an error message. */ + _("Error reading the transferred address book file: %s"), + e1.message); + } + + /* Now that all the I/O is done and no more errors can be thrown, update + * the store’s internal state. */ + foreach (var p in added_personas) + this._personas.set (p.iid, p); + + if (added_personas.is_empty == false) + this._emit_personas_changed (added_personas, null); + } + + /** + * Set the persona store's alias. + * + * This will be called in response to a property change sent to the Backend. + * + * @param alias the device’s new alias + * + * @since UNRELEASED + */ + internal void set_alias (string alias) + { + debug ("Device ‘%s’ (%s) changed alias to ‘%s’.", this._display_name, + this._device.address, alias); + + this._display_name = alias; + this.notify_property ("display-name"); + } + + /** + * Set the persona store's trust level. + * + * This will be called in response to a property change sent to the Backend. + * + * Default to partial trust. BlueZ persona UIDs are built from a SHA1 + * of the contact’s vCard, which we believe can’t be maliciously edited + * to corrupt linking. + * + * The trust for each device is manually set by the user in the BlueZ + * interface on the computer. + * + * @param trusted ``true`` if the user trusts the device, ``false`` otherwise + * + * @since UNRELEASED + */ + internal void set_is_trusted (bool trusted) + { + debug ("Device ‘%s’ (%s) marked as %s.", this._device.alias, + this._device.address, trusted ? "trusted" : "untrusted"); + + this.trust_level = + trusted ? PersonaStoreTrust.FULL : PersonaStoreTrust.PARTIAL; + } + + /** + * Set the persona store's connection state. + * + * This will be called in response to a property change sent to the Backend. + * + * If this throws an error, it guarantees to leave the store’s internal state + * unchanged. + * + * @param connected ``true`` if the device is now connected, ``false`` + * otherwise + * + * @throws IOError if the operation was cancelled + * (see {@link _update_contacts}) + * @throws PersonaStoreError if the contacts couldn’t be updated + * (see {@link _update_contacts}) + * + * @since UNRELEASED + */ + internal async void set_connection_state (bool connected) + throws IOError, PersonaStoreError + { + if (connected == true) + { + debug ("Device ‘%s’ (%s) is connected.", this._device.alias, + this._device.address); + + yield this._update_contacts (); + } + else + { + debug ("Device ‘%s’ (%s) is disconnected.", this._device.alias, + this._device.address); + + /* Cancel any ongoing transfers. */ + if (this._update_contacts_cancellable != null) + this._update_contacts_cancellable.cancel (); + } + } + + /** + * Create a new obex session for this Persona store. + * + * Create a new obex session for this Persona store if no previous session + * already exists. + * + * @param obex_pbap return location for an OBEX PBAP proxy object + * @returns the path of the OBEX session D-Bus object + * @throws IOError if it can't connect to D-Bus + * @throws DBusError if it can't create a new OBEX session + * + * @since UNRELEASED + */ + private async dynamic ObjectPath _new_obex_session ( + out org.bluez.obex.PhonebookAccess obex_pbap) + throws DBusError, IOError + { + debug ("Creating a new OBEX session."); + + var args = new HashTable<string, Variant> (null, null); + args["Target"] = "PBAP"; + + var session_path = yield this._obex_client.create_session (this.id, args); + + debug (" Got OBEX session path: %s", session_path); + + obex_pbap = + yield Bus.get_proxy (BusType.SESSION, "org.bluez.obex", session_path); + + debug (" Got OBEX PBAP proxy: %p", obex_pbap); + + return session_path; + } + + /** + * Remove the specified OBEX session from this persona store. + * + * Remove the specified OBEX session for this persona store and discard its + * transfer. + * + * @param session_path the path of the OBEX session D-Bus object to remove + * + * @since UNRELEASED + */ + private async void _remove_obex_session (dynamic ObjectPath session_path) + { + try + { + yield this._obex_client.remove_session (session_path); + } + catch (IOError ie) + { + warning ("Couldn’t remove OBEX session ‘%s’: %s", + session_path, ie.message); + } + catch (DBusError de) + { + warning ("Couldn’t remove OBEX session ‘%s’: %s", + session_path, de.message); + } + } + + /** + * Watch an OBEX transfer identified by its D-Bus path. + * + * This only returns once the transfer is complete (or has failed) and the + * transfer object has been destroyed. + * + * If this throws an error, it guarantees to leave the store’s internal state + * unchanged. + * + * @param path the D-Bus transfer object path to watch. + * @param obex_pbap an OBEX PBAP proxy object to access the address book from + * @param cancellable an optional {@link Cancellable} object to cancel the + * transfer + * + * @throws IOError if the operation was cancelled, or if another failure + * occurred (unavoidable; valac generates invalid C if we try to handle + * IOError internally here) + * @throws PersonaStoreError if the transfer failed + * + * @since UNRELEASED + */ + private async void _perform_obex_transfer (string path, + org.bluez.obex.PhonebookAccess obex_pbap, + Cancellable? cancellable = null) + throws IOError, PersonaStoreError + { + org.bluez.obex.Transfer? transfer = null; + + try + { + /* Bail early if the transfer's already been cancelled. */ + if (cancellable != null) + cancellable.set_error_if_cancelled (); + + /* Get an OBEX proxy for the transfer object. */ + transfer = + yield Bus.get_proxy (BusType.SESSION, "org.bluez.obex", path); + var transfer_proxy = (DBusProxy) transfer; + + var has_yielded = false; + string? transfer_status = null; + ulong signal_id; + ulong cancellable_id = 0; + + /* Set up the cancellable. */ + if (cancellable != null) + { + cancellable_id = cancellable.connect (() => + { + transfer_status = "error"; + if (has_yielded == true) + this._perform_obex_transfer.callback (); + }); + } + + /* There is no need to add a timeout here, as BlueZ already has one + * implemented for if transactions take too long. */ + signal_id = transfer_proxy.g_properties_changed.connect ( + (changed, invalidated) => + { + var property = + changed.lookup_value ("Status", VariantType.STRING); + if (property == null) + return; + + var status = property.get_string (); + transfer_status = status; + + if (status == "complete" || status == "error") + { + /* Finished. Return to the yield. */ + if (has_yielded == true) + this._perform_obex_transfer.callback (); + } + else if (status == "queued" || status == "active") + { + /* Do nothing. */ + } + else + { + warning ("Unknown OBEX transfer status ‘%s’.", status); + } + }); + + /* Yield until the above signal handler is called with a ‘success’ or + * ‘error’ status. */ + if (transfer_status == null) + { + has_yielded = true; + yield; + } + + transfer_proxy.disconnect (signal_id); + + if (cancellable_id != 0) + cancellable.disconnect (cancellable_id); + + /* Process the results: either success or error. */ + if (transfer_status == "complete") + { + string filename = transfer.filename; + var file = File.new_for_path (filename); + + debug ("vCard’s filename for device ‘%s’ (%s): %s", + this._display_name, this.id, filename); + + yield this._update_contacts_from_file (file, obex_pbap); + } + else if (transfer_status == "error") + { + /* On cancellation, throw an IOError instead of a + * PersonaStoreError. */ + if (cancellable != null) + cancellable.set_error_if_cancelled (); + + throw new PersonaStoreError.STORE_OFFLINE ( + /* Translators: the first parameter is the name of the failed + * transfer, and the second is a Bluetooth device alias. */ + _("Error during transfer of the address book ‘%s’ from " + + "Bluetooth device ‘%s’."), + transfer.name, this._display_name); + } + else + { + assert_not_reached (); + } + } + catch (DBusError e2) + { + throw new PersonaStoreError.STORE_OFFLINE ( + /* Translators: the first parameter is the name of the + * failed transfer, the second is a Bluetooth device + * alias, and the third is an error message. */ + _("Error during transfer of the address book ‘%s’ from " + + "Bluetooth device ‘%s’: %s"), + transfer.name, this._display_name, e2.message); + } + finally + { + /* Reset the OBEX transfer and clear out the temporary file. Do this + * without yielding because BlueZ should choose a different filename + * next time (using mkstemp() or similar). */ + if (transfer != null && transfer.filename != null) + { + var file = File.new_for_path (transfer.filename); + file.delete_async.begin (GLib.Priority.DEFAULT, null, + (o, r) => + { + try + { + file.delete_async.end (r); + } + catch (GLib.Error e1) + { + /* Ignore. */ + } + }); + } + } + } + + /** + * Update contacts from this persona store. + * + * Update contacts from this persona store by initiating a new OBEX + * transfer, unless one is already in progress. If a transfer is already in + * progress, leave it running and return immediately. + * + * If this throws an error, it guarantees to leave the store’s internal state + * unchanged. + * + * @throws IOError if the operation was cancelled + * @throws PersonaStoreError if the contacts couldn’t be downloaded from the + * device + * + * @since UNRELEASED + */ + private async void _update_contacts () throws IOError, PersonaStoreError + { + dynamic ObjectPath? session_path = null; + org.bluez.obex.PhonebookAccess? obex_pbap = null; + + if (this._update_contacts_cancellable != null) + { + /* There’s an ongoing _update_contacts() call. Since downloading the + * address book takes a long time (tens of seconds), we don’t want + * to cancel the ongoing operation. Just return immediately. */ + debug ("Not updating contacts due to ongoing update operation."); + return; + } + + Internal.profiling_start ("updating BlueZ.PersonaStore (ID: %s) contacts", + this.id); + + debug ("Updating contacts."); + + try + { + string path; + HashTable<string, Variant> props; + + this._update_contacts_cancellable = new Cancellable (); + + /* Set up an OBEX session. */ + try + { + session_path = yield this._new_obex_session (out obex_pbap); + } + catch (GLib.Error e1) + { + if (e1 is IOError.DBUS_ERROR && + e1.message.has_suffix ("OBEX Connect failed with 0x43")) + { + /* This error is sent when the user denies the computer access + * to the phone’s address book over Bluetooth, after accepting + * the pairing request. */ + throw new PersonaStoreError.PERMISSION_DENIED ( + _("Permission to access the address book on Bluetooth " + + "device ‘%s’ was denied by the user."), + this._device.alias); + } + + throw new PersonaStoreError.STORE_OFFLINE ( + /* Translators: the first parameter is a Bluetooth device + * alias, and the second is an error message. */ + _("An OBEX address book transfer from device ‘%s’ could " + + "not be started: %s"), + this._device.alias, e1.message); + } + + try + { + /* Select the phonebook object we want to download ie: + * PB: phonebook for the saved contacts */ + obex_pbap.select ("int", "PB"); + + /* Initiate a phone book transfer from the PSE server using a + * plain string vCard format, transferring to a temporary file. */ + obex_pbap.pull_all ("", this._phonebook_filter, out path, + out props); + } + catch (GLib.Error e2) + { + throw new PersonaStoreError.STORE_OFFLINE ( + /* Translators: the first parameter is a Bluetooth device + * alias, and the second is an error message. */ + _("The OBEX address book transfer from device ‘%s’ " + + "failed: %s"), + this._device.alias, e2.message); + } + + try + { + yield this._perform_obex_transfer (path, obex_pbap, + this._update_contacts_cancellable); + } + catch (IOError e3) + { + if (e3 is IOError.CANCELLED) + throw e3; + + throw new PersonaStoreError.STORE_OFFLINE ( + /* Translators: the first parameter is a Bluetooth device + * alias, and the second is an error message. */ + _("Error during transfer of the address book from " + + "Bluetooth device ‘%s’: %s"), + this._display_name, e3.message); + } + } + finally + { + /* Tear down again. */ + if (session_path != null) + yield this._remove_obex_session (session_path); + obex_pbap = null; + + this._update_contacts_cancellable = null; + + Internal.profiling_end ("updating BlueZ.PersonaStore (ID: %s) " + + "contacts", this.id); + } + } + + /** + * {@inheritDoc} + * + * @since UNRELEASED + */ + public override async void prepare () throws PersonaStoreError + { + Internal.profiling_start ("preparing BlueZ.PersonaStore (ID: %s)", + this.id); + + if (this._is_prepared || this._prepare_pending) + { + return; + } + + try + { + this._prepare_pending = true; + + /* Start downloading the contacts, regardless of the phone’s + * connection state. If the phone is disconnected, the download should + * force it to be connected. */ + try + { + yield this._update_contacts (); + } + catch (IOError e1) + { + /* If this happens, the update operation was cancelled, which + * means the phone spontaneously disconnected during the transfer. + * Act as if the store has gone offline and mark preparation as + * complete. */ + throw new PersonaStoreError.STORE_OFFLINE ( + _("Bluetooth device ‘%s’ disappeared during address book " + + "transfer."), this._device.alias); + } + finally + { + /* Done or failed. We always mark the persona store as prepared + * and quiescent because of the limited data available to us from + * BlueZ: we only have the Paired and Connected properties. + * So a phone can be paired with the laptop, but its Bluetooth + * can be turned off; or a phone can be paired with the laptop and + * its Bluetooth turned on but no connection is active. In the + * former case, we don't want to connect to the device (because + * that will just fail). In the latter case, we do, because we + * want to download the address book. However, BlueZ exposes no + * information allowing differentiation of the two cases, so we + * must always create a persona store for a paired device, and + * must always try and connect. In order to prevent paired but + * disconnected phones from causing quiescence to never be reached + * (which may be a common occurrence), we always mark the stores + * as prepared and quiescent. + * + * FIXME: Note that this will fit in well with caching, if that is + * ever implemented in the BlueZ backend. Paired but disconnected + * phones (with their Bluetooth off) can still have persona stores + * on the laptop, and those persona stores can be populated by + * cached personas until the phone is reconnected. */ + this._is_prepared = true; + this.notify_property ("is-prepared"); + + this._is_quiescent = true; + this.notify_property ("is-quiescent"); + } + } + finally + { + this._prepare_pending = false; + } + + Internal.profiling_end ("preparing BlueZ.PersonaStore (ID: %s)", this.id); + } + + /** + * Remove a {@link Persona} from the PersonaStore. + * + * See {@link Folks.PersonaStore.remove_persona}. + * + * @param persona the {@link Persona} to remove + * @throws Folks.PersonaStoreError.READ_ONLY every time since the + * BlueZ backend is read-only. + * + * @since UNRELEASED + */ + public override async void remove_persona (Folks.Persona persona) + throws Folks.PersonaStoreError + { + throw new PersonaStoreError.READ_ONLY ( + "Personas cannot be removed from this store."); + } + + /** + * Add a new {@link Persona} to the PersonaStore. + * + * See {@link Folks.PersonaStore.add_persona_from_details}. + * + * @param details a map of keys to values giving the persona’s initial details + * @throws Folks.PersonaStoreError.READ_ONLY every time since the + * BlueZ backend is read-only. + * + * @since UNRELEASED + */ + public override async Folks.Persona? add_persona_from_details ( + HashTable<string, Value?> details) throws Folks.PersonaStoreError + { + throw new PersonaStoreError.READ_ONLY ( + "Personas cannot be added to this store."); + } +} diff --git a/backends/bluez/bluez-persona.vala b/backends/bluez/bluez-persona.vala new file mode 100644 index 00000000..a96b0fae --- /dev/null +++ b/backends/bluez/bluez-persona.vala @@ -0,0 +1,312 @@ +/* + * Copyright (C) 2010-2013 Collabora Ltd. + * + * This library is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 library. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Arun Raghavan <arun.raghavan@collabora.co.uk> + * Jeremy Whiting <jeremy.whiting@collabora.com> + * Simon McVittie <simon.mcvittie@collabora.co.uk> + * Matthieu Bouron <matthieu.bouron@collabora.com> + * + * Based on kf-persona.vala by: + * Philip Withnall <philip.withnall@collabora.co.uk> + */ + +using GLib; +using Gee; +using Folks; +using Folks.Backends.BlueZ; + +/** + * A persona subclass which represents a single persona from a simple key file. + * + * @since UNRELEASED + */ +public class Folks.Backends.BlueZ.Persona : Folks.Persona, + AvatarDetails, + EmailDetails, + NameDetails, + PhoneDetails, + UrlDetails +{ + private StructuredName? _structured_name = null; + private string _full_name = ""; + private string _nickname = ""; + private Set<UrlFieldDetails>? _urls = null; + private Set<UrlFieldDetails>? _urls_ro = null; + private LoadableIcon? _avatar = null; + private HashSet<PhoneFieldDetails> _phone_numbers; + private Set<PhoneFieldDetails> _phone_numbers_ro; + private HashSet<EmailFieldDetails> _email_addresses; + private Set<EmailFieldDetails> _email_addresses_ro; + + private const string[] _linkable_properties = + { + "phone-numbers", + "email-addresses" + }; + private static string[] _writeable_properties = { }; + + /** + * {@inheritDoc} + * + * @since UNRELEASED + */ + public override string[] linkable_properties + { + get { return BlueZ.Persona._linkable_properties; } + } + + /** + * {@inheritDoc} + * + * @since UNRELEASED + */ + [CCode (notify = false)] + public Set<UrlFieldDetails> urls + { + get { return this._urls_ro; } + set { this.change_urls.begin (value); } /* not writeable */ + } + + /** + * {@inheritDoc} + * + * @since UNRELEASED + */ + [CCode (notify = false)] + public LoadableIcon? avatar + { + get { return this._avatar; } + set { this.change_avatar.begin (value); } + } + + /** + * {@inheritDoc} + * + * @since UNRELEASED + */ + public override string[] writeable_properties + { + get { return BlueZ.Persona._writeable_properties; } + } + + /** + * {@inheritDoc} + * + * @since UNRELEASED + */ + [CCode (notify = false)] + public Set<PhoneFieldDetails> phone_numbers + { + get { return this._phone_numbers_ro; } + set { this.change_phone_numbers.begin (value); } /* not writeable */ + } + + /** + * {@inheritDoc} + * + * @since UNRELEASED + */ + [CCode (notify = false)] + public StructuredName? structured_name + { + get { return this._structured_name; } + set { this.change_structured_name.begin (value); } /* not writeable */ + } + + /** + * {@inheritDoc} + * + * @since UNRELEASED + */ + [CCode (notify = false)] + public string full_name + { + get { return this._full_name; } + set { this.change_full_name.begin (value); } /* not writeable */ + } + + /** + * {@inheritDoc} + * + * @since UNRELEASED + */ + [CCode (notify = false)] + public string nickname + { + get { return this._nickname; } + set { this.change_nickname.begin (value); } /* not writeable */ + } + + /** + * {@inheritDoc} + * + * @since UNRELEASED + */ + [CCode (notify = false)] + public Set<EmailFieldDetails> email_addresses + { + get { return this._email_addresses_ro; } + set { this.change_email_addresses.begin (value); } /* not writeable */ + } + + /** + * Create a new persona. + * + * Create a new persona for the {@link PersonaStore} ``store``, representing + * the Persona given by the group ``uid`` in the key file ``key_file``. + * + * @param vcf the VCard filename reference. For example: 0.vcf. + * @param name the Persona the contact name or alias. + * @param vcard the Vcard stored as a string. + * @param store the store to which the Persona belongs. + * @param is_user whether the Persona is the user itself or not. + * + * @since UNRELEASED + */ + public Persona (string vcf, string name, string vcard, + Folks.PersonaStore store, bool is_user) + { + var iid = Checksum.compute_for_string (ChecksumType.SHA1, vcard); + var uid = Folks.Persona.build_uid ("bluez", store.id, iid); + + Object (display_id: name, + iid: iid, + uid: uid, + store: store, + is_user: is_user); + + this._set_vcard (vcard); + } + + construct + { + debug ("Adding BlueZ Persona '%s' (IID '%s', group '%s')", this.uid, + this.iid, this.display_id); + + this._phone_numbers = new HashSet<PhoneFieldDetails> (); + this._phone_numbers_ro = this._phone_numbers.read_only_view; + + this._email_addresses = new HashSet<EmailFieldDetails> (); + this._email_addresses_ro = this._email_addresses.read_only_view; + + this._urls = new HashSet<UrlFieldDetails> (); + this._urls_ro = this._urls.read_only_view; + } + + private void _set_vcard (string vcard) + { + E.VCard card = new E.VCard.from_string (vcard); + + E.VCardAttribute? attribute = card.get_attribute ("TEL"); + if (attribute != null) + { + this._phone_numbers.add ( + new PhoneFieldDetails (attribute.get_value_decoded ().str)); + } + + attribute = card.get_attribute ("FN"); + if (attribute != null) + { + this._full_name = attribute.get_value_decoded ().str; + } + + attribute = card.get_attribute ("NICKNAME"); + if (attribute != null) + { + this._nickname = attribute.get_value_decoded ().str; + } + + attribute = card.get_attribute ("URL"); + if (attribute != null) + { + var url = attribute.get_value_decoded ().str; + this._urls.add (new UrlFieldDetails (url)); + } + + attribute = card.get_attribute ("PHOTO"); + if (attribute != null) + { + var encoded_data = (string) attribute.get_value ().data; + var bytes = new Bytes (Base64.decode (encoded_data)); + this._avatar = new BytesIcon (bytes); + } + + attribute = card.get_attribute ("N"); + if (attribute != null) + { + string[] components = {"", "", "", "", ""}; + uint components_size = 5; + unowned GLib.List<StringBuilder> values = + attribute.get_values_decoded (); + + if (values.length () < components_size) + components_size = values.length (); + + for (int i = 0; i < components_size; i++) + { + components[i] = values.nth_data (i).str; + } + + this._structured_name = new StructuredName (components[0], + components[1], components[2], components[3], components[4]); + + if (values.length () != 5) + { + debug ("Expected 5 components to N value of vcard, got %u", + values.length ()); + } + } + + attribute = card.get_attribute ("EMAIL"); + if (attribute != null) + { + this._email_addresses.add ( + new EmailFieldDetails (attribute.get_value_decoded ().str)); + } + } + + /** + * {@inheritDoc} + * + * @since UNRELEASED + */ + public override void linkable_property_to_links (string prop_name, + Folks.Persona.LinkablePropertyCallback callback) + { + if (prop_name == "phone-numbers") + { + foreach (var phone_number in this._phone_numbers) + { + if (phone_number.value != null) + callback (phone_number.value); + } + } + else if (prop_name == "email-addresses") + { + foreach (var email_address in this._email_addresses) + { + if (email_address.value != null) + callback (email_address.value); + } + } + else + { + /* Chain up */ + base.linkable_property_to_links (prop_name, callback); + } + } +} diff --git a/backends/bluez/org-bluez-obex-client.vala b/backends/bluez/org-bluez-obex-client.vala new file mode 100644 index 00000000..b22f21fd --- /dev/null +++ b/backends/bluez/org-bluez-obex-client.vala @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2012-2013 Collabora Ltd. + * + * This library is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 library. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Arun Raghavan <arun.raghavan@collabora.co.uk> + * Gustavo Padovan <gustavo.padovan@collabora.co.uk> + * Matthieu Bouron <matthieu.bouron@collabora.com> + */ + +using GLib; + +namespace org + { + namespace bluez + { + namespace obex + { + [DBus (name = "org.bluez.obex.Client1")] + public interface Client : Object + { + [DBus (name = "CreateSession")] + public async abstract ObjectPath create_session (string address, + HashTable<string, Variant> args) throws DBusError, IOError; + [DBus (name = "RemoveSession")] + public async abstract void remove_session (ObjectPath session) + throws DBusError, IOError; + } + + [DBus (name = "org.bluez.obex.PhonebookAccess1")] + public interface PhonebookAccess : Object + { + /* Returned by List () */ + public struct PhonebookEntry + { + public string vcard; + public string name; + } + + public struct PhonebookPull + { + public ObjectPath path; + public HashTable<string, Variant> props; + } + + [DBus (name = "Select")] + public abstract void select (string location, string phonebook) + throws DBusError, IOError; + [DBus (name = "List")] + public abstract PhonebookEntry[] list ( + HashTable<string, Variant> filters) + throws DBusError, IOError; + [DBus (name = "ListFilterFields")] + public abstract string[] list_filter_fields () + throws DBusError, IOError; + [DBus (name = "PullAll")] + public abstract void pull_all (string target, + HashTable<string, Variant> filters, out string path, + out HashTable<string, Variant> props) + throws DBusError, IOError; + } + + [DBus (name = "org.bluez.obex.Transfer1")] + public interface Transfer : Object + { + [Dbus (name = "Cancel")] + public abstract void cancel () throws DBusError; + [Dbus (name = "Status")] + public abstract string status { owned get; } + [Dbus (name = "Session")] + public abstract ObjectPath session { owned get; } + [Dbus (name = "Name")] + public abstract string name { owned get; } + [Dbus (name = "Type")] + public abstract string transfer_type { owned get; } + [Dbus (name = "Time")] + public abstract int64 time { get; } + [Dbus (name = "Size")] + public abstract uint64 size { get; } + [Dbus (name = "Transferred")] + public abstract uint64 transferred { get; } + [Dbus (name = "Filename")] + public abstract string filename { owned get; } + } + } + } + } diff --git a/backends/bluez/org-bluez.vala b/backends/bluez/org-bluez.vala new file mode 100644 index 00000000..641bd09b --- /dev/null +++ b/backends/bluez/org-bluez.vala @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2012-2013 Collabora Ltd. + * + * This library is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 library. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Arun Raghavan <arun.raghavan@collabora.co.uk> + * Gustavo Padovan <gustavo.padovan@collabora.co.uk> + * Matthieu Bouron <matthieu.bouron@collabora.com> + */ + +using GLib; + +/* Reference: + * http://git.kernel.org/cgit/bluetooth/bluez.git/tree/doc/device-api.txt */ +namespace org + { + namespace bluez + { + [DBus (name = "org.bluez.Error")] + public errordomain Error + { + NOT_READY, + FAILED, + IN_PROGRESS, + ALREADY_CONNECTED, + NOT_CONNECTED, + DOES_NOT_EXIST, + CONNECT_FAILED, + NOT_SUPPORTED, + INVALID_ARGUMENTS, + AUTHENTICATION_CANCELED, + AUTHENTICATION_FAILED, + AUTHENTICATION_REJECTED, + AUTHENTICATION_TIMEOUT, + CONNECTION_ATTEMPT_FAILED + } + + [DBus (name = "org.bluez.Device1")] + public interface Device : Object + { + /* Methods. */ + [DBus (name = "Connect")] + public abstract void connect () throws org.bluez.Error; + + [DBus (name = "Disconnect")] + public abstract void disconnect () throws org.bluez.Error; + + [DBus (name = "DisconnectProfile")] + public abstract void disconnect_profile (string uuid) throws org.bluez.Error; + + [DBus (name = "Pair")] + public abstract void pair () throws org.bluez.Error; + + [DBus (name = "CancelPairing")] + public abstract void cancel_pairing () throws org.bluez.Error; + + /* Properties. */ + [DBus (name = "Address")] + public abstract string address { owned get; } + + [DBus (name = "Name")] + public abstract string name { owned get; } + + [DBus (name = "Icon")] + public abstract string icon { owned get; } + + [DBus (name = "Class")] + public abstract uint32 bluetooth_class { owned get; } + + [DBus (name = "Appearance")] + public abstract uint16 appearance { owned get; } + + [DBus (name = "UUIDs")] + public abstract string[] uuids { owned get; } + + [DBus (name = "Paired")] + public abstract bool paired { owned get; } + + [DBus (name = "Connected")] + public abstract bool connected { owned get; } + + [DBus (name = "Trusted")] + public abstract bool trusted { owned get; set; } + + [DBus (name = "Blocked")] + public abstract bool blocked { owned get; set; } + + [DBus (name = "Alias")] + public abstract string alias { owned get; set; } + + [DBus (name = "Adapter")] + public abstract ObjectPath adapter { owned get; } + + [DBus (name = "LegacyPairing")] + public abstract bool legacy_pairing { owned get; } + + [DBus (name = "Modalias")] + public abstract string mod_alias { owned get; } + + [DBus (name = "RSSI")] + public abstract int16 rssi { owned get; } + } + } + } diff --git a/configure.ac b/configure.ac index 3dea7cef..d0dfcc6e 100644 --- a/configure.ac +++ b/configure.ac @@ -119,6 +119,20 @@ AS_IF([test "x$enable_ofono_backend" = "xyes"], [ AM_CONDITIONAL([ENABLE_OFONO], [test "x$enable_ofono_backend" = "xyes"]) +AC_ARG_ENABLE(bluez-backend, + AC_HELP_STRING([--enable-bluez-backend], + [ build the bluez backend]), + enable_bluez_backend=$enableval, + enable_bluez_backend=yes ) + +AS_IF([test "x$enable_bluez_backend" = "xyes"], [ + AC_DEFINE(HAVE_BLUEZ, [1], [Define as 1 if you have the BlueZ backend]) +], [ + AC_DEFINE(HAVE_BLUEZ, [0], [Define as 1 if you have the BlueZ backend]) +]) + +AM_CONDITIONAL([ENABLE_BLUEZ], [test "x$enable_bluez_backend" = "xyes"]) + AC_ARG_ENABLE(telepathy-backend, AC_HELP_STRING([--enable-telepathy-backend], [ build the Telepathy backend]), @@ -183,8 +197,8 @@ AM_CONDITIONAL([ENABLE_LIBSOCIALWEB], # Dependencies # ----------------------------------------------------------- -GLIB_REQUIRED=2.37.6 -VALA_REQUIRED=0.17.6 +GLIB_REQUIRED=2.39.0 +VALA_REQUIRED=0.22.0.28-9090 VALADOC_REQUIRED=0.3.1 TRACKER_SPARQL_MAJOR=0.16 TRACKER_SPARQL_REQUIRED=0.15.2 @@ -259,6 +273,10 @@ AS_IF([test x$enable_ofono_backend = xyes], [ PKG_CHECK_MODULES([EBOOK], [libebook-1.2 >= $EBOOK_REQUIRED]) ]) +AS_IF([test x$enable_bluez_backend = xyes], [ + PKG_CHECK_MODULES([EBOOK], [libebook-1.2 >= $EBOOK_REQUIRED]) +]) + # # Vala building options -- allows tarball builds without installing Vala # @@ -354,6 +372,10 @@ AS_IF([test "x$enable_vala" = "xyes"], [ AS_IF([test x$enable_ofono_backend = xyes], [ VALA_CHECK_PACKAGES([libebook-1.2]) ]) + + AS_IF([test x$enable_bluez_backend = xyes], [ + VALA_CHECK_PACKAGES([libebook-1.2]) + ]) ]) # this will set HAVE_INTROSPECTION @@ -657,6 +679,7 @@ AC_CONFIG_FILES([ backends/eds/Makefile backends/eds/lib/Makefile backends/ofono/Makefile + backends/bluez/Makefile folks/Makefile docs/Makefile po/Makefile.in @@ -700,6 +723,7 @@ Configure summary: Libsocialweb backend........: ${have_libsocialweb_backend} E-D-S backend...............: ${enable_eds_backend} Ofono backend...............: ${enable_ofono_backend} + BlueZ backend...............: ${enable_bluez_backend} Zeitgeist support...........: ${have_zeitgeist} Build tests.................: ${enable_tests} " diff --git a/folks/build-conf.vapi b/folks/build-conf.vapi index dbca4369..5dbb4d1f 100644 --- a/folks/build-conf.vapi +++ b/folks/build-conf.vapi @@ -54,6 +54,9 @@ public class Folks.BuildConf [CCode (cname = "HAVE_OFONO")] public static bool HAVE_OFONO; + [CCode (cname = "HAVE_BLUEZ")] + public static bool HAVE_BLUEZ; + [CCode (cname = "HAVE_TELEPATHY")] public static bool HAVE_TELEPATHY; diff --git a/po/POTFILES.in b/po/POTFILES.in index c6b1d5c9..2cec85ae 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -1,4 +1,6 @@ [encoding: UTF-8] +backends/bluez/bluez-backend.vala +backends/bluez/bluez-persona-store.vala backends/eds/lib/edsf-persona-store.vala backends/key-file/kf-backend-factory.vala backends/key-file/kf-persona-store.vala diff --git a/po/POTFILES.skip b/po/POTFILES.skip index b4fd7ed0..9a796192 100644 --- a/po/POTFILES.skip +++ b/po/POTFILES.skip @@ -1,3 +1,5 @@ +backends/bluez/bluez-backend.c +backends/bluez/bluez-persona-store.c backends/eds/lib/edsf-persona-store.c backends/key-file/kf-backend-factory.c backends/key-file/kf-persona-store.c |