summary refs log tree commit diff
path: root/src/devices/bluetooth/nm-bluez-manager.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/devices/bluetooth/nm-bluez-manager.c')
-rw-r--r--src/devices/bluetooth/nm-bluez-manager.c2881
1 files changed, 2642 insertions, 239 deletions
diff --git a/src/devices/bluetooth/nm-bluez-manager.c b/src/devices/bluetooth/nm-bluez-manager.c
index 7577ab8b..ef087d83 100644
--- a/src/devices/bluetooth/nm-bluez-manager.c
+++ b/src/devices/bluetooth/nm-bluez-manager.c
@@ -1,73 +1,185 @@
-/* NetworkManager -- Network link manager
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- *
+// SPDX-License-Identifier: GPL-2.0+
+/*
  * Copyright (C) 2013 - 2014 Red Hat, Inc.
  */
 
 #include "nm-default.h"
 
+#include "nm-bluez-manager.h"
+
 #include <signal.h>
 #include <stdlib.h>
 #include <gmodule.h>
 
+#include "nm-glib-aux/nm-dbus-aux.h"
+#include "nm-glib-aux/nm-c-list.h"
+#include "nm-dbus-manager.h"
 #include "devices/nm-device-factory.h"
 #include "devices/nm-device-bridge.h"
 #include "nm-setting-bluetooth.h"
 #include "settings/nm-settings.h"
-#include "nm-bluez4-manager.h"
-#include "nm-bluez5-manager.h"
-#include "nm-bluez-device.h"
 #include "nm-bluez-common.h"
 #include "nm-device-bt.h"
+#include "nm-manager.h"
+#include "nm-bluez5-dun.h"
 #include "nm-core-internal.h"
 #include "platform/nm-platform.h"
 #include "nm-std-aux/nm-dbus-compat.h"
 
 /*****************************************************************************/
 
-#define NM_TYPE_BLUEZ_MANAGER            (nm_bluez_manager_get_type ())
-#define NM_BLUEZ_MANAGER(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), NM_TYPE_BLUEZ_MANAGER, NMBluezManager))
-#define NM_BLUEZ_MANAGER_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass),  NM_TYPE_BLUEZ_MANAGER, NMBluezManagerClass))
-#define NM_IS_BLUEZ_MANAGER(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), NM_TYPE_BLUEZ_MANAGER))
-#define NM_IS_BLUEZ_MANAGER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass),  NM_TYPE_BLUEZ_MANAGER))
-#define NM_BLUEZ_MANAGER_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj),  NM_TYPE_BLUEZ_MANAGER, NMBluezManagerClass))
+#if WITH_BLUEZ5_DUN
+#define _NM_BT_CAPABILITY_SUPPORTED_DUN NM_BT_CAPABILITY_DUN
+#else
+#define _NM_BT_CAPABILITY_SUPPORTED_DUN NM_BT_CAPABILITY_NONE
+#endif
+#define _NM_BT_CAPABILITY_SUPPORTED (NM_BT_CAPABILITY_NAP | _NM_BT_CAPABILITY_SUPPORTED_DUN)
+
+typedef struct {
+	const char *bdaddr;
+	CList lst_head;
+	NMBluetoothCapabilities bt_type:8;
+	char bdaddr_data[];
+} ConnDataHead;
+
+typedef struct {
+	NMSettingsConnection *sett_conn;
+	ConnDataHead *cdata_hd;
+	CList lst;
+} ConnDataElem;
 
 typedef struct {
-	int bluez_version;
+	GCancellable *ext_cancellable;
+	GCancellable *int_cancellable;
+	NMBtVTableRegisterCallback callback;
+	gpointer callback_user_data;
+	gulong ext_cancelled_id;
+} NetworkServerRegisterReqData;
+
+typedef struct {
+	GCancellable *ext_cancellable;
+	GCancellable *int_cancellable;
+	NMBluezManagerConnectCb callback;
+	gpointer callback_user_data;
+	char *device_name;
+	gulong ext_cancelled_id;
+	guint timeout_id;
+	guint timeout_wait_connect_id;
+} DeviceConnectReqData;
+
+typedef struct {
+	const char *object_path;
+
+	NMBluezManager *self;
 
+	/* Fields name with "d_" prefix are purely cached values from BlueZ's
+	 * ObjectManager D-Bus interface. There is no logic whatsoever about
+	 * them.
+	 */
+
+	CList process_change_lst;
+
+	struct {
+		char *address;
+	} d_adapter;
+
+	struct {
+		char *address;
+		char *name;
+		char *adapter;
+	} d_device;
+
+	struct {
+		char *interface;
+	} d_network;
+
+	struct {
+		CList lst;
+		char *adapter_address;
+		NMDevice *device_br;
+		NetworkServerRegisterReqData *r_req_data;
+	} x_network_server;
+
+	struct {
+		NMSettingsConnection *panu_connection;
+		NMDeviceBt *device_bt;
+		DeviceConnectReqData *c_req_data;
+		NMBluez5DunContext *connect_dun_context;
+		gulong device_bt_signal_id;
+	} x_device;
+
+	/* indicate whether the D-Bus object has the particular D-Bus interface. */
+	bool d_has_adapter_iface:1;
+	bool d_has_device_iface:1;
+	bool d_has_network_iface:1;
+	bool d_has_network_server_iface:1;
+
+	/* cached D-Bus properties for Device1 ("d_device*"). */
+	NMBluetoothCapabilities d_device_capabilities:6;
+	bool d_device_connected:1;
+	bool d_device_paired:1;
+
+	/* cached D-Bus properties for Network1 ("d_network*"). */
+	bool d_network_connected:1;
+
+	/* cached D-Bus properties for Adapter1 ("d_adapter*"). */
+	bool d_adapter_powered:1;
+
+	/* properties related to device ("x_device*"). */
+	NMBluetoothCapabilities x_device_connect_bt_type:6;
+	bool x_device_is_usable:1;
+	bool x_device_is_connected:1;
+
+	bool x_device_panu_connection_allow_create:1;
+
+	/* flag to remember last time when we checked wether the object
+	 * was  a suitable adapter that is usable to a device. */
+	bool was_usable_adapter_for_device_before:1;
+
+	char _object_path_intern[];
+} BzDBusObj;
+
+typedef struct {
+	NMManager *manager;
 	NMSettings *settings;
-	NMBluez4Manager *manager4;
-	NMBluez5Manager *manager5;
 
-	guint watch_name_id;
+	GDBusConnection *dbus_connection;
+
+	NMBtVTableNetworkServer vtable_network_server;
+
+	GCancellable *name_owner_get_cancellable;
+	GCancellable *get_managed_objects_cancellable;
+
+	GHashTable *bzobjs;
+
+	char *name_owner;
+
+	GHashTable *conn_data_heads;
+	GHashTable *conn_data_elems;
+
+	CList network_server_lst_head;
+
+	CList process_change_lst_head;
+
+	guint name_owner_changed_id;
 
-	GDBusProxy *introspect_proxy;
-	GCancellable *async_cancellable;
+	guint managed_objects_changed_id;
+
+	guint properties_changed_id;
+
+	guint process_change_idle_id;
+
+	bool settings_registered:1;
 } NMBluezManagerPrivate;
 
-typedef struct {
+struct _NMBluezManager {
 	NMDeviceFactory parent;
 	NMBluezManagerPrivate _priv;
-} NMBluezManager;
+};
 
-typedef struct {
+struct _NMBluezManagerClass {
 	NMDeviceFactoryClass parent;
-} NMBluezManagerClass;
-
-static GType nm_bluez_manager_get_type (void);
+};
 
 G_DEFINE_TYPE (NMBluezManager, nm_bluez_manager, NM_TYPE_DEVICE_FACTORY);
 
@@ -93,313 +205,2583 @@ nm_device_factory_create (GError **error)
 
 /*****************************************************************************/
 
-static void check_bluez_and_try_setup (NMBluezManager *self);
+static NMBluetoothCapabilities
+convert_uuids_to_capabilities (const char *const*strv)
+{
+	NMBluetoothCapabilities capabilities = NM_BT_CAPABILITY_NONE;
+
+	if (strv) {
+		for (; strv[0]; strv++) {
+			gs_free char *s_part1 = NULL;
+			const char *str = strv[0];
+			const char *s;
+
+			s = strchr (str, '-');
+			if (!s)
+				continue;
+
+			s_part1 = g_strndup (str, s - str);
+			switch (g_ascii_strtoull (s_part1, NULL, 16)) {
+			case 0x1103:
+				capabilities |= NM_BT_CAPABILITY_DUN;
+				break;
+			case 0x1116:
+				capabilities |= NM_BT_CAPABILITY_NAP;
+				break;
+			default:
+				break;
+			}
+		}
+	}
+
+	return capabilities;
+}
+
+/*****************************************************************************/
+
+static void _cleanup_for_name_owner (NMBluezManager *self);
+static void _connect_disconnect (NMBluezManager *self,
+                                 BzDBusObj *bzobj,
+                                 const char *reason);
+static gboolean _bzobjs_network_server_is_usable (const BzDBusObj *bzobj,
+                                                  gboolean require_powered);
+static gboolean _bzobjs_is_dead (const BzDBusObj *bzobj);
+static gboolean _bzobjs_device_is_usable (const BzDBusObj *bzobj,
+                                          BzDBusObj **out_adapter_bzobj,
+                                          gboolean *out_create_panu_connection);
+static gboolean _bzobjs_adapter_is_usable_for_device (const BzDBusObj *bzobj);
+static ConnDataHead *_conn_track_find_head (NMBluezManager *self,
+                                            NMBluetoothCapabilities bt_type,
+                                            const char *bdaddr);
+static void _process_change_idle_schedule (NMBluezManager *self,
+                                           BzDBusObj *bzobj);
+static void _network_server_unregister_bridge (NMBluezManager *self,
+                                               BzDBusObj *bzobj,
+                                               const char *reason);
+static gboolean _connect_timeout_wait_connected_cb (gpointer user_data);
 
 /*****************************************************************************/
 
-struct AsyncData {
+static void
+_dbus_call_complete_cb_nop (GObject *source_object,
+                            GAsyncResult *res,
+                            gpointer user_data)
+{
+	/* we don't do anything at all. The only reason to register this
+	 * callback is so that GDBusConnection keeps the cancellable alive
+	 * long enough until the call completes.
+	 *
+	 * Note that this cancellable in turn is registered via
+	 * nm_shutdown_wait_obj_register_*(), to block shutdown until
+	 * we are done. */
+}
+
+/*****************************************************************************/
+
+static void
+_network_server_register_req_data_complete (NetworkServerRegisterReqData *r_req_data,
+                                            GError *error)
+{
+	nm_clear_g_signal_handler (r_req_data->ext_cancellable, &r_req_data->ext_cancelled_id);
+
+	nm_clear_g_cancellable (&r_req_data->int_cancellable);
+
+	if (r_req_data->callback) {
+		gs_free_error GError *error_cancelled = NULL;
+
+		if (g_cancellable_set_error_if_cancelled (r_req_data->ext_cancellable, &error_cancelled))
+			error = error_cancelled;
+
+		r_req_data->callback (error, r_req_data->callback_user_data);
+	}
+
+	g_object_unref (r_req_data->ext_cancellable);
+	nm_g_slice_free (r_req_data);
+}
+
+static void
+_device_connect_req_data_complete (DeviceConnectReqData *c_req_data,
+                                   NMBluezManager *self,
+                                   const char *device_name,
+                                   GError *error)
+{
+	nm_assert ((!!device_name) != (!!error));
+
+	nm_clear_g_signal_handler (c_req_data->ext_cancellable, &c_req_data->ext_cancelled_id);
+
+	nm_clear_g_cancellable (&c_req_data->int_cancellable);
+	nm_clear_g_source (&c_req_data->timeout_id);
+	nm_clear_g_source (&c_req_data->timeout_wait_connect_id);
+
+	if (c_req_data->callback) {
+		gs_free_error GError *error_cancelled = NULL;
+
+		if (g_cancellable_set_error_if_cancelled (c_req_data->ext_cancellable, &error_cancelled)) {
+			error = error_cancelled;
+			device_name = NULL;
+		}
+
+		c_req_data->callback (self, TRUE, device_name, error, c_req_data->callback_user_data);
+	}
+
+	g_object_unref (c_req_data->ext_cancellable);
+	nm_clear_g_free (&c_req_data->device_name);
+	nm_g_slice_free (c_req_data);
+}
+
+/*****************************************************************************/
+
+static BzDBusObj *
+_bz_dbus_obj_new (NMBluezManager *self,
+                  const char *object_path)
+{
+	BzDBusObj *bzobj;
+	gsize l;
+
+	nm_assert (NM_IS_BLUEZ_MANAGER (self));
+
+	l = strlen (object_path) + 1;
+
+	bzobj = g_malloc (sizeof (BzDBusObj) + l);
+	*bzobj = (BzDBusObj) {
+		.object_path                           = bzobj->_object_path_intern,
+		.self                                  = self,
+		.x_network_server.lst                  = C_LIST_INIT (bzobj->x_network_server.lst),
+		.process_change_lst                    = C_LIST_INIT (bzobj->process_change_lst),
+		.x_device_panu_connection_allow_create = TRUE,
+	};
+	memcpy (bzobj->_object_path_intern, object_path, l);
+
+	return bzobj;
+}
+
+static void
+_bz_dbus_obj_free (BzDBusObj *bzobj)
+{
+	nm_assert (bzobj);
+	nm_assert (NM_IS_BLUEZ_MANAGER (bzobj->self));
+	nm_assert (!bzobj->x_network_server.device_br);
+	nm_assert (!bzobj->x_network_server.r_req_data);
+	nm_assert (!bzobj->x_device.c_req_data);
+
+	c_list_unlink_stale (&bzobj->process_change_lst);
+	c_list_unlink_stale (&bzobj->x_network_server.lst);
+	g_free (bzobj->x_network_server.adapter_address);
+	g_free (bzobj->d_adapter.address);
+	g_free (bzobj->d_network.interface);
+	g_free (bzobj->d_device.address);
+	g_free (bzobj->d_device.name);
+	g_free (bzobj->d_device.adapter);
+	g_free (bzobj);
+}
+
+/*****************************************************************************/
+
+static const char *
+_bzobj_to_string (const BzDBusObj *bzobj, char *buf, gsize len)
+{
+	char *buf0 = buf;
+	const char *prefix = "";
+	gboolean device_is_usable;
+	gboolean create_panu_connection = FALSE;
+	gboolean network_server_is_usable;
+	char sbuf_cap[100];
+
+	if (len > 0)
+		buf[0] = '\0';
+
+	if (bzobj->d_has_adapter_iface) {
+		nm_utils_strbuf_append_str (&buf, &len, prefix);
+		prefix = ", ";
+		nm_utils_strbuf_append_str (&buf, &len, "Adapter1 {");
+		if (bzobj->d_adapter.address) {
+			nm_utils_strbuf_append (&buf, &len, " d.address: \"%s\"", bzobj->d_adapter.address);
+			if (bzobj->d_adapter_powered)
+				nm_utils_strbuf_append_str (&buf, &len, ",");
+		}
+		if (bzobj->d_adapter_powered)
+			nm_utils_strbuf_append (&buf, &len, " d.powered: 1");
+		nm_utils_strbuf_append_str (&buf, &len, " }");
+	}
+
+	if (bzobj->d_has_device_iface) {
+		const char *prefix1 = "";
+
+		nm_utils_strbuf_append_str (&buf, &len, prefix);
+		prefix = ", ";
+		nm_utils_strbuf_append_str (&buf, &len, "Device1 {");
+		if (bzobj->d_device.address) {
+			nm_utils_strbuf_append (&buf, &len, "%s d.address: \"%s\"", prefix1, bzobj->d_device.address);
+			prefix1 = ",";
+		}
+		if (bzobj->d_device.name) {
+			nm_utils_strbuf_append (&buf, &len, "%s d.name: \"%s\"", prefix1, bzobj->d_device.name);
+			prefix1 = ",";
+		}
+		if (bzobj->d_device.adapter) {
+			nm_utils_strbuf_append (&buf, &len, "%s d.adapter: \"%s\"", prefix1, bzobj->d_device.adapter);
+			prefix1 = ",";
+		}
+		if (bzobj->d_device_capabilities != NM_BT_CAPABILITY_NONE) {
+			nm_utils_strbuf_append (&buf, &len, "%s d.capabilities: \"%s\"",
+			                        prefix1,
+			                        nm_bluetooth_capability_to_string (bzobj->d_device_capabilities, sbuf_cap, sizeof (sbuf_cap)));
+			prefix1 = ",";
+		}
+		if (bzobj->d_device_connected) {
+			nm_utils_strbuf_append (&buf, &len, "%s d.connected: 1", prefix1);
+			prefix1 = ",";
+		}
+		if (bzobj->d_device_paired) {
+			nm_utils_strbuf_append (&buf, &len, "%s d.paired: 1", prefix1);
+			prefix1 = ",";
+		}
+		nm_utils_strbuf_append_str (&buf, &len, " }");
+	}
+
+	network_server_is_usable = _bzobjs_network_server_is_usable (bzobj, TRUE);
+
+	if (   bzobj->d_has_network_server_iface
+	    || network_server_is_usable != (!c_list_is_empty (&bzobj->x_network_server.lst))
+	    || !c_list_is_empty (&bzobj->x_network_server.lst)
+	    || !nm_streq0 (bzobj->d_has_adapter_iface ? bzobj->d_adapter.address : NULL, bzobj->x_network_server.adapter_address)
+	    || bzobj->x_network_server.device_br
+	    || bzobj->x_network_server.r_req_data) {
+
+		nm_utils_strbuf_append_str (&buf, &len, prefix);
+		prefix = ", ";
+
+		nm_utils_strbuf_append (&buf, &len, "NetworkServer1 { ");
+
+		if (!bzobj->d_has_network_server_iface)
+			nm_utils_strbuf_append (&buf, &len, " has-d-iface: 0, ");
+
+		if (network_server_is_usable != (!c_list_is_empty (&bzobj->x_network_server.lst)))
+			nm_utils_strbuf_append (&buf, &len, "usable: %d, used: %d", !!network_server_is_usable, !network_server_is_usable);
+		else if (network_server_is_usable)
+			nm_utils_strbuf_append (&buf, &len, "used: 1");
+		else
+			nm_utils_strbuf_append (&buf, &len, "usable: 0");
+
+		if (!nm_streq0 (bzobj->d_has_adapter_iface ? bzobj->d_adapter.address : NULL, bzobj->x_network_server.adapter_address)) {
+			if (bzobj->x_network_server.adapter_address)
+				nm_utils_strbuf_append (&buf, &len, ", adapter-address: \"%s\"", bzobj->x_network_server.adapter_address);
+			else
+				nm_utils_strbuf_append (&buf, &len, ", adapter-address: <NULL>");
+		}
+
+		if (bzobj->x_network_server.device_br)
+			nm_utils_strbuf_append (&buf, &len, ", bridge-device: 1");
+
+		if (bzobj->x_network_server.r_req_data)
+			nm_utils_strbuf_append (&buf, &len, ", register-in-progress: 1");
+
+		nm_utils_strbuf_append_str (&buf, &len, " }");
+	}
+
+	device_is_usable = _bzobjs_device_is_usable (bzobj, NULL, &create_panu_connection);
+
+	if (   bzobj->d_has_network_iface
+	    || bzobj->d_network.interface
+	    || bzobj->d_network_connected
+	    || create_panu_connection
+	    || bzobj->x_device.panu_connection
+	    || device_is_usable != bzobj->x_device_is_usable
+	    || bzobj->x_device.device_bt
+	    || bzobj->x_device_connect_bt_type != NM_BT_CAPABILITY_NONE
+	    || bzobj->x_device.connect_dun_context
+	    || bzobj->x_device.c_req_data
+	    || bzobj->x_device_is_connected != bzobj->d_network_connected) {
+
+		nm_utils_strbuf_append_str (&buf, &len, prefix);
+		prefix = ", ";
+		nm_utils_strbuf_append_str (&buf, &len, "Network1 {");
+		if (bzobj->d_network.interface)
+			nm_utils_strbuf_append (&buf, &len, " d.interface: \"%s\", ", bzobj->d_network.interface);
+		if (bzobj->d_network_connected)
+			nm_utils_strbuf_append (&buf, &len, " d.connected: %d, ", !!bzobj->d_network_connected);
+		if (!bzobj->d_has_network_iface)
+			nm_utils_strbuf_append (&buf, &len, " has-d-iface: 0, ");
+		if (device_is_usable != bzobj->x_device_is_usable)
+			nm_utils_strbuf_append (&buf, &len, " usable: %d, used: %d", !!device_is_usable, !device_is_usable);
+		else if (device_is_usable)
+			nm_utils_strbuf_append (&buf, &len, " used: 1");
+		else
+			nm_utils_strbuf_append (&buf, &len, " usable: 0");
+
+		if (create_panu_connection)
+			nm_utils_strbuf_append (&buf, &len, ", create-panu-connection: 1");
+
+		if (bzobj->x_device.panu_connection)
+			nm_utils_strbuf_append (&buf, &len, ", has-panu-connection: 1");
+
+		if (bzobj->x_device.device_bt)
+			nm_utils_strbuf_append (&buf, &len, ", has-device: 1");
+
+		if (   bzobj->x_device_connect_bt_type != NM_BT_CAPABILITY_NONE
+		    || bzobj->x_device.connect_dun_context) {
+			nm_utils_strbuf_append (&buf, &len, ", connect: %s%s",
+			                        nm_bluetooth_capability_to_string (bzobj->x_device_connect_bt_type, sbuf_cap, sizeof (sbuf_cap)),
+			                        bzobj->x_device.connect_dun_context ? ",with-dun-context" : "");
+		}
+
+		if (bzobj->x_device.c_req_data)
+			nm_utils_strbuf_append (&buf, &len, ", connecting: 1");
+
+		if (bzobj->x_device_is_connected != bzobj->d_network_connected)
+			nm_utils_strbuf_append (&buf, &len, ", connected: %d", !!bzobj->x_device_is_connected);
+
+		nm_utils_strbuf_append_str (&buf, &len, " }");
+	}
+
+	if (_bzobjs_is_dead (bzobj)) {
+		nm_utils_strbuf_append_str (&buf, &len, prefix);
+		prefix = ", ";
+		nm_utils_strbuf_append_str (&buf, &len, "dead: 1");
+	}
+
+	if (!c_list_is_empty (&bzobj->process_change_lst)) {
+		nm_utils_strbuf_append_str (&buf, &len, prefix);
+		prefix = ", ";
+		nm_utils_strbuf_append (&buf, &len, "change-pending-on-idle: 1");
+	}
+
+	if (_bzobjs_adapter_is_usable_for_device (bzobj) != bzobj->was_usable_adapter_for_device_before) {
+		nm_utils_strbuf_append_str (&buf, &len, prefix);
+		prefix = ", ";
+		nm_utils_strbuf_append (&buf, &len, "change-usable-adapter-for-device: 1");
+	}
+
+	return buf0;
+}
+
+#define _LOG_bzobj(bzobj, context) \
+	G_STMT_START { \
+		const BzDBusObj *const _bzobj = (bzobj); \
+		char _buf[500]; \
+		\
+		_LOGT ("change %-21s %s : { %s }", \
+		       (context), \
+		       _bzobj->object_path, \
+		       _bzobj_to_string (_bzobj, _buf, sizeof (_buf))); \
+	} G_STMT_END
+
+static gboolean
+_bzobjs_is_dead (const BzDBusObj *bzobj)
+{
+	return    !bzobj->d_has_adapter_iface
+	       && !bzobj->d_has_device_iface
+	       && !bzobj->d_has_network_iface
+	       && !bzobj->d_has_network_server_iface
+	       && c_list_is_empty (&bzobj->process_change_lst);
+}
+
+static BzDBusObj *
+_bzobjs_get (NMBluezManager *self, const char *object_path)
+{
+	return g_hash_table_lookup (NM_BLUEZ_MANAGER_GET_PRIVATE (self)->bzobjs, &object_path);
+}
+
+static BzDBusObj *
+_bzobjs_add (NMBluezManager *self,
+             const char *object_path)
+{
+	NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE (self);
+	BzDBusObj *bzobj;
+
+	bzobj = _bz_dbus_obj_new (self, object_path);
+	if (!g_hash_table_add (priv->bzobjs, bzobj))
+		nm_assert_not_reached ();
+	return bzobj;
+}
+
+static void
+_bzobjs_del (BzDBusObj *bzobj)
+{
+	nm_assert (bzobj);
+	nm_assert (bzobj == _bzobjs_get (bzobj->self, bzobj->object_path));
+
+	if (!g_hash_table_remove (NM_BLUEZ_MANAGER_GET_PRIVATE (bzobj->self)->bzobjs, bzobj))
+		nm_assert_not_reached ();
+}
+
+static void
+_bzobjs_del_if_dead (BzDBusObj *bzobj)
+{
+	if (_bzobjs_is_dead (bzobj))
+		_bzobjs_del (bzobj);
+}
+
+static BzDBusObj *
+_bzobjs_init (NMBluezManager *self, BzDBusObj **inout, const char *object_path)
+{
+	nm_assert (NM_IS_BLUEZ_MANAGER (self));
+	nm_assert (object_path);
+	nm_assert (inout);
+
+	if (!*inout) {
+		*inout = _bzobjs_get (self, object_path);
+		if (!*inout)
+			*inout = _bzobjs_add (self, object_path);
+	}
+
+	nm_assert (nm_streq ((*inout)->object_path, object_path));
+	nm_assert (*inout == _bzobjs_get (self, object_path));
+	return *inout;
+}
+
+static gboolean
+_bzobjs_adapter_is_usable_for_device (const BzDBusObj *bzobj)
+{
+	return    bzobj->d_has_adapter_iface
+	       && bzobj->d_adapter.address
+	       && bzobj->d_adapter_powered;
+}
+
+static gboolean
+_bzobjs_device_is_usable (const BzDBusObj *bzobj,
+                          BzDBusObj **out_adapter_bzobj,
+                          gboolean *out_create_panu_connection)
+{
 	NMBluezManager *self;
-	GCancellable *async_cancellable;
-};
+	NMBluezManagerPrivate *priv;
+	gboolean usable_dun = FALSE;
+	gboolean usable_nap = FALSE;
+	BzDBusObj *bzobj_adapter;
+	gboolean create_panu_connection = FALSE;
+
+	if (   !bzobj->d_has_device_iface
+	    || !NM_FLAGS_ANY ((NMBluetoothCapabilities) bzobj->d_device_capabilities, _NM_BT_CAPABILITY_SUPPORTED)
+	    || !bzobj->d_device.name
+	    || !bzobj->d_device.address
+	    || !bzobj->d_device_paired
+	    || !bzobj->d_device.adapter)
+		goto out_unusable;
+
+	self = bzobj->self;
+
+	priv = NM_BLUEZ_MANAGER_GET_PRIVATE (self);
+
+	if (!priv->settings_registered)
+		goto out_unusable;
+
+	bzobj_adapter = _bzobjs_get (self, bzobj->d_device.adapter);
+	if (   !bzobj_adapter
+	    || !_bzobjs_adapter_is_usable_for_device (bzobj_adapter))
+		goto out_unusable;
+
+#if WITH_BLUEZ5_DUN
+	if (NM_FLAGS_HAS (bzobj->d_device_capabilities, NM_BT_CAPABILITY_DUN)) {
+		if (_conn_track_find_head (self, NM_BT_CAPABILITY_DUN, bzobj->d_device.address))
+			usable_dun = TRUE;
+	}
+#endif
+
+	if (NM_FLAGS_HAS (bzobj->d_device_capabilities, NM_BT_CAPABILITY_NAP)) {
+		if (!bzobj->d_has_network_iface)
+			usable_nap = FALSE;
+		else if (_conn_track_find_head (self, NM_BT_CAPABILITY_NAP, bzobj->d_device.address))
+			usable_nap = TRUE;
+		else if (bzobj->x_device_panu_connection_allow_create) {
+			/* We didn't yet try to create a connection. Presume we are going to create
+			 * it when the time comes... */
+			usable_nap = TRUE;
+			create_panu_connection = TRUE;
+		}
+	}
+
+	if (   !usable_dun
+	    && !usable_nap) {
+		if (   bzobj->x_device.device_bt
+		    && nm_device_get_state (NM_DEVICE (bzobj->x_device.device_bt)) > NM_DEVICE_STATE_DISCONNECTED) {
+			/* The device is still activated... the absence of a profile does not
+			 * render it unusable (yet). But since there is no more profile, the
+			 * device is probably about to disconnect. */
+		} else
+			goto out_unusable;
+	}
+
+	NM_SET_OUT (out_create_panu_connection, create_panu_connection);
+	NM_SET_OUT (out_adapter_bzobj, bzobj_adapter);
+	return TRUE;
 
-static struct AsyncData *
-async_data_pack (NMBluezManager *self)
+out_unusable:
+	NM_SET_OUT (out_create_panu_connection, FALSE);
+	NM_SET_OUT (out_adapter_bzobj, NULL);
+	return FALSE;
+}
+
+static gboolean
+_bzobjs_device_is_connected (const BzDBusObj *bzobj)
 {
-	struct AsyncData *data = g_new (struct AsyncData, 1);
+	nm_assert (_bzobjs_device_is_usable (bzobj, NULL, NULL));
+
+	if (   !bzobj->d_has_device_iface
+	    || !bzobj->d_device_connected)
+		return FALSE;
+
+	if (   bzobj->d_has_network_iface
+	    && bzobj->d_network_connected)
+		return TRUE;
+	if (bzobj->x_device.connect_dun_context) {
+		/* As long as we have a dun-context, we consider it connected.
+		 *
+		 * We require NMDeviceBt to try to connect to the modem, and if that fails,
+		 * it will disconnect. */
+		return TRUE;
+	}
+	return FALSE;
+}
 
-	data->self = self;
-	data->async_cancellable = g_object_ref (NM_BLUEZ_MANAGER_GET_PRIVATE (self)->async_cancellable);
-	return data;
+static gboolean
+_bzobjs_network_server_is_usable (const BzDBusObj *bzobj,
+                                  gboolean require_powered)
+{
+	return    bzobj->d_has_network_server_iface
+	       && bzobj->d_has_adapter_iface
+	       && bzobj->d_adapter.address
+	       && (   !require_powered
+	           || bzobj->d_adapter_powered);
 }
 
-static NMBluezManager *
-async_data_unpack (struct AsyncData *async_data)
+/*****************************************************************************/
+
+static ConnDataHead *
+_conn_data_head_new (NMBluetoothCapabilities bt_type,
+                     const char *bdaddr)
 {
-	NMBluezManager *self = g_cancellable_is_cancelled (async_data->async_cancellable)
-	                       ? NULL : async_data->self;
+	ConnDataHead *cdata_hd;
+	gsize l;
 
-	g_object_unref (async_data->async_cancellable);
-	g_free (async_data);
-	return self;
+	nm_assert (NM_IN_SET (bt_type, NM_BT_CAPABILITY_DUN,
+	                               NM_BT_CAPABILITY_NAP));
+	nm_assert (bdaddr);
+
+	l = strlen (bdaddr) + 1;
+	cdata_hd = g_malloc (sizeof (ConnDataHead) + l);
+	*cdata_hd = (ConnDataHead) {
+		.bdaddr   = cdata_hd->bdaddr_data,
+		.lst_head = C_LIST_INIT (cdata_hd->lst_head),
+		.bt_type  = bt_type,
+	};
+	memcpy (cdata_hd->bdaddr_data, bdaddr, l);
+
+	nm_assert (cdata_hd->bt_type == bt_type);
+
+	return cdata_hd;
+}
+
+static guint
+_conn_data_head_hash (gconstpointer ptr)
+{
+	const ConnDataHead *cdata_hd = ptr;
+	NMHashState h;
+
+	nm_hash_init (&h, 520317467u);
+	nm_hash_update_val (&h, (NMBluetoothCapabilities) cdata_hd->bt_type);
+	nm_hash_update_str (&h, cdata_hd->bdaddr);
+	return nm_hash_complete (&h);
+}
+
+static gboolean
+_conn_data_head_equal (gconstpointer a, gconstpointer b)
+{
+	const ConnDataHead *cdata_hd_a = a;
+	const ConnDataHead *cdata_hd_b = b;
+
+	return     cdata_hd_a->bt_type == cdata_hd_b->bt_type
+	        && nm_streq (cdata_hd_a->bdaddr, cdata_hd_b->bdaddr);
+}
+
+static ConnDataHead *
+_conn_track_find_head (NMBluezManager *self,
+                       NMBluetoothCapabilities bt_type,
+                       const char *bdaddr)
+{
+	ConnDataHead cdata_hd = {
+		.bt_type = bt_type,
+		.bdaddr  = bdaddr,
+	};
+
+	return g_hash_table_lookup (NM_BLUEZ_MANAGER_GET_PRIVATE (self)->conn_data_heads, &cdata_hd);
+}
+
+static ConnDataElem *
+_conn_track_find_elem (NMBluezManager *self,
+                       NMSettingsConnection *sett_conn)
+{
+	G_STATIC_ASSERT (G_STRUCT_OFFSET (ConnDataElem, sett_conn) == 0);
+
+	return g_hash_table_lookup (NM_BLUEZ_MANAGER_GET_PRIVATE (self)->conn_data_elems, &sett_conn);
+}
+
+static gboolean
+_conn_track_is_relevant_connection (NMConnection *connection,
+                                    NMBluetoothCapabilities *out_bt_type,
+                                    const char **out_bdaddr)
+{
+	NMSettingBluetooth *s_bt;
+	NMBluetoothCapabilities bt_type;
+	const char *bdaddr;
+	const char *b_type;
+
+	s_bt = nm_connection_get_setting_bluetooth (connection);
+	if (!s_bt)
+		return FALSE;
+
+	if (!nm_connection_is_type (connection, NM_SETTING_BLUETOOTH_SETTING_NAME))
+		return FALSE;
+
+	bdaddr = nm_setting_bluetooth_get_bdaddr (s_bt);
+	if (!bdaddr)
+		return FALSE;
+
+	b_type = nm_setting_bluetooth_get_connection_type (s_bt);
+
+	if (nm_streq (b_type, NM_SETTING_BLUETOOTH_TYPE_DUN))
+		bt_type = NM_BT_CAPABILITY_DUN;
+	else if (nm_streq (b_type, NM_SETTING_BLUETOOTH_TYPE_PANU))
+		bt_type = NM_BT_CAPABILITY_NAP;
+	else
+		return FALSE;
+
+	NM_SET_OUT (out_bt_type, bt_type);
+	NM_SET_OUT (out_bdaddr, bdaddr);
+	return TRUE;
+}
+
+static gboolean
+_conn_track_is_relevant_sett_conn (NMSettingsConnection *sett_conn,
+                                   NMBluetoothCapabilities *out_bt_type,
+                                   const char **out_bdaddr)
+{
+	NMConnection *connection;
+
+	connection = nm_settings_connection_get_connection (sett_conn);
+	if (!connection)
+		return FALSE;
+
+	return _conn_track_is_relevant_connection (connection, out_bt_type, out_bdaddr);
+}
+
+static gboolean
+_conn_track_is_relevant_for_sett_conn (NMSettingsConnection *sett_conn,
+                                       NMBluetoothCapabilities bt_type,
+                                       const char *bdaddr)
+{
+	NMBluetoothCapabilities x_bt_type;
+	const char *x_bdaddr;
+
+	return    bdaddr
+	       && _conn_track_is_relevant_sett_conn (sett_conn, &x_bt_type, &x_bdaddr)
+	       && x_bt_type == bt_type
+	       && nm_streq (x_bdaddr, bdaddr);
+}
+
+static void
+_conn_track_schedule_notify (NMBluezManager *self,
+                             NMBluetoothCapabilities bt_type,
+                             const char *bdaddr)
+{
+	NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE (self);
+	GHashTableIter iter;
+	BzDBusObj *bzobj;
+
+	g_hash_table_iter_init (&iter, priv->bzobjs);
+	while (g_hash_table_iter_next (&iter, (gpointer *) &bzobj, NULL)) {
+		gboolean device_is_usable;
+
+		device_is_usable = _bzobjs_device_is_usable (bzobj, NULL, NULL);
+		if (bzobj->x_device_is_usable != device_is_usable)
+			_process_change_idle_schedule (self, bzobj);
+	}
 }
 
-/**
- * Cancel any current attempt to detect the version and cleanup
- * the related fields.
- **/
 static void
-cleanup_checking (NMBluezManager *self, gboolean do_unwatch_name)
+_conn_track_update (NMBluezManager *self,
+                    NMSettingsConnection *sett_conn,
+                    gboolean track,
+                    gboolean *out_changed,
+                    gboolean *out_changed_usable,
+                    ConnDataElem **out_conn_data_elem)
 {
 	NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE (self);
+	ConnDataHead *cdata_hd;
+	ConnDataElem *cdata_el;
+	ConnDataElem *cdata_el_remove = NULL;
+	NMBluetoothCapabilities bt_type;
+	const char *bdaddr;
+	gboolean changed = FALSE;
+	gboolean changed_usable = FALSE;
+	char sbuf_cap[100];
+
+	nm_assert (NM_IS_SETTINGS_CONNECTION (sett_conn));
+
+	cdata_el = _conn_track_find_elem (self, sett_conn);
+
+	if (track)
+		track = _conn_track_is_relevant_sett_conn (sett_conn, &bt_type, &bdaddr);
 
-	nm_clear_g_cancellable (&priv->async_cancellable);
+	if (!track) {
+		cdata_el_remove = g_steal_pointer (&cdata_el);
+		goto out_remove;
+	}
+
+	if (cdata_el) {
+		cdata_hd = cdata_el->cdata_hd;
+		if (   cdata_hd->bt_type != bt_type
+	        || !nm_streq (cdata_hd->bdaddr, bdaddr))
+			cdata_el_remove = g_steal_pointer (&cdata_el);
+	}
+
+	if (!cdata_el) {
+		_LOGT ("connecton: track for %s, %s: %s (%s)",
+		       nm_bluetooth_capability_to_string (bt_type, sbuf_cap, sizeof (sbuf_cap)),
+		       bdaddr,
+		       nm_settings_connection_get_uuid (sett_conn),
+		       nm_settings_connection_get_id (sett_conn));
+		changed = TRUE;
+		cdata_hd = _conn_track_find_head (self, bt_type, bdaddr);
+		if (!cdata_hd) {
+			changed_usable = TRUE;
+			cdata_hd = _conn_data_head_new (bt_type, bdaddr);
+			if (!g_hash_table_add (priv->conn_data_heads, cdata_hd))
+				nm_assert_not_reached ();
+			_conn_track_schedule_notify (self, bt_type, bdaddr);
+		}
+		cdata_el = g_slice_new (ConnDataElem);
+		cdata_el->sett_conn = sett_conn;
+		cdata_el->cdata_hd = cdata_hd;
+		c_list_link_tail (&cdata_hd->lst_head, &cdata_el->lst);
+		if (!g_hash_table_add (priv->conn_data_elems, cdata_el))
+			nm_assert_not_reached ();
+	}
 
-	g_clear_object (&priv->introspect_proxy);
+out_remove:
+	if (cdata_el_remove) {
+		GHashTableIter iter;
+		BzDBusObj *bzobj;
+
+		_LOGT ("connecton: untrack for %s, %s: %s (%s)",
+		       nm_bluetooth_capability_to_string (cdata_el_remove->cdata_hd->bt_type, sbuf_cap, sizeof (sbuf_cap)),
+		       cdata_el_remove->cdata_hd->bdaddr,
+		       nm_settings_connection_get_uuid (sett_conn),
+		       nm_settings_connection_get_id (sett_conn));
+
+		g_hash_table_iter_init (&iter, priv->bzobjs);
+		while (g_hash_table_iter_next (&iter, (gpointer *) &bzobj, NULL)) {
+			if (bzobj->x_device.panu_connection == sett_conn)
+				bzobj->x_device.panu_connection = NULL;
+		}
 
-	if (do_unwatch_name && priv->watch_name_id) {
-		g_bus_unwatch_name (priv->watch_name_id);
-		priv->watch_name_id = 0;
+		changed = TRUE;
+		cdata_hd = cdata_el_remove->cdata_hd;
+		c_list_unlink_stale (&cdata_el_remove->lst);
+		if (!g_hash_table_remove (priv->conn_data_elems, cdata_el_remove))
+			nm_assert_not_reached ();
+		if (c_list_is_empty (&cdata_hd->lst_head)) {
+			changed_usable = TRUE;
+			_conn_track_schedule_notify (self, cdata_hd->bt_type, cdata_hd->bdaddr);
+			if (!g_hash_table_remove (priv->conn_data_heads, cdata_hd))
+				nm_assert_not_reached ();
+		}
 	}
+
+	NM_SET_OUT (out_changed, changed);
+	NM_SET_OUT (out_changed_usable, changed_usable);
+	NM_SET_OUT (out_conn_data_elem, cdata_el);
+}
+
+/*****************************************************************************/
+
+static void
+cp_connection_added (NMSettings *settings,
+                     NMSettingsConnection *sett_conn,
+                     NMBluezManager *self)
+{
+	_conn_track_update (self, sett_conn, TRUE, NULL, NULL, NULL);
 }
 
 static void
-manager_bdaddr_added_cb (GObject *manager,
-                         NMBluezDevice *bt_device,
-                         const char *bdaddr,
-                         const char *name,
-                         const char *object_path,
-                         guint32 capabilities,
-                         gpointer user_data)
+cp_connection_updated (NMSettings *settings,
+                       NMSettingsConnection *sett_conn,
+                       guint update_reason_u,
+                       NMBluezManager *self)
 {
-	NMBluezManager *self = NM_BLUEZ_MANAGER (user_data);
-	NMDevice *device;
-	gboolean has_dun = (capabilities & NM_BT_CAPABILITY_DUN);
-	gboolean has_nap = (capabilities & NM_BT_CAPABILITY_NAP);
+	_conn_track_update (self, sett_conn, TRUE, NULL, NULL, NULL);
+}
 
-	g_return_if_fail (bdaddr != NULL);
-	g_return_if_fail (name != NULL);
-	g_return_if_fail (object_path != NULL);
-	g_return_if_fail (capabilities != NM_BT_CAPABILITY_NONE);
-	g_return_if_fail (NM_IS_BLUEZ_DEVICE (bt_device));
+static void
+cp_connection_removed (NMSettings *settings,
+                       NMSettingsConnection *sett_conn,
+                       NMBluezManager *self)
+{
+	_conn_track_update (self, sett_conn, FALSE, NULL, NULL, NULL);
+}
+
+/*****************************************************************************/
 
-	device = nm_device_bt_new (bt_device, object_path, bdaddr, name, capabilities);
-	if (!device)
+static NMBluezManager *
+_network_server_get_bluez_manager (const NMBtVTableNetworkServer *vtable_network_server)
+{
+	NMBluezManager *self;
+
+	self = (NMBluezManager *) (((char *) vtable_network_server) - G_STRUCT_OFFSET (NMBluezManager, _priv.vtable_network_server));
+
+	g_return_val_if_fail (NM_IS_BLUEZ_MANAGER (self), NULL);
+
+	return self;
+}
+
+static BzDBusObj *
+_network_server_find_has_device (NMBluezManagerPrivate *priv,
+                                 NMDevice *device)
+{
+	BzDBusObj *bzobj;
+
+	c_list_for_each_entry (bzobj, &priv->network_server_lst_head, x_network_server.lst) {
+		if (bzobj->x_network_server.device_br == device)
+			return bzobj;
+	}
+	return NULL;
+}
+
+static BzDBusObj *
+_network_server_find_available (NMBluezManagerPrivate *priv,
+                                const char *addr,
+                                NMDevice *device_accept_busy)
+{
+	BzDBusObj *bzobj;
+
+	c_list_for_each_entry (bzobj, &priv->network_server_lst_head, x_network_server.lst) {
+		if (bzobj->x_network_server.device_br) {
+			if (bzobj->x_network_server.device_br != device_accept_busy)
+				continue;
+		}
+		if (   addr
+		    && !nm_streq (addr, bzobj->d_adapter.address))
+			continue;
+		nm_assert (!bzobj->x_network_server.r_req_data);
+		return bzobj;
+	}
+	return NULL;
+}
+
+static gboolean
+_network_server_vt_is_available (const NMBtVTableNetworkServer *vtable,
+                                 const char *addr,
+                                 NMDevice *device_accept_busy)
+{
+	NMBluezManager *self = _network_server_get_bluez_manager (vtable);
+	NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE (self);
+
+	return !!_network_server_find_available (priv, addr, device_accept_busy);
+}
+
+static void
+_network_server_register_cb (GObject *source_object,
+                             GAsyncResult *res,
+                             gpointer user_data)
+{
+	gs_unref_variant GVariant *ret = NULL;
+	gs_free_error GError *error = NULL;
+	BzDBusObj *bzobj;
+
+	ret = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source_object), res, &error);
+	if (   !ret
+	    && nm_utils_error_is_cancelled (error, FALSE))
 		return;
 
-	_LOGI ("BT device %s (%s) added (%s%s%s)",
-	       name,
-	       bdaddr,
-	       has_dun ? "DUN" : "",
-	       has_dun && has_nap ? " " : "",
-	       has_nap ? "NAP" : "");
-	g_signal_emit_by_name (self, NM_DEVICE_FACTORY_DEVICE_ADDED, device);
-	g_object_unref (device);
+	bzobj = user_data;
+
+	if (!ret) {
+		_LOGT ("NAP: [%s]: registering failed: %s", bzobj->object_path, error->message);
+	} else
+		_LOGT ("NAP: [%s]: registration successful", bzobj->object_path);
+
+	g_clear_object (&bzobj->x_network_server.r_req_data->int_cancellable);
+	_network_server_register_req_data_complete (g_steal_pointer (&bzobj->x_network_server.r_req_data), error);
+}
+
+static void
+_network_server_register_cancelled_cb (GCancellable *cancellable,
+                                       BzDBusObj *bzobj)
+{
+	_network_server_unregister_bridge (bzobj->self, bzobj, "registration cancelled");
+}
+
+static gboolean
+_network_server_vt_register_bridge (const NMBtVTableNetworkServer *vtable,
+                                    const char *addr,
+                                    NMDevice *device,
+                                    GCancellable *cancellable,
+                                    NMBtVTableRegisterCallback callback,
+                                    gpointer callback_user_data,
+                                    GError **error)
+{
+	NMBluezManager *self = _network_server_get_bluez_manager (vtable);
+	NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE (self);
+	NetworkServerRegisterReqData *r_req_data;
+	BzDBusObj *bzobj;
+	const char *ifname;
+
+	g_return_val_if_fail (NM_IS_DEVICE (device), FALSE);
+	g_return_val_if_fail (G_IS_CANCELLABLE (cancellable), FALSE);
+
+	nm_assert (!g_cancellable_is_cancelled (cancellable));
+	nm_assert (!_network_server_find_has_device (priv, device));
+
+	ifname = nm_device_get_iface (device);
+	g_return_val_if_fail (ifname, FALSE);
+
+	g_return_val_if_fail (ifname, FALSE);
+
+	bzobj = _network_server_find_available (priv, addr, NULL);
+	if (!bzobj) {
+		/* The device checked that a network server is available, before
+		 * starting the activation, but for some reason it no longer is.
+		 * Indicate that the activation should not proceed. */
+		if (addr) {
+			nm_utils_error_set (error, NM_UTILS_ERROR_UNKNOWN,
+			                    "adapter %s is not available for %s",
+			                    addr, ifname);
+		} else {
+			nm_utils_error_set (error, NM_UTILS_ERROR_UNKNOWN,
+			                    "no adapter available for %s",
+			                    ifname);
+		}
+		return FALSE;
+	}
+
+	_LOGD ("NAP: [%s]: registering \"%s\" on adapter %s",
+	       bzobj->object_path,
+	       ifname,
+	       bzobj->d_adapter.address);
+
+	r_req_data = g_slice_new (NetworkServerRegisterReqData);
+	*r_req_data = (NetworkServerRegisterReqData) {
+		.int_cancellable     = g_cancellable_new (),
+		.ext_cancellable     = g_object_ref (cancellable),
+		.callback            = callback,
+		.callback_user_data  = callback_user_data,
+		.ext_cancelled_id    = g_signal_connect (cancellable,
+		                                         "cancelled",
+		                                         G_CALLBACK (_network_server_register_cancelled_cb),
+		                                         bzobj),
+	};
+
+	bzobj->x_network_server.device_br = g_object_ref (device);
+	bzobj->x_network_server.r_req_data = r_req_data;
+
+	g_dbus_connection_call (priv->dbus_connection,
+	                        priv->name_owner,
+	                        bzobj->object_path,
+	                        NM_BLUEZ5_NETWORK_SERVER_INTERFACE,
+	                        "Register",
+	                        g_variant_new ("(ss)",
+	                                       BLUETOOTH_CONNECT_NAP,
+	                                       ifname),
+	                        NULL,
+	                        G_DBUS_CALL_FLAGS_NO_AUTO_START,
+	                        -1,
+	                        bzobj->x_network_server.r_req_data->int_cancellable,
+	                        _network_server_register_cb,
+	                        bzobj);
+	return TRUE;
+}
+
+static void
+_network_server_unregister_bridge_complete_on_idle_cb (gpointer user_data,
+                                                       GCancellable *cancellable)
+{
+	gs_free_error GError *error = NULL;
+	gs_free char *reason  = NULL;
+	NetworkServerRegisterReqData *r_req_data;
+
+	nm_utils_user_data_unpack (user_data, &r_req_data, &reason);
+
+	nm_utils_error_set (&error, NM_UTILS_ERROR_UNKNOWN,
+	                    "registration was aborted due to %s",
+	                    reason);
+	_network_server_register_req_data_complete (r_req_data, error);
 }
 
 static void
-manager_network_server_added_cb (GObject *manager,
-                                 gpointer user_data)
+_network_server_unregister_bridge (NMBluezManager *self,
+                                   BzDBusObj *bzobj,
+                                   const char *reason)
+{
+	NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE (self);
+	_nm_unused gs_unref_object NMDevice *device = NULL;
+	NetworkServerRegisterReqData *r_req_data;
+
+	nm_assert (NM_IS_DEVICE (bzobj->x_network_server.device_br));
+
+	_LOGD ("NAP: [%s]: unregistering \"%s\" (%s)",
+	       bzobj->object_path,
+	       nm_device_get_iface (bzobj->x_network_server.device_br),
+	       reason);
+
+	device = g_steal_pointer (&bzobj->x_network_server.device_br);
+
+	r_req_data = g_steal_pointer (&bzobj->x_network_server.r_req_data);
+
+	if (priv->name_owner) {
+		gs_unref_object GCancellable *cancellable = NULL;
+
+		cancellable = g_cancellable_new ();
+
+		nm_shutdown_wait_obj_register_cancellable_full (cancellable,
+		                                                g_strdup_printf ("bt-unregister-nap[%s]", bzobj->object_path),
+		                                                TRUE);
+
+		g_dbus_connection_call (priv->dbus_connection,
+		                        priv->name_owner,
+		                        bzobj->object_path,
+		                        NM_BLUEZ5_NETWORK_SERVER_INTERFACE,
+		                        "Unregister",
+		                        g_variant_new ("(s)", BLUETOOTH_CONNECT_NAP),
+		                        NULL,
+		                        G_DBUS_CALL_FLAGS_NO_AUTO_START,
+		                        -1,
+		                        cancellable,
+		                        _dbus_call_complete_cb_nop,
+		                        NULL);
+	}
+
+	if (r_req_data) {
+		nm_clear_g_cancellable (&r_req_data->int_cancellable);
+		nm_utils_invoke_on_idle (_network_server_unregister_bridge_complete_on_idle_cb,
+		                         nm_utils_user_data_pack (r_req_data, g_strdup (reason)),
+		                         r_req_data->ext_cancellable);
+	}
+
+	_nm_device_bridge_notify_unregister_bt_nap (device, reason);
+}
+
+static gboolean
+_network_server_vt_unregister_bridge (const NMBtVTableNetworkServer *vtable,
+                                      NMDevice *device)
 {
-	nm_device_factory_emit_component_added (NM_DEVICE_FACTORY (user_data), NULL);
+	NMBluezManager *self = _network_server_get_bluez_manager (vtable);
+	NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE (self);
+	BzDBusObj *bzobj;
+
+	g_return_val_if_fail (NM_IS_DEVICE (device), FALSE);
+
+	bzobj = _network_server_find_has_device (priv, device);
+	if (bzobj)
+		_network_server_unregister_bridge (self, bzobj, "disconnecting");
+
+	return TRUE;
 }
 
 static void
-setup_version_number (NMBluezManager *self, int bluez_version)
+_network_server_process_change (BzDBusObj *bzobj,
+                                gboolean *out_emit_device_availability_changed)
 {
+	NMBluezManager *self = bzobj->self;
 	NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE (self);
+	gboolean network_server_is_usable;
+	gboolean emit_device_availability_changed = FALSE;
+
+	network_server_is_usable = _bzobjs_network_server_is_usable (bzobj, TRUE);
 
-	g_return_if_fail (!priv->bluez_version);
+	if (!network_server_is_usable) {
 
-	_LOGI ("use BlueZ version %d", bluez_version);
+		if (!c_list_is_empty (&bzobj->x_network_server.lst)) {
+			emit_device_availability_changed = TRUE;
+			c_list_unlink (&bzobj->x_network_server.lst);
+		}
 
-	priv->bluez_version = bluez_version;
+		nm_clear_g_free (&bzobj->x_network_server.adapter_address);
+
+		if (bzobj->x_network_server.device_br) {
+			_network_server_unregister_bridge (self,
+			                                   bzobj,
+			                                     _bzobjs_network_server_is_usable (bzobj, FALSE)
+			                                   ? "adapter disabled"
+			                                   : "adapter disappeared");
+		}
+
+	} else {
+
+		if (!nm_streq0 (bzobj->x_network_server.adapter_address, bzobj->d_adapter.address)) {
+			emit_device_availability_changed = TRUE;
+			g_free (bzobj->x_network_server.adapter_address);
+			bzobj->x_network_server.adapter_address = g_strdup (bzobj->d_adapter.address);
+		}
 
-	/* Just detected the version. Cleanup the ongoing checking/detection. */
-	cleanup_checking (self, TRUE);
+		if (c_list_is_empty (&bzobj->x_network_server.lst)) {
+			emit_device_availability_changed = TRUE;
+			c_list_link_tail (&priv->network_server_lst_head, &bzobj->x_network_server.lst);
+		}
+
+	}
+
+	if (emit_device_availability_changed)
+		NM_SET_OUT (out_emit_device_availability_changed, TRUE);
 }
 
+/*****************************************************************************/
+
 static void
-setup_bluez4 (NMBluezManager *self)
+_conn_create_panu_connection (NMBluezManager *self,
+                              BzDBusObj *bzobj)
 {
-	NMBluez4Manager *manager;
 	NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE (self);
+	gs_unref_object NMConnection *connection = NULL;
+	NMSettingsConnection *added;
+	NMSetting *setting;
+	gs_free char *id = NULL;
+	char uuid[37];
+	gs_free_error GError *error = NULL;
+
+	nm_utils_uuid_generate_buf (uuid);
+	id = g_strdup_printf (_("%s Network"), bzobj->d_device.name);
+
+	connection = nm_simple_connection_new ();
+
+	setting = nm_setting_connection_new ();
+	g_object_set (setting,
+	              NM_SETTING_CONNECTION_ID, id,
+	              NM_SETTING_CONNECTION_UUID, uuid,
+	              NM_SETTING_CONNECTION_AUTOCONNECT, FALSE,
+	              NM_SETTING_CONNECTION_TYPE, NM_SETTING_BLUETOOTH_SETTING_NAME,
+	              NULL);
+	nm_connection_add_setting (connection, setting);
+
+	setting = nm_setting_bluetooth_new ();
+	g_object_set (setting,
+	              NM_SETTING_BLUETOOTH_BDADDR, bzobj->d_device.address,
+	              NM_SETTING_BLUETOOTH_TYPE, NM_SETTING_BLUETOOTH_TYPE_PANU,
+	              NULL);
+	nm_connection_add_setting (connection, setting);
+
+	if (!nm_connection_normalize (connection, NULL, NULL, &error)) {
+		_LOGE ("connection: couldn't generate a connection for NAP device: %s",
+		       error->message);
+		g_return_if_reached ();
+	}
 
-	g_return_if_fail (!priv->manager4 && !priv->manager5 && !priv->bluez_version);
+	nm_assert (_conn_track_is_relevant_connection (connection, NULL, NULL));
+
+	_LOGT ("connection: create in-memory PANU connection %s (%s) for device \"%s\" (%s)",
+	       uuid,
+	       id,
+	       bzobj->d_device.name,
+	       bzobj->d_device.address);
+
+	nm_settings_add_connection (priv->settings,
+	                            connection,
+	                            NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY_ONLY,
+	                            NM_SETTINGS_CONNECTION_ADD_REASON_NONE,
+	                            NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED,
+	                            &added,
+	                            &error);
+	if (!added) {
+		_LOGW ("connection: couldn't add new Bluetooth connection for NAP device: '%s' (%s): %s",
+		       id, uuid, error->message);
+		return;
+	}
 
-	setup_version_number (self, 4);
-	priv->manager4 = manager = nm_bluez4_manager_new (priv->settings);
+	if (   !_conn_track_is_relevant_for_sett_conn (added, NM_BT_CAPABILITY_NAP, bzobj->d_device.address)
+	    || !_conn_track_find_elem (self, added)
+	    || bzobj->x_device.panu_connection) {
+		_LOGE ("connection: something went wrong creating PANU connection %s (%s) for device '%s'",
+		       uuid, id, bzobj->d_device.address);
+		g_return_if_reached ();
+	}
 
-	g_signal_connect (manager,
-	                  NM_BLUEZ_MANAGER_BDADDR_ADDED,
-	                  G_CALLBACK (manager_bdaddr_added_cb),
-	                  self);
+	bzobj->x_device.panu_connection = added;
+}
+
+/*****************************************************************************/
+
+static void
+_device_state_changed_cb (NMDevice *device,
+                          guint new_state_u,
+                          guint old_state_u,
+                          guint reason_u,
+                          gpointer user_data)
+{
+	BzDBusObj *bzobj = user_data;
 
-	nm_bluez4_manager_query_devices (manager);
+	if (!_bzobjs_device_is_usable (bzobj, NULL, NULL)) {
+		/* the device got unusable? Need to revisit it... */
+		_process_change_idle_schedule (bzobj->self, bzobj);
+	}
 }
 
 static void
-setup_bluez5 (NMBluezManager *self)
+_device_process_change (BzDBusObj *bzobj)
+{
+	NMBluezManager *self = bzobj->self;
+	gs_unref_object NMDeviceBt *device_added = NULL;
+	gs_unref_object NMDeviceBt *device_deleted = NULL;
+	gboolean device_is_usable;
+	gboolean create_panu_connection = FALSE;
+
+	device_is_usable = _bzobjs_device_is_usable (bzobj, NULL, &create_panu_connection);
+
+	if (create_panu_connection) {
+		bzobj->x_device_panu_connection_allow_create = FALSE;
+		_conn_create_panu_connection (self, bzobj);
+		device_is_usable = _bzobjs_device_is_usable (bzobj, NULL, NULL);
+	} else {
+		if (   device_is_usable
+		    && bzobj->x_device_panu_connection_allow_create
+		    && NM_FLAGS_HAS (bzobj->d_device_capabilities, NM_BT_CAPABILITY_NAP)
+		    && _conn_track_find_head (self, NM_BT_CAPABILITY_NAP, bzobj->d_device.address) ) {
+			/* We have a useable device and also a panu-connection. We block future attemps
+			 * to generate a connection. */
+			bzobj->x_device_panu_connection_allow_create = FALSE;
+		}
+		if (bzobj->x_device.panu_connection) {
+			if (!NM_FLAGS_HAS (nm_settings_connection_get_flags (bzobj->x_device.panu_connection),
+			                   NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED)) {
+				/* the connection that we generated earlier still exists, but it's not longer the same
+				 * as it was when we created it. Forget about it, so that we don't delete the profile later... */
+				bzobj->x_device.panu_connection = NULL;
+			} else {
+				if (   !device_is_usable
+				    || !_conn_track_is_relevant_for_sett_conn (bzobj->x_device.panu_connection,
+				                                               NM_BT_CAPABILITY_NAP,
+				                                               bzobj->d_device.address)) {
+					_LOGT ("connection: delete in-memory PANU connection %s (%s) as device %s",
+					       nm_settings_connection_get_uuid (bzobj->x_device.panu_connection),
+					       nm_settings_connection_get_id (bzobj->x_device.panu_connection),
+					       !device_is_usable ? "is now unusable" : "no longer matches");
+					bzobj->x_device_panu_connection_allow_create = TRUE;
+					nm_settings_connection_delete (g_steal_pointer (&bzobj->x_device.panu_connection), FALSE);
+				}
+			}
+		}
+	}
+
+	bzobj->x_device_is_connected =    device_is_usable
+	                               && _bzobjs_device_is_connected (bzobj);
+
+	bzobj->x_device_is_usable = device_is_usable;
+
+	if (bzobj->x_device.device_bt) {
+		const char *device_to_delete_msg;
+
+		if (!device_is_usable)
+			device_to_delete_msg = "device became unusable";
+		else if (!_nm_device_bt_for_same_device (bzobj->x_device.device_bt,
+		                                         bzobj->object_path,
+		                                         bzobj->d_device.address,
+		                                         NULL,
+		                                         bzobj->d_device_capabilities))
+			device_to_delete_msg = "device is no longer compatible";
+		else
+			device_to_delete_msg = NULL;
+
+		if (device_to_delete_msg) {
+			nm_clear_g_signal_handler (bzobj->x_device.device_bt, &bzobj->x_device.device_bt_signal_id);
+
+			device_deleted = g_steal_pointer (&bzobj->x_device.device_bt);
+
+			_LOGD ("[%s]: drop device because %s",
+			       bzobj->object_path,
+			       device_to_delete_msg);
+
+			_connect_disconnect (self, bzobj, device_to_delete_msg);
+		}
+	}
+
+	if (device_is_usable) {
+		if (!bzobj->x_device.device_bt) {
+			bzobj->x_device.device_bt = nm_device_bt_new (self,
+			                                              bzobj->object_path,
+			                                              bzobj->d_device.address,
+			                                              bzobj->d_device.name,
+			                                              bzobj->d_device_capabilities);
+			device_added = g_object_ref (bzobj->x_device.device_bt);
+			bzobj->x_device.device_bt_signal_id = g_signal_connect (device_added,
+			                                                        NM_DEVICE_STATE_CHANGED,
+			                                                        G_CALLBACK (_device_state_changed_cb),
+			                                                        bzobj);
+		} else
+			_nm_device_bt_notify_set_name (bzobj->x_device.device_bt, bzobj->d_device.name);
+
+		_nm_device_bt_notify_set_connected (bzobj->x_device.device_bt, bzobj->x_device_is_connected);
+	}
+
+	if (   bzobj->x_device.c_req_data
+	    && !bzobj->x_device.c_req_data->int_cancellable
+	    && bzobj->x_device_is_connected) {
+		gs_free char *device_name = g_steal_pointer (&bzobj->x_device.c_req_data->device_name);
+
+		_device_connect_req_data_complete (g_steal_pointer (&bzobj->x_device.c_req_data),
+		                                   self,
+		                                   device_name,
+		                                   NULL);
+	}
+
+	if (device_added)
+		g_signal_emit_by_name (self, NM_DEVICE_FACTORY_DEVICE_ADDED, device_added);
+
+	if (device_deleted)
+		_nm_device_bt_notify_removed (device_deleted);
+}
+
+/*****************************************************************************/
+
+static void
+_process_change_idle_all (NMBluezManager *self,
+                          gboolean *out_emit_device_availability_changed)
 {
-	NMBluez5Manager *manager;
 	NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE (self);
+	BzDBusObj *bzobj;
+
+	while ((bzobj = c_list_first_entry (&priv->process_change_lst_head, BzDBusObj, process_change_lst))) {
+
+		c_list_unlink (&bzobj->process_change_lst);
+
+		_LOG_bzobj (bzobj, "before-processing");
+
+		_device_process_change (bzobj);
+
+		_network_server_process_change (bzobj, out_emit_device_availability_changed);
+
+		_LOG_bzobj (bzobj, "after-processing");
+
+		_bzobjs_del_if_dead (bzobj);
+	}
+
+	nm_clear_g_source (&priv->process_change_idle_id);
+}
 
-	g_return_if_fail (!priv->manager4 && !priv->manager5 && !priv->bluez_version);
+static gboolean
+_process_change_idle_cb (gpointer user_data)
+{
+	NMBluezManager *self = user_data;
+	NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE (self);
+	gboolean emit_device_availability_changed = FALSE;
 
-	setup_version_number (self, 5);
-	priv->manager5 = manager = nm_bluez5_manager_new (priv->settings);
+	_process_change_idle_all (self, &emit_device_availability_changed);
 
-	g_signal_connect (manager,
-	                  NM_BLUEZ_MANAGER_BDADDR_ADDED,
-	                  G_CALLBACK (manager_bdaddr_added_cb),
-	                  self);
-	g_signal_connect (manager,
-	                  NM_BLUEZ_MANAGER_NETWORK_SERVER_ADDED,
-	                  G_CALLBACK (manager_network_server_added_cb),
-	                  self);
+	if (emit_device_availability_changed)
+		nm_manager_notify_device_availibility_maybe_changed (priv->manager);
 
-	nm_bluez5_manager_query_devices (manager);
+	return G_SOURCE_CONTINUE;
 }
 
 static void
-watch_name_on_appeared (GDBusConnection *connection,
-                        const char *name,
-                        const char *name_owner,
-                        gpointer user_data)
+_process_change_idle_schedule (NMBluezManager *self,
+                               BzDBusObj *bzobj)
 {
-	check_bluez_and_try_setup (NM_BLUEZ_MANAGER (user_data));
+	NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE (self);
+
+	nm_c_list_move_tail (&priv->process_change_lst_head, &bzobj->process_change_lst);
+	if (priv->process_change_idle_id == 0)
+		priv->process_change_idle_id = g_idle_add_full (G_PRIORITY_DEFAULT_IDLE + 1, _process_change_idle_cb, self, NULL);
 }
 
 static void
-check_bluez_and_try_setup_final_step (NMBluezManager *self, int bluez_version, const char *reason)
+_dbus_process_changes (NMBluezManager *self,
+                       BzDBusObj *bzobj,
+                       const char *log_reason)
 {
 	NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE (self);
+	gboolean network_server_is_usable;
+	gboolean adapter_is_usable_for_device;
+	gboolean device_is_usable;
+	gboolean changes = FALSE;
+	gboolean recheck_devices_for_adapter = FALSE;
+
+	nm_assert (bzobj);
+
+	_LOG_bzobj (bzobj, log_reason);
+
+	device_is_usable = _bzobjs_device_is_usable (bzobj, NULL, NULL);
+
+	if (bzobj->x_device_is_usable != device_is_usable)
+		changes = TRUE;
+	else if (bzobj->x_device.device_bt) {
+		if (!device_is_usable)
+			changes = TRUE;
+		else {
+			if (   bzobj->x_device_is_connected != _bzobjs_device_is_connected (bzobj)
+			    || !_nm_device_bt_for_same_device (bzobj->x_device.device_bt,
+			                                       bzobj->object_path,
+			                                       bzobj->d_device.address,
+			                                       bzobj->d_device.name,
+			                                       bzobj->d_device_capabilities))
+				changes = TRUE;
+		}
+	}
+
+	adapter_is_usable_for_device = _bzobjs_adapter_is_usable_for_device (bzobj);
+	if (adapter_is_usable_for_device != bzobj->was_usable_adapter_for_device_before) {
+		/* this function does not modify bzobj in any other cases except here.
+		 * Usually changes are processed delayed, in the idle handler.
+		 *
+		 * But the bzobj->was_usable_adapter_for_device_before only exists to know whether
+		 * we need to re-check device availability. It is correct to set the flag
+		 * here, right before we checked. */
+		bzobj->was_usable_adapter_for_device_before = adapter_is_usable_for_device;
+		recheck_devices_for_adapter = TRUE;
+		changes = TRUE;
+	}
+
+	if (!changes) {
+		network_server_is_usable = _bzobjs_network_server_is_usable (bzobj, TRUE);
+
+		if (network_server_is_usable != (!c_list_is_empty (&bzobj->x_network_server.lst)))
+			changes = TRUE;
+		else if (   bzobj->x_network_server.device_br
+		         && !network_server_is_usable)
+			changes = TRUE;
+		else if (!nm_streq0 (bzobj->d_has_adapter_iface ? bzobj->d_adapter.address : NULL,
+		                     bzobj->x_network_server.adapter_address))
+			changes = TRUE;
+	}
+
+	if (changes)
+		_process_change_idle_schedule (self, bzobj);
+
+	if (recheck_devices_for_adapter) {
+		GHashTableIter iter;
+		BzDBusObj *bzobj2;
+
+		/* we got a change to the availability of an adapter. We might need to recheck
+		 * all devices that use this adapter... */
+		g_hash_table_iter_init (&iter, priv->bzobjs);
+		while (g_hash_table_iter_next (&iter, (gpointer *) &bzobj2, NULL)) {
+			if (bzobj2 == bzobj)
+				continue;
+			if (!nm_streq0 (bzobj2->d_device.adapter, bzobj->object_path))
+				continue;
+			if (c_list_is_empty (&bzobj2->process_change_lst))
+				_dbus_process_changes (self, bzobj2, "adapter-changed");
+			else
+				nm_c_list_move_tail (&priv->process_change_lst_head, &bzobj2->process_change_lst);
+		}
+	}
+
+	_bzobjs_del_if_dead (bzobj);
+}
+
+/*****************************************************************************/
+
+#define ALL_RELEVANT_INTERFACE_NAMES NM_MAKE_STRV (NM_BLUEZ5_ADAPTER_INTERFACE, \
+                                                   NM_BLUEZ5_DEVICE_INTERFACE, \
+                                                   NM_BLUEZ5_NETWORK_INTERFACE, \
+                                                   NM_BLUEZ5_NETWORK_SERVER_INTERFACE)
+
+static gboolean
+_dbus_handle_properties_changed (NMBluezManager *self,
+                                 const char *object_path,
+                                 const char *interface_name,
+                                 GVariant *changed_properties,
+                                 const char *const*invalidated_properties,
+                                 BzDBusObj **inout_bzobj)
+{
+	BzDBusObj *bzobj = NULL;
+	gboolean changed = FALSE;
+	const char *property_name;
+	GVariant *property_value;
+	GVariantIter iter_prop;
+	gsize i;
 
-	g_return_if_fail (!priv->bluez_version);
+	if (!invalidated_properties)
+		invalidated_properties = NM_PTRARRAY_EMPTY (const char *);
 
-	switch (bluez_version) {
-	case 4:
-		setup_bluez4 (self);
-		break;
-	case 5:
-		setup_bluez5 (self);
-		break;
-	default:
-		_LOGD ("detecting BlueZ version failed: %s", reason);
+	nm_assert (g_variant_is_of_type (changed_properties, G_VARIANT_TYPE ("a{sv}")));
 
-		/* cancel current attempts to detect the version. */
-		cleanup_checking (self, FALSE);
-		if (!priv->watch_name_id) {
-			priv->watch_name_id = g_bus_watch_name (G_BUS_TYPE_SYSTEM,
-			                                        NM_BLUEZ_SERVICE,
-			                                        G_BUS_NAME_WATCHER_FLAGS_NONE,
-			                                        watch_name_on_appeared,
-			                                        NULL,
-			                                        self,
-			                                        NULL);
+	if (inout_bzobj) {
+		bzobj = *inout_bzobj;
+		nm_assert (!bzobj || nm_streq (object_path, bzobj->object_path));
+	}
+
+	if (changed_properties)
+		g_variant_iter_init (&iter_prop, changed_properties);
+
+	if (nm_streq (interface_name, NM_BLUEZ5_ADAPTER_INTERFACE)) {
+		_bzobjs_init (self, &bzobj, object_path);
+		if (!bzobj->d_has_adapter_iface) {
+			changed = TRUE;
+			bzobj->d_has_adapter_iface = TRUE;
+		}
+
+		while (   changed_properties
+		       && g_variant_iter_next (&iter_prop, "{&sv}", &property_name, &property_value)) {
+			_nm_unused gs_unref_variant GVariant *property_value_free = property_value;
+
+			if (nm_streq (property_name, "Address")) {
+				gs_free char *s =   g_variant_is_of_type (property_value, G_VARIANT_TYPE_STRING)
+				                  ? nm_utils_hwaddr_canonical (g_variant_get_string (property_value, NULL), ETH_ALEN)
+				                  : NULL;
+
+				if (!nm_streq0 (bzobj->d_adapter.address, s)) {
+					changed = TRUE;
+					nm_clear_g_free (&bzobj->d_adapter.address);
+					bzobj->d_adapter.address = g_steal_pointer (&s);
+				}
+				continue;
+			}
+			if (nm_streq (property_name, "Powered")) {
+				bool v =    g_variant_is_of_type (property_value, G_VARIANT_TYPE_BOOLEAN)
+				         && g_variant_get_boolean (property_value);
+
+				if (bzobj->d_adapter_powered != v) {
+					changed = TRUE;
+					bzobj->d_adapter_powered = v;
+				}
+				continue;
+			}
+		}
+
+		for (i = 0; (property_name = invalidated_properties[i]); i++) {
+			if (nm_streq (property_name, "Address")) {
+				if (bzobj->d_adapter.address) {
+					changed = TRUE;
+					nm_clear_g_free (&bzobj->d_adapter.address);
+				}
+				continue;
+			}
+			if (nm_streq (property_name, "Powered")) {
+				if (bzobj->d_adapter_powered) {
+					changed = TRUE;
+					bzobj->d_adapter_powered = FALSE;
+				}
+				continue;
+			}
+		}
+
+	} else if (nm_streq (interface_name, NM_BLUEZ5_DEVICE_INTERFACE)) {
+		_bzobjs_init (self, &bzobj, object_path);
+		if (!bzobj->d_has_device_iface) {
+			changed = TRUE;
+			bzobj->d_has_device_iface = TRUE;
+		}
+
+		while (   changed_properties
+		       && g_variant_iter_next (&iter_prop, "{&sv}", &property_name, &property_value)) {
+			_nm_unused gs_unref_variant GVariant *property_value_free = property_value;
+
+			if (nm_streq (property_name, "Address")) {
+				gs_free char *s =   g_variant_is_of_type (property_value, G_VARIANT_TYPE_STRING)
+				                  ? nm_utils_hwaddr_canonical (g_variant_get_string (property_value, NULL), ETH_ALEN)
+				                  : NULL;
+
+				if (!nm_streq0 (bzobj->d_device.address, s)) {
+					changed = TRUE;
+					nm_clear_g_free (&bzobj->d_device.address);
+					bzobj->d_device.address = g_steal_pointer (&s);
+				}
+				continue;
+			}
+			if (nm_streq (property_name, "Name")) {
+				const char *s =   g_variant_is_of_type (property_value, G_VARIANT_TYPE_STRING)
+				                ? g_variant_get_string (property_value, NULL)
+				                : NULL;
+
+				if (!nm_streq0 (bzobj->d_device.name, s)) {
+					changed = TRUE;
+					nm_clear_g_free (&bzobj->d_device.name);
+					bzobj->d_device.name = g_strdup (s);
+				}
+				continue;
+			}
+			if (nm_streq (property_name, "Adapter")) {
+				const char *s =   g_variant_is_of_type (property_value, G_VARIANT_TYPE_OBJECT_PATH)
+				                ? g_variant_get_string (property_value, NULL)
+				                : NULL;
+
+				if (!nm_streq0 (bzobj->d_device.adapter, s)) {
+					changed = TRUE;
+					nm_clear_g_free (&bzobj->d_device.adapter);
+					bzobj->d_device.adapter = g_strdup (s);
+				}
+				continue;
+			}
+			if (nm_streq (property_name, "UUIDs")) {
+				NMBluetoothCapabilities capabilities = NM_BT_CAPABILITY_NONE;
+
+				if (g_variant_is_of_type (property_value, G_VARIANT_TYPE_STRING_ARRAY)) {
+					gs_free const char **s = g_variant_get_strv (property_value, NULL);
+
+					capabilities = convert_uuids_to_capabilities (s);
+				}
+				if (bzobj->d_device_capabilities != capabilities) {
+					changed = TRUE;
+					bzobj->d_device_capabilities = capabilities;
+					nm_assert (bzobj->d_device_capabilities == capabilities);
+				}
+				continue;
+			}
+			if (nm_streq (property_name, "Connected")) {
+				bool v =    g_variant_is_of_type (property_value, G_VARIANT_TYPE_BOOLEAN)
+				         && g_variant_get_boolean (property_value);
+
+				if (bzobj->d_device_connected != v) {
+					changed = TRUE;
+					bzobj->d_device_connected = v;
+				}
+				continue;
+			}
+			if (nm_streq (property_name, "Paired")) {
+				bool v =    g_variant_is_of_type (property_value, G_VARIANT_TYPE_BOOLEAN)
+				         && g_variant_get_boolean (property_value);
+
+				if (bzobj->d_device_paired != v) {
+					changed = TRUE;
+					bzobj->d_device_paired = v;
+				}
+				continue;
+			}
+		}
+
+		for (i = 0; (property_name = invalidated_properties[i]); i++) {
+			if (nm_streq (property_name, "Address")) {
+				if (bzobj->d_device.address) {
+					changed = TRUE;
+					nm_clear_g_free (&bzobj->d_device.address);
+				}
+				continue;
+			}
+			if (nm_streq (property_name, "Name")) {
+				if (bzobj->d_device.name) {
+					changed = TRUE;
+					nm_clear_g_free (&bzobj->d_device.name);
+				}
+				continue;
+			}
+			if (nm_streq (property_name, "Adapter")) {
+				if (bzobj->d_device.adapter) {
+					changed = TRUE;
+					nm_clear_g_free (&bzobj->d_device.adapter);
+				}
+				continue;
+			}
+			if (nm_streq (property_name, "UUIDs")) {
+				if (bzobj->d_device_capabilities != NM_BT_CAPABILITY_NONE) {
+					changed = TRUE;
+					bzobj->d_device_capabilities = NM_BT_CAPABILITY_NONE;
+				}
+				continue;
+			}
+			if (nm_streq (property_name, "Connected")) {
+				if (bzobj->d_device_connected) {
+					changed = TRUE;
+					bzobj->d_device_connected = FALSE;
+				}
+				continue;
+			}
+			if (nm_streq (property_name, "Paired")) {
+				if (bzobj->d_device_paired) {
+					changed = TRUE;
+					bzobj->d_device_paired = FALSE;
+				}
+				continue;
+			}
+		}
+
+	} else if (nm_streq (interface_name, NM_BLUEZ5_NETWORK_INTERFACE)) {
+		_bzobjs_init (self, &bzobj, object_path);
+		if (!bzobj->d_has_network_iface) {
+			changed = TRUE;
+			bzobj->d_has_network_iface = TRUE;
+		}
+
+		while (   changed_properties
+		       && g_variant_iter_next (&iter_prop, "{&sv}", &property_name, &property_value)) {
+			_nm_unused gs_unref_variant GVariant *property_value_free = property_value;
+
+			if (nm_streq (property_name, "Interface")) {
+				const char *s =   g_variant_is_of_type (property_value, G_VARIANT_TYPE_STRING)
+				                ? g_variant_get_string (property_value, NULL)
+				                : NULL;
+
+				if (!nm_streq0 (bzobj->d_network.interface, s)) {
+					changed = TRUE;
+					nm_clear_g_free (&bzobj->d_network.interface);
+					bzobj->d_network.interface = g_strdup (s);
+				}
+				continue;
+			}
+			if (nm_streq (property_name, "Connected")) {
+				bool v =    g_variant_is_of_type (property_value, G_VARIANT_TYPE_BOOLEAN)
+				         && g_variant_get_boolean (property_value);
+
+				if (bzobj->d_network_connected != v) {
+					changed = TRUE;
+					bzobj->d_network_connected = v;
+				}
+				continue;
+			}
+		}
+
+		for (i = 0; (property_name = invalidated_properties[i]); i++) {
+			if (nm_streq (property_name, "Interface")) {
+				if (bzobj->d_network.interface) {
+					changed = TRUE;
+					nm_clear_g_free (&bzobj->d_network.interface);
+				}
+				continue;
+			}
+			if (nm_streq (property_name, "Connected")) {
+				if (bzobj->d_network_connected) {
+					changed = TRUE;
+					bzobj->d_network_connected = FALSE;
+				}
+				continue;
+			}
+		}
+
+	} else if (nm_streq (interface_name, NM_BLUEZ5_NETWORK_SERVER_INTERFACE)) {
+		_bzobjs_init (self, &bzobj, object_path);
+		if (!bzobj->d_has_network_server_iface) {
+			changed = TRUE;
+			bzobj->d_has_network_server_iface = TRUE;
 		}
-		break;
 	}
+
+	nm_assert (!changed || bzobj);
+
+	if (inout_bzobj)
+		*inout_bzobj = bzobj;
+
+	return changed;
 }
 
 static void
-check_bluez_and_try_setup_do_introspect (GObject *source_object,
-                                         GAsyncResult *res,
-                                         gpointer user_data)
+_dbus_handle_interface_added (NMBluezManager *self,
+                              const char *object_path,
+                              GVariant *ifaces,
+                              gboolean initial_get_managed_objects)
+{
+	BzDBusObj *bzobj = NULL;
+	gboolean changed = FALSE;
+	const char *interface_name;
+	GVariant *changed_properties;
+	GVariantIter iter_ifaces;
+
+	nm_assert (g_variant_is_of_type (ifaces, G_VARIANT_TYPE ("a{sa{sv}}")));
+
+	g_variant_iter_init (&iter_ifaces, ifaces);
+	while (g_variant_iter_next (&iter_ifaces, "{&s@a{sv}}", &interface_name, &changed_properties)) {
+		_nm_unused gs_unref_variant GVariant *changed_properties_free = changed_properties;
+
+		if (_dbus_handle_properties_changed (self, object_path, interface_name, changed_properties, NULL, &bzobj))
+			changed = TRUE;
+	}
+
+	if (changed) {
+		_dbus_process_changes (self,
+		                       bzobj,
+		                         initial_get_managed_objects
+		                       ? "dbus-init"
+		                       : "dbus-iface-added");
+	}
+}
+
+static gboolean
+_dbus_handle_interface_removed (NMBluezManager *self,
+                                const char *object_path,
+                                BzDBusObj **inout_bzobj,
+                                const char *const*removed_interfaces)
 {
-	NMBluezManager *self = async_data_unpack (user_data);
+	gboolean changed = FALSE;
+	BzDBusObj *bzobj;
+	gsize i;
+
+	if (   inout_bzobj
+	    && *inout_bzobj) {
+		bzobj = *inout_bzobj;
+		nm_assert (bzobj == _bzobjs_get (self, object_path));
+	} else {
+		bzobj = _bzobjs_get (self, object_path);
+		if (!bzobj)
+			return FALSE;
+		NM_SET_OUT (inout_bzobj, bzobj);
+	}
+
+	for (i = 0; removed_interfaces[i]; i++) {
+		const char *interface_name = removed_interfaces[i];
+
+		if (nm_streq (interface_name, NM_BLUEZ5_ADAPTER_INTERFACE)) {
+			if (bzobj->d_has_adapter_iface) {
+				changed = TRUE;
+				bzobj->d_has_adapter_iface = FALSE;
+			}
+			if (bzobj->d_adapter.address) {
+				changed = TRUE;
+				nm_clear_g_free (&bzobj->d_adapter.address);
+			}
+			if (bzobj->d_adapter_powered) {
+				changed = TRUE;
+				bzobj->d_adapter_powered = FALSE;
+			}
+		} else if (nm_streq (interface_name, NM_BLUEZ5_DEVICE_INTERFACE)) {
+			if (bzobj->d_has_device_iface) {
+				changed = TRUE;
+				bzobj->d_has_device_iface = FALSE;
+			}
+			if (bzobj->d_device.address) {
+				changed = TRUE;
+				nm_clear_g_free (&bzobj->d_device.address);
+			}
+			if (bzobj->d_device.name) {
+				changed = TRUE;
+				nm_clear_g_free (&bzobj->d_device.name);
+			}
+			if (bzobj->d_device.adapter) {
+				changed = TRUE;
+				nm_clear_g_free (&bzobj->d_device.adapter);
+			}
+			if (bzobj->d_device_capabilities != NM_BT_CAPABILITY_NONE) {
+				changed = TRUE;
+				bzobj->d_device_capabilities = NM_BT_CAPABILITY_NONE;
+			}
+			if (bzobj->d_device_connected) {
+				changed = TRUE;
+				bzobj->d_device_connected = FALSE;
+			}
+			if (bzobj->d_device_paired) {
+				changed = TRUE;
+				bzobj->d_device_paired = FALSE;
+			}
+		} else if (nm_streq (interface_name, NM_BLUEZ5_NETWORK_INTERFACE)) {
+			if (bzobj->d_has_network_iface) {
+				changed = TRUE;
+				bzobj->d_has_network_iface = FALSE;
+			}
+			if (bzobj->d_network.interface) {
+				changed = TRUE;
+				nm_clear_g_free (&bzobj->d_network.interface);
+			}
+			if (bzobj->d_network_connected) {
+				changed = TRUE;
+				bzobj->d_network_connected = FALSE;
+			}
+		} else if (nm_streq (interface_name, NM_BLUEZ5_NETWORK_SERVER_INTERFACE)) {
+			if (bzobj->d_has_network_server_iface) {
+				changed = TRUE;
+				bzobj->d_has_network_server_iface = FALSE;
+			}
+		}
+	}
+
+	return changed;
+}
+
+static void
+_dbus_managed_objects_changed_cb (const char *object_path,
+                                  GVariant *added_interfaces_and_properties,
+                                  const char *const*removed_interfaces,
+                                  gpointer user_data)
+{
+	NMBluezManager *self = user_data;
+	NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE (self);
+	BzDBusObj *bzobj = NULL;
+	gboolean changed;
+
+	if (priv->get_managed_objects_cancellable) {
+		/* we still wait for the initial GetManagedObjects(). Ignore the event. */
+		return;
+	}
+
+	if (!added_interfaces_and_properties) {
+		changed = _dbus_handle_interface_removed (self, object_path, &bzobj, removed_interfaces);
+		if (changed)
+			_dbus_process_changes (self, bzobj, "dbus-iface-removed");
+	} else
+		_dbus_handle_interface_added (self, object_path, added_interfaces_and_properties, FALSE);
+}
+
+static void
+_dbus_properties_changed_cb (GDBusConnection *connection,
+                             const char *sender_name,
+                             const char *object_path,
+                             const char *signal_interface_name,
+                             const char *signal_name,
+                             GVariant *parameters,
+                             gpointer user_data)
+{
+	NMBluezManager *self = user_data;
+	NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE (self);
+	const char *interface_name;
+	gs_unref_variant GVariant *changed_properties = NULL;
+	gs_free const char **invalidated_properties = NULL;
+	BzDBusObj *bzobj = NULL;
+
+	if (priv->get_managed_objects_cancellable) {
+		/* we still wait for the initial GetManagedObjects(). Ignore the event. */
+		return;
+	}
+
+	if (!g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(sa{sv}as)")))
+		return;
+
+	g_variant_get (parameters,
+	               "(&s@a{sv}^a&s)",
+	               &interface_name,
+	               &changed_properties,
+	               &invalidated_properties);
+
+	if (_dbus_handle_properties_changed (self, object_path, interface_name, changed_properties, invalidated_properties, &bzobj))
+		_dbus_process_changes (self, bzobj, "dbus-property-changed");
+}
+
+static void
+_dbus_get_managed_objects_cb (GVariant *result,
+                              GError *error,
+                              gpointer user_data)
+{
+	NMBluezManager *self;
 	NMBluezManagerPrivate *priv;
-	GError *error = NULL;
-	GVariant *result;
-	const char *xml_data;
-	int bluez_version = 0;
-	const char *reason = NULL;
+	GVariantIter iter;
+	const char *object_path;
+	GVariant *ifaces;
 
-	if (!self)
+	if (   !result
+	    && nm_utils_error_is_cancelled (error, FALSE))
 		return;
 
+	self = user_data;
 	priv = NM_BLUEZ_MANAGER_GET_PRIVATE (self);
 
-	g_return_if_fail (priv->introspect_proxy);
-	g_return_if_fail (!g_cancellable_is_cancelled (priv->async_cancellable));
-	g_return_if_fail (!priv->bluez_version);
-
-	g_clear_object (&priv->async_cancellable);
+	g_clear_object (&priv->get_managed_objects_cancellable);
 
-	result = _nm_dbus_proxy_call_finish (priv->introspect_proxy, res,
-	                                     G_VARIANT_TYPE ("(s)"), &error);
 	if (!result) {
-		char *reason2;
-
-		g_dbus_error_strip_remote_error (error);
-		reason2 = g_strdup_printf ("introspect failed with %s", error->message);
-		check_bluez_and_try_setup_final_step (self, 0, reason2);
-		g_error_free (error);
-		g_free (reason2);
+		_LOGT ("initial GetManagedObjects() call failed: %s", error->message);
+		_cleanup_for_name_owner (self);
 		return;
 	}
 
-	g_variant_get (result, "(&s)", &xml_data);
+	_LOGT ("initial GetManagedObjects call succeeded");
+
+	g_variant_iter_init (&iter, result);
+	while (g_variant_iter_next (&iter, "{&o@a{sa{sv}}}", &object_path, &ifaces)) {
+		_nm_unused gs_unref_variant GVariant *ifaces_free = ifaces;
+
+		_dbus_handle_interface_added (self, object_path, ifaces, TRUE);
+	}
+}
+
+/*****************************************************************************/
+
+static void
+_cleanup_for_name_owner (NMBluezManager *self)
+{
+	NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE (self);
+	gboolean emit_device_availability_changed = FALSE;
+	GHashTableIter iter;
+	BzDBusObj *bzobj;
+	gboolean first = TRUE;
+
+	nm_clear_g_cancellable (&priv->get_managed_objects_cancellable);
+
+	nm_clear_g_dbus_connection_signal (priv->dbus_connection,
+	                                   &priv->managed_objects_changed_id);
+	nm_clear_g_dbus_connection_signal (priv->dbus_connection,
+	                                   &priv->properties_changed_id);
+
+	nm_clear_g_free (&priv->name_owner);
+
+	g_hash_table_iter_init (&iter, priv->bzobjs);
+	while (g_hash_table_iter_next (&iter, (gpointer *) &bzobj, NULL)) {
+		if (first) {
+			first = FALSE;
+			_LOGT ("drop all objects form D-Bus cache...");
+		}
+		_dbus_handle_interface_removed (self,
+		                                bzobj->object_path,
+		                                &bzobj,
+		                                ALL_RELEVANT_INTERFACE_NAMES);
+		nm_c_list_move_tail (&priv->process_change_lst_head, &bzobj->process_change_lst);
+	}
+	_process_change_idle_all (self, &emit_device_availability_changed);
+	nm_assert (g_hash_table_size (priv->bzobjs) == 0);
+
+	if (emit_device_availability_changed)
+		nm_manager_notify_device_availibility_maybe_changed (priv->manager);
+}
 
-	/* might not be the best approach to detect the version, but it's good enough in practice. */
-	if (strstr (xml_data, "org.freedesktop.DBus.ObjectManager"))
-		bluez_version = 5;
-	else if (strstr (xml_data, NM_BLUEZ4_MANAGER_INTERFACE))
-		bluez_version = 4;
+static void
+name_owner_changed (NMBluezManager *self,
+                    const char *owner)
+{
+	_nm_unused gs_unref_object NMBluezManager *self_keep_alive = g_object_ref (self);
+	NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE (self);
+
+	owner = nm_str_not_empty (owner);
+
+	if (!owner)
+		_LOGT ("D-Bus name for bluez has no owner");
 	else
-		reason = "unexpected introspect result";
+		_LOGT ("D-Bus name for bluez has owner %s", owner);
+
+	nm_clear_g_cancellable (&priv->name_owner_get_cancellable);
+
+	if (nm_streq0 (priv->name_owner, owner))
+		return;
+
+	_cleanup_for_name_owner (self);
 
-	g_variant_unref (result);
+	if (!owner)
+		return;
 
-	check_bluez_and_try_setup_final_step (self, bluez_version, reason);
+	priv->name_owner = g_strdup (owner);
+
+	priv->get_managed_objects_cancellable = g_cancellable_new ();
+
+	priv->managed_objects_changed_id = nm_dbus_connection_signal_subscribe_object_manager (priv->dbus_connection,
+	                                                                                       priv->name_owner,
+	                                                                                       NM_BLUEZ_MANAGER_PATH,
+	                                                                                       _dbus_managed_objects_changed_cb,
+	                                                                                       self,
+	                                                                                       NULL);
+
+	priv->properties_changed_id = nm_dbus_connection_signal_subscribe_properties_changed (priv->dbus_connection,
+	                                                                                      priv->name_owner,
+	                                                                                      NULL,
+	                                                                                      NULL,
+	                                                                                      _dbus_properties_changed_cb,
+	                                                                                      self,
+	                                                                                      NULL);
+
+	nm_dbus_connection_call_get_managed_objects (priv->dbus_connection,
+	                                             priv->name_owner,
+	                                             NM_BLUEZ_MANAGER_PATH,
+	                                             G_DBUS_CALL_FLAGS_NO_AUTO_START,
+	                                             20000,
+	                                             priv->get_managed_objects_cancellable,
+	                                             _dbus_get_managed_objects_cb,
+	                                             self);
 }
 
 static void
-check_bluez_and_try_setup_on_new_proxy (GObject *source_object,
-                                        GAsyncResult *res,
-                                        gpointer user_data)
+name_owner_changed_cb (GDBusConnection *connection,
+                       const char *sender_name,
+                       const char *object_path,
+                       const char *interface_name,
+                       const char *signal_name,
+                       GVariant *parameters,
+                       gpointer user_data)
 {
-	NMBluezManager *self = async_data_unpack (user_data);
-	NMBluezManagerPrivate *priv;
-	GError *error = NULL;
+	NMBluezManager *self = user_data;
+	const char *new_owner;
 
-	if (!self)
+	if (!g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(sss)")))
 		return;
 
+	g_variant_get (parameters,
+	               "(&s&s&s)",
+	               NULL,
+	               NULL,
+	               &new_owner);
+
+	name_owner_changed (self, new_owner);
+}
+
+static void
+name_owner_get_cb (const char *name_owner,
+                   GError *error,
+                   gpointer user_data)
+{
+	if (   name_owner
+	    || !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+		name_owner_changed (user_data, name_owner);
+}
+
+/*****************************************************************************/
+
+static void
+_cleanup_all (NMBluezManager *self)
+{
+	NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE (self);
+
+	priv->settings_registered = FALSE;
+
+	g_signal_handlers_disconnect_by_func (priv->settings, cp_connection_added, self);
+	g_signal_handlers_disconnect_by_func (priv->settings, cp_connection_updated, self);
+	g_signal_handlers_disconnect_by_func (priv->settings, cp_connection_removed, self);
+
+	g_hash_table_remove_all (priv->conn_data_elems);
+	g_hash_table_remove_all (priv->conn_data_heads);
+
+	_cleanup_for_name_owner (self);
+
+	nm_clear_g_cancellable (&priv->name_owner_get_cancellable);
+
+	nm_clear_g_dbus_connection_signal (priv->dbus_connection,
+	                                   &priv->name_owner_changed_id);
+}
+
+static void
+start (NMDeviceFactory *factory)
+{
+	NMBluezManager *self;
+	NMBluezManagerPrivate *priv;
+	NMSettingsConnection *const*sett_conns;
+	guint n_sett_conns;
+	guint i;
+
+	g_return_if_fail (NM_IS_BLUEZ_MANAGER (factory));
+
+	self = NM_BLUEZ_MANAGER (factory);
 	priv = NM_BLUEZ_MANAGER_GET_PRIVATE (self);
 
-	g_return_if_fail (!priv->introspect_proxy);
-	g_return_if_fail (!g_cancellable_is_cancelled (priv->async_cancellable));
-	g_return_if_fail (!priv->bluez_version);
+	_cleanup_all (self);
+
+	if (!priv->dbus_connection) {
+		_LOGI ("no D-Bus connection available");
+		return;
+	}
+
+	g_signal_connect (priv->settings, NM_SETTINGS_SIGNAL_CONNECTION_ADDED,   G_CALLBACK (cp_connection_added),   self);
+	g_signal_connect (priv->settings, NM_SETTINGS_SIGNAL_CONNECTION_UPDATED, G_CALLBACK (cp_connection_updated), self);
+	g_signal_connect (priv->settings, NM_SETTINGS_SIGNAL_CONNECTION_REMOVED, G_CALLBACK (cp_connection_removed), self);
 
-	priv->introspect_proxy = g_dbus_proxy_new_for_bus_finish (res, &error);
+	priv->settings_registered = TRUE;
 
-	if (!priv->introspect_proxy) {
-		char *reason = g_strdup_printf ("bluez error creating dbus proxy: %s", error->message);
-		check_bluez_and_try_setup_final_step (self, 0, reason);
-		g_error_free (error);
-		g_free (reason);
+	sett_conns = nm_settings_get_connections (priv->settings, &n_sett_conns);
+	for (i = 0; i < n_sett_conns; i++)
+		_conn_track_update (self, sett_conns[i], TRUE, NULL, NULL, NULL);
+
+	priv->name_owner_changed_id = nm_dbus_connection_signal_subscribe_name_owner_changed (priv->dbus_connection,
+	                                                                                      NM_BLUEZ_SERVICE,
+	                                                                                      name_owner_changed_cb,
+	                                                                                      self,
+	                                                                                      NULL);
+
+	priv->name_owner_get_cancellable = g_cancellable_new ();
+
+	nm_dbus_connection_call_get_name_owner (priv->dbus_connection,
+	                                        NM_BLUEZ_SERVICE,
+	                                        10000,
+	                                        priv->name_owner_get_cancellable,
+	                                        name_owner_get_cb,
+	                                        self);
+}
+
+/*****************************************************************************/
+
+static void
+_connect_returned (NMBluezManager *self,
+                   BzDBusObj *bzobj,
+                   NMBluetoothCapabilities bt_type,
+                   const char *device_name,
+                   NMBluez5DunContext *dun_context,
+                   GError *error)
+{
+	char sbuf_cap[100];
+
+	if (error) {
+		nm_assert (!device_name);
+		nm_assert (!dun_context);
+
+		_LOGI ("%s [%s]: connect failed: %s",
+		       nm_bluetooth_capability_to_string (bzobj->x_device_connect_bt_type, sbuf_cap, sizeof (sbuf_cap)),
+		       bzobj->object_path,
+		       error->message);
+
+		_device_connect_req_data_complete (g_steal_pointer (&bzobj->x_device.c_req_data),
+		                                   self,
+		                                   NULL,
+		                                   error);
+		_connect_disconnect (self, bzobj, "cleanup after connect failure");
 		return;
 	}
 
-	g_dbus_proxy_call (priv->introspect_proxy,
-	                   "Introspect",
-	                   NULL,
-	                   G_DBUS_CALL_FLAGS_NO_AUTO_START,
-	                   3000,
-	                   priv->async_cancellable,
-	                   check_bluez_and_try_setup_do_introspect,
-	                   async_data_pack (self));
+	nm_assert (bzobj->x_device_connect_bt_type == bt_type);
+	nm_assert (device_name);
+	nm_assert ((bt_type == NM_BT_CAPABILITY_DUN) == (!!dun_context));
+	nm_assert (bzobj->x_device.c_req_data);
+
+	g_clear_object (&bzobj->x_device.c_req_data->int_cancellable);
+
+	bzobj->x_device.connect_dun_context = dun_context;
+
+	_LOGD ("%s [%s]: connect successful to device %s",
+	       nm_bluetooth_capability_to_string (bzobj->x_device_connect_bt_type, sbuf_cap, sizeof (sbuf_cap)),
+	       bzobj->object_path,
+	       device_name);
+
+	/* we already have another over-all timer running. But after we connected the device,
+	 * we still need to wait for bluez to acknowledge the connected state (via D-Bus, for NAP).
+	 * For DUN profiles we likely are already fully connected by now.
+	 *
+	 * Anyway, schedule another timeout that is possibly shorter than the overall, original
+	 * timeout. Now this should go down fast. */
+	bzobj->x_device.c_req_data->timeout_wait_connect_id = g_timeout_add (5000,
+	                                                                     _connect_timeout_wait_connected_cb,
+	                                                                     bzobj),
+	bzobj->x_device.c_req_data->device_name = g_strdup (device_name);
+
+	if (   _bzobjs_device_is_usable (bzobj, NULL, NULL)
+	    && _bzobjs_device_is_connected (bzobj)) {
+		/* We are now connected. Schedule the task that completes the state. */
+		_process_change_idle_schedule (self, bzobj);
+	}
+}
+
+#if WITH_BLUEZ5_DUN
+static void
+_connect_dun_notify_tty_hangup_cb (NMBluez5DunContext *context,
+                                   gpointer user_data)
+{
+	BzDBusObj *bzobj = user_data;
+
+	_connect_disconnect (bzobj->self,
+	                     bzobj,
+	                     "DUN connection hung up");
 }
 
 static void
-check_bluez_and_try_setup (NMBluezManager *self)
+_connect_dun_step2_cb (NMBluez5DunContext *context,
+                       const char *rfcomm_dev,
+                       GError *error,
+                       gpointer user_data)
 {
-	NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE (self);
+	BzDBusObj *bzobj;
+
+	if (nm_utils_error_is_cancelled (error, FALSE))
+		return;
+
+	bzobj = user_data;
+
+	if (rfcomm_dev) {
+		/* We want to early notifiy about the rfcomm path. That is because we might still delay
+		 * to signal full activation longer (asynchronously). But the earliest time the callback
+		 * is invoked with the rfcomm path, we just created the device synchronously.
+		 *
+		 * By already notifying the caller about the path early, it avoids a race where ModemManager
+		 * would find the modem before the bluetooth code considers the profile fully activated. */
 
-	g_return_if_fail (!priv->bluez_version);
+		nm_assert (!error);
+		nm_assert (bzobj->x_device.c_req_data);
 
-	/* there should be no ongoing detection. Anyway, cleanup_checking. */
-	cleanup_checking (self, FALSE);
+		if (!g_cancellable_is_cancelled (bzobj->x_device.c_req_data->ext_cancellable))
+			bzobj->x_device.c_req_data->callback (bzobj->self, FALSE, rfcomm_dev, NULL, bzobj->x_device.c_req_data->callback_user_data);
 
-	priv->async_cancellable = g_cancellable_new ();
+		if (!context) {
+			/* No context set. This means, we just got notified about the rfcomm path and need to wait
+			 * longer, for the next callback. */
+			return;
+		}
+	}
 
-	g_dbus_proxy_new_for_bus (G_BUS_TYPE_SYSTEM,
-	                          G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES | G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START,
-	                          NULL,
-	                          NM_BLUEZ_SERVICE,
-	                          "/",
-	                          DBUS_INTERFACE_INTROSPECTABLE,
-	                          priv->async_cancellable,
-	                          check_bluez_and_try_setup_on_new_proxy,
-	                          async_data_pack (self));
+	_connect_returned (bzobj->self, bzobj, NM_BT_CAPABILITY_DUN, rfcomm_dev, context, error);
 }
 
 static void
-start (NMDeviceFactory *factory)
+_connect_dun_step1_cb (GObject *source_object,
+                       GAsyncResult *res,
+                       gpointer user_data)
+{
+	gs_unref_variant GVariant *ret = NULL;
+	gs_free_error GError *error = NULL;
+	BzDBusObj *bzobj_adapter;
+	BzDBusObj *bzobj;
+
+	ret = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source_object), res, &error);
+
+	if (   !ret
+	    && nm_utils_error_is_cancelled (error, FALSE))
+		return;
+
+	bzobj = user_data;
+
+	if (error) {
+		_LOGT ("DUN: [%s]: bluetooth device connect failed: %s", bzobj->object_path, error->message);
+		/* we actually ignore this error. Let's try, maybe we still can connect via DUN. */
+		g_clear_error (&error);
+	} else
+		_LOGT ("DUN: [%s]: bluetooth device connected successfully", bzobj->object_path);
+
+	if (!_bzobjs_device_is_usable (bzobj, &bzobj_adapter, NULL)) {
+		nm_utils_error_set (&error, NM_UTILS_ERROR_UNKNOWN,
+		                    "device %s is not usable for DUN after connect",
+		                    bzobj->object_path);
+		_connect_returned (bzobj->self, bzobj, NM_BT_CAPABILITY_DUN, NULL, NULL, error);
+		return;
+	}
+
+	if (!nm_bluez5_dun_connect (bzobj_adapter->d_adapter.address,
+	                            bzobj->d_device.address,
+	                            bzobj->x_device.c_req_data->int_cancellable,
+	                            _connect_dun_step2_cb,
+	                            bzobj,
+	                            _connect_dun_notify_tty_hangup_cb,
+	                            bzobj,
+	                            &error)) {
+		_connect_returned (bzobj->self, bzobj, NM_BT_CAPABILITY_DUN, NULL, NULL, error);
+		return;
+	}
+}
+#endif
+
+static void
+_connect_nap_cb (GObject *source_object,
+                 GAsyncResult *res,
+                 gpointer user_data)
 {
-	check_bluez_and_try_setup (NM_BLUEZ_MANAGER (factory));
+	gs_unref_variant GVariant *ret = NULL;
+	const char *network_iface_name = NULL;
+	gs_free_error GError *error = NULL;
+	BzDBusObj *bzobj;
+
+	ret = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source_object), res, &error);
+
+	if (   !ret
+	    && nm_utils_error_is_cancelled (error, FALSE))
+		return;
+
+	if (ret)
+		g_variant_get (ret, "(&s)", &network_iface_name);
+
+	bzobj = user_data;
+
+	_connect_returned (bzobj->self, bzobj, NM_BT_CAPABILITY_NAP, network_iface_name, NULL, error);
+}
+
+static void
+_connect_cancelled_cb (GCancellable *cancellable,
+                       BzDBusObj *bzobj)
+{
+	_connect_disconnect (bzobj->self, bzobj, "connect cancelled");
 }
 
+static gboolean
+_connect_timeout_wait_connected_cb (gpointer user_data)
+{
+	BzDBusObj *bzobj = user_data;
+
+	bzobj->x_device.c_req_data->timeout_wait_connect_id = 0;
+	_connect_disconnect (bzobj->self, bzobj, "timeout waiting for connected");
+	return G_SOURCE_REMOVE;
+}
+
+static gboolean
+_connect_timeout_cb (gpointer user_data)
+{
+	BzDBusObj *bzobj = user_data;
+
+	bzobj->x_device.c_req_data->timeout_id = 0;
+	_connect_disconnect (bzobj->self, bzobj, "timeout connecting");
+	return G_SOURCE_REMOVE;
+}
+
+static void
+_connect_disconnect (NMBluezManager *self,
+                     BzDBusObj *bzobj,
+                     const char *reason)
+{
+	NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE (self);
+	DeviceConnectReqData *c_req_data;
+	char sbuf_cap[100];
+	gboolean bt_type;
+
+	if (bzobj->x_device_connect_bt_type == NM_BT_CAPABILITY_NONE) {
+		nm_assert (!bzobj->x_device.c_req_data);
+		return;
+	}
+
+	bt_type = bzobj->x_device_connect_bt_type;
+	nm_assert (NM_IN_SET (bt_type, NM_BT_CAPABILITY_DUN, NM_BT_CAPABILITY_NAP));
+	bzobj->x_device_connect_bt_type = NM_BT_CAPABILITY_NONE;
+
+	c_req_data = g_steal_pointer (&bzobj->x_device.c_req_data);
+
+	_LOGD ("%s [%s]: disconnect due to %s",
+	       nm_bluetooth_capability_to_string (bt_type, sbuf_cap, sizeof (sbuf_cap)),
+	       bzobj->object_path,
+	       reason);
+
+	if (c_req_data)
+		nm_clear_g_cancellable (&c_req_data->int_cancellable);
+
+	if (bt_type == NM_BT_CAPABILITY_DUN) {
+		/* For DUN devices, we also called org.bluez.Device1.Connect() (because in order
+		 * for nm_bluez5_dun_connect() to succeed, we need to be already connected *why??).
+		 *
+		 * But upon disconnect we don't call Disconnect() because we don't know whether somebody
+		 * else also uses the bluetooth device for other purposes. During disconnect we only
+		 * terminate the DUN connection, but don't disconnect entirely. I think that's the
+		 * best we can do. */
+#if WITH_BLUEZ5_DUN
+		nm_clear_pointer (&bzobj->x_device.connect_dun_context, nm_bluez5_dun_disconnect);
+#else
+		nm_assert_not_reached ();
+#endif
+	} else {
+		if (priv->name_owner) {
+			gs_unref_object GCancellable *cancellable = NULL;
+
+			cancellable = g_cancellable_new ();
+
+			nm_shutdown_wait_obj_register_cancellable_full (cancellable,
+			                                                g_strdup_printf ("bt-disconnect-nap[%s]", bzobj->object_path),
+			                                                TRUE);
+
+			g_dbus_connection_call (priv->dbus_connection,
+			                        priv->name_owner,
+			                        bzobj->object_path,
+			                        NM_BLUEZ5_NETWORK_INTERFACE,
+			                        "Disconnect",
+			                        g_variant_new("()"),
+			                        NULL,
+			                        G_DBUS_CALL_FLAGS_NO_AUTO_START,
+			                        -1,
+			                        cancellable,
+			                        _dbus_call_complete_cb_nop,
+			                        NULL);
+		}
+	}
+
+	if (c_req_data) {
+		gs_free_error GError *error = NULL;
+
+		nm_utils_error_set (&error,
+		                    NM_UTILS_ERROR_UNKNOWN,
+		                    "connect aborted due to %s",
+		                    reason);
+		_device_connect_req_data_complete (c_req_data, self, NULL, error);
+	}
+}
+
+gboolean
+nm_bluez_manager_connect (NMBluezManager *self,
+                          const char *object_path,
+                          NMBluetoothCapabilities connection_bt_type,
+                          int timeout_msec,
+                          GCancellable *cancellable,
+                          NMBluezManagerConnectCb callback,
+                          gpointer callback_user_data,
+                          GError **error)
+{
+	gs_unref_object GCancellable *int_cancellable = NULL;
+	DeviceConnectReqData *c_req_data;
+	NMBluezManagerPrivate *priv;
+	BzDBusObj *bzobj;
+	char sbuf_cap[100];
+
+	g_return_val_if_fail (NM_IS_BLUEZ_MANAGER (self), FALSE);
+	g_return_val_if_fail (NM_IN_SET (connection_bt_type, NM_BT_CAPABILITY_DUN,
+	                                                     NM_BT_CAPABILITY_NAP), FALSE);
+	g_return_val_if_fail (callback, FALSE);
+
+	nm_assert (timeout_msec > 0);
+
+	priv = NM_BLUEZ_MANAGER_GET_PRIVATE (self);
+
+	bzobj = _bzobjs_get (self, object_path);
+
+	if (!bzobj) {
+		nm_utils_error_set (error, NM_UTILS_ERROR_UNKNOWN,
+		                    "device %s does not exist",
+		                    object_path);
+		return FALSE;
+	}
+
+	if (!_bzobjs_device_is_usable (bzobj, NULL, NULL)) {
+		nm_utils_error_set (error, NM_UTILS_ERROR_UNKNOWN,
+		                    "device %s is not usable",
+		                    object_path);
+		return FALSE;
+	}
+
+	if (!NM_FLAGS_ALL (bzobj->d_device_capabilities, connection_bt_type)) {
+		nm_utils_error_set (error, NM_UTILS_ERROR_UNKNOWN,
+		                    "device %s has not the required capabilities",
+		                    object_path);
+		return FALSE;
+	}
+
+#if !WITH_BLUEZ5_DUN
+	if (connection_bt_type == NM_BT_CAPABILITY_DUN) {
+		nm_utils_error_set (error, NM_UTILS_ERROR_UNKNOWN,
+		                    "DUN is not supported");
+		return FALSE;
+	}
+#endif
+
+	_connect_disconnect (self, bzobj, "new activation");
+
+	_LOGD ("%s [%s]: connecting...",
+	       nm_bluetooth_capability_to_string (connection_bt_type, sbuf_cap, sizeof (sbuf_cap)),
+	       bzobj->object_path);
+
+	int_cancellable = g_cancellable_new();
+
+#if WITH_BLUEZ5_DUN
+	if (connection_bt_type == NM_BT_CAPABILITY_DUN) {
+		g_dbus_connection_call (priv->dbus_connection,
+		                        priv->name_owner,
+		                        bzobj->object_path,
+		                        NM_BLUEZ5_DEVICE_INTERFACE,
+		                        "Connect",
+		                        NULL,
+		                        NULL,
+		                        G_DBUS_CALL_FLAGS_NO_AUTO_START,
+		                        timeout_msec,
+		                        int_cancellable,
+		                        _connect_dun_step1_cb,
+		                        bzobj);
+	} else
+#endif
+	{
+		nm_assert (connection_bt_type == NM_BT_CAPABILITY_NAP);
+		g_dbus_connection_call (priv->dbus_connection,
+		                        priv->name_owner,
+		                        bzobj->object_path,
+		                        NM_BLUEZ5_NETWORK_INTERFACE,
+		                        "Connect",
+		                        g_variant_new ("(s)", BLUETOOTH_CONNECT_NAP),
+		                        G_VARIANT_TYPE ("(s)"),
+		                        G_DBUS_CALL_FLAGS_NO_AUTO_START,
+		                        timeout_msec,
+		                        int_cancellable,
+		                        _connect_nap_cb,
+		                        bzobj);
+	}
+
+	c_req_data = g_slice_new (DeviceConnectReqData);
+	*c_req_data = (DeviceConnectReqData) {
+		.int_cancellable     = g_steal_pointer (&int_cancellable),
+		.ext_cancellable     = g_object_ref (cancellable),
+		.callback            = callback,
+		.callback_user_data  = callback_user_data,
+		.ext_cancelled_id    = g_signal_connect (cancellable,
+		                                         "cancelled",
+		                                         G_CALLBACK (_connect_cancelled_cb),
+		                                         bzobj),
+		.timeout_id          = g_timeout_add (timeout_msec,
+		                                      _connect_timeout_cb,
+		                                      bzobj),
+	};
+
+	bzobj->x_device_connect_bt_type = connection_bt_type;
+	bzobj->x_device.c_req_data = c_req_data;
+
+	return TRUE;
+}
+
+void
+nm_bluez_manager_disconnect (NMBluezManager *self,
+                             const char *object_path)
+{
+	BzDBusObj *bzobj;
+
+	g_return_if_fail (NM_IS_BLUEZ_MANAGER (self));
+	g_return_if_fail (object_path);
+
+	bzobj = _bzobjs_get (self, object_path);
+	if (!bzobj)
+		return;
+
+	_connect_disconnect (self, bzobj, "disconnected by user");
+}
+
+/*****************************************************************************/
+
 static NMDevice *
 create_device (NMDeviceFactory *factory,
                const char *iface,
@@ -407,8 +2789,8 @@ create_device (NMDeviceFactory *factory,
                NMConnection *connection,
                gboolean *out_ignore)
 {
-	g_warn_if_fail (plink->type == NM_LINK_TYPE_BNEP);
 	*out_ignore = TRUE;
+	g_return_val_if_fail (plink->type == NM_LINK_TYPE_BNEP, NULL);
 	return NULL;
 }
 
@@ -433,7 +2815,25 @@ nm_bluez_manager_init (NMBluezManager *self)
 {
 	NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE (self);
 
+	priv->vtable_network_server = (NMBtVTableNetworkServer) {
+		.is_available      = _network_server_vt_is_available,
+		.register_bridge   = _network_server_vt_register_bridge,
+		.unregister_bridge = _network_server_vt_unregister_bridge,
+	};
+
+	c_list_init (&priv->network_server_lst_head);
+	c_list_init (&priv->process_change_lst_head);
+
+	priv->conn_data_heads = g_hash_table_new_full (_conn_data_head_hash, _conn_data_head_equal, g_free, NULL);
+	priv->conn_data_elems = g_hash_table_new_full (nm_pdirect_hash, nm_pdirect_equal, nm_g_slice_free_fcn (ConnDataElem), NULL);
+
+	priv->bzobjs = g_hash_table_new_full (nm_pstr_hash, nm_pstr_equal, (GDestroyNotify) _bz_dbus_obj_free, NULL);
+
+	priv->manager = g_object_ref (NM_MANAGER_GET);
 	priv->settings = g_object_ref (NM_SETTINGS_GET);
+	priv->dbus_connection = nm_g_object_ref (NM_MAIN_DBUS_CONNECTION_GET);
+
+	g_atomic_pointer_compare_and_exchange (&nm_bt_vtable_network_server, NULL, &priv->vtable_network_server);
 }
 
 static void
@@ -442,22 +2842,25 @@ dispose (GObject *object)
 	NMBluezManager *self = NM_BLUEZ_MANAGER (object);
 	NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE (self);
 
-	if (priv->manager4) {
-		g_signal_handlers_disconnect_by_func (priv->manager4, manager_bdaddr_added_cb, self);
-		g_clear_object (&priv->manager4);
-	}
-	if (priv->manager5) {
-		g_signal_handlers_disconnect_by_data (priv->manager5, self);
-		g_clear_object (&priv->manager5);
-	}
+	/* FIXME(shutdown): we need a nm_device_factory_stop() hook to first unregister all
+	 *   BzDBusObj instances and do necessary cleanup actions (like disconnecting devices
+	 *   or deleting panu_connection). */
 
-	cleanup_checking (self, TRUE);
+	nm_assert (c_list_is_empty (&priv->network_server_lst_head));
+	nm_assert (c_list_is_empty (&priv->process_change_lst_head));
+	nm_assert (priv->process_change_idle_id == 0);
 
-	priv->bluez_version = 0;
+	g_atomic_pointer_compare_and_exchange (&nm_bt_vtable_network_server, &priv->vtable_network_server, NULL);
+
+	_cleanup_all (self);
 
 	G_OBJECT_CLASS (nm_bluez_manager_parent_class)->dispose (object);
 
 	g_clear_object (&priv->settings);
+	g_clear_object (&priv->manager);
+	g_clear_object (&priv->dbus_connection);
+
+	nm_clear_pointer (&priv->bzobjs, g_hash_table_destroy);
 }
 
 static void
@@ -466,10 +2869,10 @@ nm_bluez_manager_class_init (NMBluezManagerClass *klass)
 	GObjectClass *object_class = G_OBJECT_CLASS (klass);
 	NMDeviceFactoryClass *factory_class = NM_DEVICE_FACTORY_CLASS (klass);
 
-	object_class->dispose = dispose;
+	object_class->dispose     = dispose;
 
 	factory_class->get_supported_types = get_supported_types;
-	factory_class->create_device = create_device;
-	factory_class->match_connection = match_connection;
-	factory_class->start = start;
+	factory_class->create_device       = create_device;
+	factory_class->match_connection    = match_connection;
+	factory_class->start               = start;
 }