[folks] bluez: Add a Bluetooth Phonebook Access Profile backend using BlueZ 5



commit 659a5c4539912bb3b31f7e55e48e6fd4215257f8
Author: Matthieu Bouron <matthieu bouron collabora com>
Date:   Tue Aug 21 16:49:44 2012 +0530

    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=710643https://bugzilla.gnome.org/show_bug.cgi?id=710726
    
    https://bugzilla.gnome.org/show_bug.cgi?id=685848

 NEWS                                      |    5 +-
 backends/Makefile.am                      |    5 +
 backends/bluez/Makefile.am                |   41 ++
 backends/bluez/bluez-backend-factory.vala |   73 +++
 backends/bluez/bluez-backend.vala         |  636 ++++++++++++++++++++++
 backends/bluez/bluez-persona-store.vala   |  843 +++++++++++++++++++++++++++++
 backends/bluez/bluez-persona.vala         |  312 +++++++++++
 backends/bluez/org-bluez-obex-client.vala |   99 ++++
 backends/bluez/org-bluez.vala             |  116 ++++
 configure.ac                              |   28 +-
 folks/build-conf.vapi                     |    3 +
 po/POTFILES.in                            |    2 +
 po/POTFILES.skip                          |    2 +
 13 files changed, 2162 insertions(+), 3 deletions(-)
---
diff --git a/NEWS b/NEWS
index 0fd1f52..02bd8df 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 0da4773..f3d5e18 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 0000000..d1a3964
--- /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 0000000..5e13968
--- /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 0000000..3f19440
--- /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 0000000..b0b0b22
--- /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 0000000..a96b0fa
--- /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 0000000..b22f21f
--- /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 0000000..641bd09
--- /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 3dea7ce..d0dfcc6 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 dbca436..5dbb4d1 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 c6b1d5c..2cec85a 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 b4fd7ed..9a79619 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


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]