summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatthieu Bouron <matthieu.bouron@collabora.com>2012-08-21 16:49:44 +0530
committerPhilip Withnall <philip.withnall@collabora.co.uk>2013-10-31 20:04:38 +0000
commit659a5c4539912bb3b31f7e55e48e6fd4215257f8 (patch)
tree2ba56888cd6064f59d0cbd49c40dc6caafb3f57a
parent1af18e6813cf1cb2e6319f03b7f5dd9bf22587bd (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--NEWS5
-rw-r--r--backends/Makefile.am5
-rw-r--r--backends/bluez/Makefile.am41
-rw-r--r--backends/bluez/bluez-backend-factory.vala73
-rw-r--r--backends/bluez/bluez-backend.vala636
-rw-r--r--backends/bluez/bluez-persona-store.vala843
-rw-r--r--backends/bluez/bluez-persona.vala312
-rw-r--r--backends/bluez/org-bluez-obex-client.vala99
-rw-r--r--backends/bluez/org-bluez.vala116
-rw-r--r--configure.ac28
-rw-r--r--folks/build-conf.vapi3
-rw-r--r--po/POTFILES.in2
-rw-r--r--po/POTFILES.skip2
13 files changed, 2162 insertions, 3 deletions
diff --git a/NEWS b/NEWS
index 0fd1f52a..02bd8df6 100644
--- a/NEWS
+++ b/NEWS
@@ -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