/*
 *
 *  Connection Manager
 *
 *  Copyright (C) 2007-2013  Intel Corporation. All rights reserved.
 *  Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
 *  Copyright (C) 2011-2014  BMW Car IT GmbH.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  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 St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <errno.h>
#include <stdlib.h>

#include <gdbus.h>
#include <string.h>
#include <stdint.h>

#define CONNMAN_API_SUBJECT_TO_CHANGE
#include <connman/plugin.h>
#include <connman/device.h>
#include <connman/network.h>
#include <connman/inet.h>
#include <connman/dbus.h>
#include <connman/log.h>
#include <connman/technology.h>
#include <connman/setting.h>

#include "mcc.h"

#define OFONO_SERVICE			"org.ofono"

#define OFONO_MANAGER_INTERFACE		OFONO_SERVICE ".Manager"
#define OFONO_MODEM_INTERFACE		OFONO_SERVICE ".Modem"
#define OFONO_SIM_INTERFACE		OFONO_SERVICE ".SimManager"
#define OFONO_NETREG_INTERFACE		OFONO_SERVICE ".NetworkRegistration"
#define OFONO_CM_INTERFACE		OFONO_SERVICE ".ConnectionManager"
#define OFONO_CONTEXT_INTERFACE		OFONO_SERVICE ".ConnectionContext"
#define OFONO_LTE_INTERFACE		OFONO_SERVICE ".LongTermEvolution"

#define MODEM_ADDED			"ModemAdded"
#define MODEM_REMOVED			"ModemRemoved"
#define PROPERTY_CHANGED		"PropertyChanged"
#define CONTEXT_ADDED			"ContextAdded"
#define CONTEXT_REMOVED			"ContextRemoved"

#define GET_PROPERTIES			"GetProperties"
#define SET_PROPERTY			"SetProperty"
#define GET_MODEMS			"GetModems"
#define GET_CONTEXTS			"GetContexts"

#define TIMEOUT 40000

enum ofono_api {
	OFONO_API_SIM =		0x1,
	OFONO_API_NETREG =	0x2,
	OFONO_API_CM =		0x4,
	OFONO_API_LTE =		0x8,
};

enum capabilities {
	LTE_CAPABLE = 0x1,
};

/*
 * The way this plugin works is following:
 *
 *   powered -> SubscriberIdentity or Online = True -> gprs, context ->
 *     attached -> netreg -> ready
 *
 * Depending on the modem type, this plugin will behave differently.
 *
 * GSM working flow:
 *
 * When a new modem appears, the plugin always powers it up. This
 * allows the plugin to create a connman_device. The core will call
 * modem_enable() if the technology is enabled. modem_enable() will
 * then set the modem online. If the technology is disabled then
 * modem_disable() will just set the modem offline. The modem is
 * always kept powered all the time.
 *
 * After setting the modem online the plugin waits for the
 * ConnectionManager and ConnectionContext to appear. When the context
 * signals that it is attached and the NetworkRegistration interface
 * appears, a new Service will be created and registered at the core.
 *
 * When asked to connect to the network (network_connect()) the plugin
 * will set the Active property on the context. If this operation is
 * successful the modem is connected to the network. oFono will inform
 * the plugin about IP configuration through the updating the context's
 * properties.
 */

static DBusConnection *connection;

static GHashTable *modem_hash;
static GHashTable *context_hash;

struct network_context {
	char *path;
	int index;
	struct connman_network *network;

	enum connman_ipconfig_method ipv4_method;
	struct connman_ipaddress *ipv4_address;
	char *ipv4_nameservers;

	enum connman_ipconfig_method ipv6_method;
	struct connman_ipaddress *ipv6_address;
	char *ipv6_nameservers;

	int refcount;

	bool active;
	bool valid_apn; /* APN is 'valid' if length > 0 */
};

struct modem_data {
	char *path;

	struct connman_device *device;

	GSList *context_list;

	/* Modem Interface */
	char *serial;
	bool powered;
	bool online;
	uint8_t interfaces;
	uint8_t capabilities;
	bool ignore;

	bool set_powered;

	/* ConnectionManager Interface */
	bool attached;
	bool cm_powered;

	/* SimManager Interface */
	char *imsi;

	/* Netreg Interface */
	char *name;
	uint8_t strength;
	bool registered;
	bool roaming;

	/* pending calls */
	DBusPendingCall	*call_set_property;
	DBusPendingCall	*call_get_properties;
	DBusPendingCall *call_get_contexts;
};

static int cm_get_contexts(struct modem_data *modem);
static int cm_get_properties(struct modem_data *modem);

static const char *api2string(enum ofono_api api)
{
	switch (api) {
	case OFONO_API_SIM:
		return "sim";
	case OFONO_API_NETREG:
		return "netreg";
	case OFONO_API_CM:
		return "cm";
	case OFONO_API_LTE:
		return "lte";
	}

	return "unknown";
}

static char *get_ident(const char *path)
{
	char *pos;

	if (*path != '/')
		return NULL;

	pos = strrchr(path, '/');
	if (!pos)
		return NULL;

	return pos + 1;
}

static struct network_context *get_context_with_path(GSList *context_list,
						const gchar *path)
{
	GSList *list;

	DBG("path %s", path);

	for (list = context_list; list; list = list->next) {
		struct network_context *context = list->data;

		if (g_strcmp0(context->path, path) == 0)
			return context;
	}

	return NULL;
}

static struct network_context *get_context_with_network(GSList *context_list,
				const struct connman_network *network)
{
	GSList *list;

	DBG("network %p", network);

	for (list = context_list; list; list = list->next) {
		struct network_context *context = list->data;

		if (context->network == network)
			return context;
	}

	return NULL;
}

static struct network_context *network_context_alloc(const char *path)
{
	struct network_context *context;

	context = g_try_new0(struct network_context, 1);
	if (!context)
		return NULL;

	context->path = g_strdup(path);
	context->index = -1;

	context->ipv4_method = CONNMAN_IPCONFIG_METHOD_UNKNOWN;
	context->ipv4_address = NULL;
	context->ipv4_nameservers = NULL;

	context->ipv6_method = CONNMAN_IPCONFIG_METHOD_UNKNOWN;
	context->ipv6_address = NULL;
	context->ipv6_nameservers = NULL;

	context->refcount = 1;

	return context;
}

static void network_context_ref(struct network_context *context)
{
	DBG("%p ref %d", context, context->refcount + 1);

	__sync_fetch_and_add(&context->refcount, 1);
}

static void network_context_unref(struct network_context *context)
{
	DBG("%p ref %d", context, context->refcount - 1);

	if (__sync_fetch_and_sub(&context->refcount, 1) != 1)
		return;

	g_free(context->path);

	connman_ipaddress_free(context->ipv4_address);
	g_free(context->ipv4_nameservers);

	connman_ipaddress_free(context->ipv6_address);
	g_free(context->ipv6_nameservers);

	g_free(context);
}

static void set_connected(struct modem_data *modem,
			struct network_context *context)
{
	struct connman_service *service;
	bool setip = false;
	enum connman_ipconfig_method method;
	char *nameservers;
	int index;

	DBG("modem %p path %s context %p path %s",
			modem, modem->path, context, context->path);

	index = context->index;

	method = context->ipv4_method;
	if (index < 0 || (!context->ipv4_address &&
				method == CONNMAN_IPCONFIG_METHOD_FIXED)) {
		connman_error("Invalid index and/or address");
		return;
	}

	DBG("context->network %p", context->network);

	service = connman_service_lookup_from_network(context->network);
	if (!service)
		return;

	DBG("service %p", service);

	connman_service_create_ip4config(service, index);
	connman_network_set_ipv4_method(context->network, method);

	if (method == CONNMAN_IPCONFIG_METHOD_FIXED ||
			method == CONNMAN_IPCONFIG_METHOD_DHCP)	{
		setip = true;
	}

	if (method == CONNMAN_IPCONFIG_METHOD_FIXED) {
		connman_network_set_ipaddress(context->network,
						context->ipv4_address);
	}

	method = context->ipv6_method;
	connman_service_create_ip6config(service, index);
	connman_network_set_ipv6_method(context->network, method);

	if (method == CONNMAN_IPCONFIG_METHOD_AUTO) {
		setip = true;
	}

	/* Set the nameservers */
	if (context->ipv4_nameservers &&
		    context->ipv6_nameservers) {
		nameservers = g_strdup_printf("%s %s",
				    context->ipv4_nameservers,
				    context->ipv6_nameservers);
		connman_network_set_nameservers(context->network, nameservers);
		g_free(nameservers);
	} else if (context->ipv4_nameservers) {
		connman_network_set_nameservers(context->network,
				    context->ipv4_nameservers);
	} else if (context->ipv6_nameservers) {
		connman_network_set_nameservers(context->network,
				    context->ipv6_nameservers);
	}

	if (setip) {
		connman_network_set_index(context->network, index);
		connman_network_set_connected(context->network, true);
	}
}

static void set_disconnected(struct network_context *context)
{
	DBG("%s", context->path);

	if (context->network)
		connman_network_set_connected(context->network, false);

	g_free(context->ipv4_nameservers);
	context->ipv4_nameservers = NULL;
	if (context->ipv4_method != CONNMAN_IPCONFIG_METHOD_OFF)
		context->ipv4_method =
			CONNMAN_IPCONFIG_METHOD_UNKNOWN;

	g_free(context->ipv6_nameservers);
	context->ipv6_nameservers = NULL;
	if (context->ipv6_method != CONNMAN_IPCONFIG_METHOD_OFF)
		context->ipv6_method =
			CONNMAN_IPCONFIG_METHOD_UNKNOWN;
}

typedef void (*set_property_cb)(struct modem_data *data,
				struct network_context *context, bool success);
typedef void (*get_properties_cb)(struct modem_data *data,
				DBusMessageIter *dict);

struct property_info {
	struct modem_data *modem;
	struct network_context *context;
	const char *path;
	const char *interface;
	const char *property;
	set_property_cb set_property_cb;
	get_properties_cb get_properties_cb;
};

static void free_property_info(void * memory)
{
	struct property_info * info = memory;

	if (info->context)
		network_context_unref(info->context);

	g_free(info);
}

static void set_property_reply(DBusPendingCall *call, void *user_data)
{
	struct property_info *info = user_data;
	DBusMessage *reply;
	DBusError error;
	bool success = true;

	DBG("%s path %s %s.%s", info->modem->path,
		info->path, info->interface, info->property);

	info->modem->call_set_property = NULL;

	dbus_error_init(&error);

	reply = dbus_pending_call_steal_reply(call);

	if (dbus_set_error_from_message(&error, reply)) {
		connman_error("Failed to change property: %s %s.%s: %s %s",
				info->path, info->interface, info->property,
				error.name, error.message);
		dbus_error_free(&error);
		success = false;
	}

	if (info->set_property_cb)
		(*info->set_property_cb)(info->modem, info->context,
						success);

	dbus_message_unref(reply);

	dbus_pending_call_unref(call);
}

static int set_property(struct modem_data *modem,
			struct network_context *context,
			const char *path, const char *interface,
			const char *property, int type, void *value,
			set_property_cb notify)
{
	DBusMessage *message;
	DBusMessageIter iter;
	struct property_info *info;

	DBG("%s path %s %s.%s", modem->path, path, interface, property);

	if (modem->call_set_property) {
		DBG("Cancel pending SetProperty");
		dbus_pending_call_cancel(modem->call_set_property);
		dbus_pending_call_unref(modem->call_set_property);
		modem->call_set_property = NULL;
	}

	message = dbus_message_new_method_call(OFONO_SERVICE, path,
					interface, SET_PROPERTY);
	if (!message)
		return -ENOMEM;

	dbus_message_iter_init_append(message, &iter);
	connman_dbus_property_append_basic(&iter, property, type, value);

	if (!dbus_connection_send_with_reply(connection, message,
					&modem->call_set_property, TIMEOUT)) {
		connman_error("Failed to change property: %s %s.%s",
				path, interface, property);
		dbus_message_unref(message);
		return -EINVAL;
	}

	if (!modem->call_set_property) {
		connman_error("D-Bus connection not available");
		dbus_message_unref(message);
		return -EINVAL;
	}

	info = g_try_new0(struct property_info, 1);
	if (!info) {
		dbus_message_unref(message);
		return -ENOMEM;
	}

	info->modem = modem;
	info->context = context;
	info->path = path;
	info->interface = interface;
	info->property = property;
	info->set_property_cb = notify;

	if (info->context)
		network_context_ref(info->context);

	dbus_pending_call_set_notify(modem->call_set_property,
				set_property_reply, info, free_property_info);

	dbus_message_unref(message);

	return -EINPROGRESS;
}

static void get_properties_reply(DBusPendingCall *call, void *user_data)
{
	struct property_info *info = user_data;
	DBusMessageIter array, dict;
	DBusMessage *reply;
	DBusError error;

	DBG("%s path %s %s", info->modem->path, info->path, info->interface);

	info->modem->call_get_properties = NULL;

	dbus_error_init(&error);

	reply = dbus_pending_call_steal_reply(call);

	if (dbus_set_error_from_message(&error, reply)) {
		connman_error("Failed to get properties: %s %s: %s %s",
				info->path, info->interface,
				error.name, error.message);
		dbus_error_free(&error);

		goto done;
	}

	if (!dbus_message_iter_init(reply, &array))
		goto done;

	if (dbus_message_iter_get_arg_type(&array) != DBUS_TYPE_ARRAY)
		goto done;

	dbus_message_iter_recurse(&array, &dict);

	if (info->get_properties_cb)
		(*info->get_properties_cb)(info->modem, &dict);

done:

	dbus_message_unref(reply);

	dbus_pending_call_unref(call);
}

static int get_properties(const char *path, const char *interface,
				get_properties_cb notify,
				struct modem_data *modem)
{
	DBusMessage *message;
	struct property_info *info;

	DBG("%s path %s %s", modem->path, path, interface);

	if (modem->call_get_properties) {
		connman_error("Pending GetProperties");
		return -EBUSY;
	}

	message = dbus_message_new_method_call(OFONO_SERVICE, path,
					interface, GET_PROPERTIES);
	if (!message)
		return -ENOMEM;

	if (!dbus_connection_send_with_reply(connection, message,
					&modem->call_get_properties, TIMEOUT)) {
		connman_error("Failed to call %s.GetProperties()", interface);
		dbus_message_unref(message);
		return -EINVAL;
	}

	if (!modem->call_get_properties) {
		connman_error("D-Bus connection not available");
		dbus_message_unref(message);
		return -EINVAL;
	}

	info = g_try_new0(struct property_info, 1);
	if (!info) {
		dbus_message_unref(message);
		return -ENOMEM;
	}

	info->modem = modem;
	info->path = path;
	info->interface = interface;
	info->get_properties_cb = notify;

	dbus_pending_call_set_notify(modem->call_get_properties,
					get_properties_reply, info, g_free);

	dbus_message_unref(message);

	return -EINPROGRESS;
}

static void context_set_active_reply(struct modem_data *modem,
				struct network_context *context, bool success)
{
	DBG("%s", context->path);

	if (success) {
		/*
		 * Don't handle do anything on success here. oFono will send
		 * the change via PropertyChanged signal.
		 */
		return;
	}

	/*
	 * Active = True might fail due a timeout. That means oFono
	 * still tries to go online. If we retry to set Active = True,
	 * we just get a InProgress error message. Should we power
	 * cycle the modem in such cases?
	 */

	if (!context->network) {
		/*
		 * In the case where we power down the device
		 * we don't wait for the reply, therefore the network
		 * might already be gone.
		 */
		return;
	}

	connman_network_set_error(context->network,
				CONNMAN_NETWORK_ERROR_ASSOCIATE_FAIL);
}

static int context_set_active(struct modem_data *modem,
			struct network_context *context,
			dbus_bool_t active)
{
	int err;

	DBG("%s active %d", modem->path, active);

	err = set_property(modem, context, context->path,
				OFONO_CONTEXT_INTERFACE,
				"Active", DBUS_TYPE_BOOLEAN,
				&active,
				context_set_active_reply);

	if (!active && err == -EINPROGRESS)
		return 0;

	return err;
}

static int modem_set_online(struct modem_data *modem, dbus_bool_t online)
{
	DBG("%s online %d", modem->path, online);

	return set_property(modem, NULL, modem->path,
				OFONO_MODEM_INTERFACE,
				"Online", DBUS_TYPE_BOOLEAN,
				&online,
				NULL);
}

static int cm_set_powered(struct modem_data *modem, dbus_bool_t powered)
{
	int err;

	DBG("%s powered %d", modem->path, powered);

	err = set_property(modem, NULL, modem->path,
				OFONO_CM_INTERFACE,
				"Powered", DBUS_TYPE_BOOLEAN,
				&powered,
				NULL);

	if (!powered && err == -EINPROGRESS)
		return 0;

	return err;
}

static int modem_set_powered(struct modem_data *modem, dbus_bool_t powered)
{
	int err;

	DBG("%s powered %d", modem->path, powered);

	modem->set_powered = powered;

	err = set_property(modem, NULL, modem->path,
				OFONO_MODEM_INTERFACE,
				"Powered", DBUS_TYPE_BOOLEAN,
				&powered,
				NULL);

	if (!powered && err == -EINPROGRESS)
		return 0;

	return err;
}

static bool has_interface(uint8_t interfaces,
					enum ofono_api api)
{
	if ((interfaces & api) == api)
		return true;

	return false;
}

struct flag_map {
	const char *value;
	uint32_t flag;
};

static int string_list_to_flags(DBusMessageIter *array,
				const struct flag_map *map, size_t map_len,
				uint32_t *out_flags)
{
	DBusMessageIter entry;
	uint32_t flags = 0;

	if (dbus_message_iter_get_arg_type(array) != DBUS_TYPE_ARRAY)
		return -EINVAL;

	if (dbus_message_iter_get_element_type(array) != DBUS_TYPE_STRING)
		return -EINVAL;

	dbus_message_iter_recurse(array, &entry);

	while (dbus_message_iter_get_arg_type(&entry) == DBUS_TYPE_STRING) {
		size_t i;
		const char *name;

		dbus_message_iter_get_basic(&entry, &name);

		for (i = 0; i < map_len; i++)
			if (!strcmp(name, map[i].value))
				flags |= map[i].flag;

		dbus_message_iter_next(&entry);
	}

	if (out_flags)
		*out_flags = flags;

	return 0;
}

static uint8_t extract_interfaces(DBusMessageIter *array)
{
	static const struct flag_map interfaces_map[] = {
		{ .value = OFONO_SIM_INTERFACE, .flag = OFONO_API_SIM },
		{ .value = OFONO_NETREG_INTERFACE, .flag = OFONO_API_NETREG },
		{ .value = OFONO_CM_INTERFACE, .flag = OFONO_API_CM },
		{ .value = OFONO_LTE_INTERFACE, .flag = OFONO_API_LTE },
	};
	uint32_t flags;

	if (string_list_to_flags(array, interfaces_map,
					G_N_ELEMENTS(interfaces_map),
					&flags) < 0)
		return 0;

	return flags;
}

static uint8_t extract_capabilities(DBusMessageIter *array)
{
	static const struct flag_map capabilities_map[] = {
		{ .value = "lte", .flag = LTE_CAPABLE },
	};
	uint32_t flags;

	if (string_list_to_flags(array, capabilities_map,
					G_N_ELEMENTS(capabilities_map),
					&flags) < 0)
		return 0;

	return flags;
}

static char *extract_nameservers(DBusMessageIter *array)
{
	DBusMessageIter entry;
	char *nameservers = NULL;
	char *tmp;

	dbus_message_iter_recurse(array, &entry);

	while (dbus_message_iter_get_arg_type(&entry) == DBUS_TYPE_STRING) {
		const char *nameserver;

		dbus_message_iter_get_basic(&entry, &nameserver);

		if (!nameservers) {
			nameservers = g_strdup(nameserver);
		} else {
			tmp = nameservers;
			nameservers = g_strdup_printf("%s %s", tmp, nameserver);
			g_free(tmp);
		}

		dbus_message_iter_next(&entry);
	}

	return nameservers;
}

static void extract_ipv4_settings(DBusMessageIter *array,
				struct network_context *context)
{
	DBusMessageIter dict;
	char *address = NULL, *netmask = NULL, *gateway = NULL;
	char *nameservers = NULL;
	const char *interface = NULL;
	int index = -1;

	connman_ipaddress_free(context->ipv4_address);
	context->ipv4_address = NULL;

	if (dbus_message_iter_get_arg_type(array) != DBUS_TYPE_ARRAY)
		return;

	dbus_message_iter_recurse(array, &dict);

	while (dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_DICT_ENTRY) {
		DBusMessageIter entry, value;
		const char *key, *val;

		dbus_message_iter_recurse(&dict, &entry);
		dbus_message_iter_get_basic(&entry, &key);

		dbus_message_iter_next(&entry);
		dbus_message_iter_recurse(&entry, &value);

		if (g_str_equal(key, "Interface")) {
			dbus_message_iter_get_basic(&value, &interface);

			DBG("Interface %s", interface);

			index = connman_inet_ifindex(interface);

			DBG("index %d", index);
		} else if (g_str_equal(key, "Method")) {
			dbus_message_iter_get_basic(&value, &val);

			DBG("Method %s", val);

			if (g_strcmp0(val, "static") == 0)
				context->ipv4_method = CONNMAN_IPCONFIG_METHOD_FIXED;
			else if (g_strcmp0(val, "dhcp") == 0)
				context->ipv4_method = CONNMAN_IPCONFIG_METHOD_DHCP;

		} else if (g_str_equal(key, "Address")) {
			dbus_message_iter_get_basic(&value, &val);

			address = g_strdup(val);

			DBG("Address %s", address);
		} else if (g_str_equal(key, "Netmask")) {
			dbus_message_iter_get_basic(&value, &val);

			netmask = g_strdup(val);

			DBG("Netmask %s", netmask);
		} else if (g_str_equal(key, "DomainNameServers")) {
			nameservers = extract_nameservers(&value);

			DBG("Nameservers %s", nameservers);
		} else if (g_str_equal(key, "Gateway")) {
			dbus_message_iter_get_basic(&value, &val);

			gateway = g_strdup(val);

			DBG("Gateway %s", gateway);
		}

		dbus_message_iter_next(&dict);
	}

	if (index < 0)
		goto out;

	context->index = index;

	if (context->ipv4_method != CONNMAN_IPCONFIG_METHOD_FIXED)
		goto out;

	context->ipv4_address = connman_ipaddress_alloc(AF_INET);
	if (!context->ipv4_address) {
		context->index = -1;
		goto out;
	}

	connman_ipaddress_set_ipv4(context->ipv4_address, address,
				netmask, gateway);

	g_free(context->ipv4_nameservers);
	context->ipv4_nameservers = nameservers;

out:
	if (context->ipv4_nameservers != nameservers)
		g_free(nameservers);

	g_free(address);
	g_free(netmask);
	g_free(gateway);
}

static void extract_ipv6_settings(DBusMessageIter *array,
				struct network_context *context)
{
	DBusMessageIter dict;
	char *address = NULL, *gateway = NULL;
	unsigned char prefix_length = 0;
	char *nameservers = NULL;
	const char *interface = NULL;
	int index = -1;

	connman_ipaddress_free(context->ipv6_address);
	context->ipv6_address = NULL;

	if (dbus_message_iter_get_arg_type(array) != DBUS_TYPE_ARRAY)
		return;

	dbus_message_iter_recurse(array, &dict);

	while (dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_DICT_ENTRY) {
		DBusMessageIter entry, value;
		const char *key, *val;

		dbus_message_iter_recurse(&dict, &entry);
		dbus_message_iter_get_basic(&entry, &key);

		dbus_message_iter_next(&entry);
		dbus_message_iter_recurse(&entry, &value);

		if (g_str_equal(key, "Interface")) {
			dbus_message_iter_get_basic(&value, &interface);

			DBG("Interface %s", interface);

			index = connman_inet_ifindex(interface);

			DBG("index %d", index);
		} else if (g_str_equal(key, "Address")) {
			dbus_message_iter_get_basic(&value, &val);

			address = g_strdup(val);

			DBG("Address %s", address);
		} else if (g_str_equal(key, "PrefixLength")) {
			dbus_message_iter_get_basic(&value, &prefix_length);

			DBG("prefix length %d", prefix_length);
		} else if (g_str_equal(key, "DomainNameServers")) {
			nameservers = extract_nameservers(&value);

			DBG("Nameservers %s", nameservers);
		} else if (g_str_equal(key, "Gateway")) {
			dbus_message_iter_get_basic(&value, &val);

			gateway = g_strdup(val);

			DBG("Gateway %s", gateway);
		}

		dbus_message_iter_next(&dict);
	}

	if (index < 0)
		goto out;

	context->ipv6_method = CONNMAN_IPCONFIG_METHOD_AUTO;

	context->ipv6_address =
		connman_ipaddress_alloc(AF_INET6);
	if (!context->ipv6_address)
		goto out;

	context->index = index;
	connman_ipaddress_set_ipv6(context->ipv6_address, address,
				prefix_length, gateway);

	g_free(context->ipv6_nameservers);
	context->ipv6_nameservers = nameservers;

out:
	if (context->ipv6_nameservers != nameservers)
		g_free(nameservers);

	g_free(address);
	g_free(gateway);
}

/*
 * This functions tests if we have the necessary information gathered
 * in order to create the device.
 */
static bool try_create_device(struct modem_data *modem)
{
	struct connman_device *device;
	char *ident;

	DBG("%s", modem->path);

	if (modem->device)
		return false;

	if (!modem->imsi)
		return false;

	if ((modem->capabilities & LTE_CAPABLE) &&
			!has_interface(modem->interfaces, OFONO_API_LTE))
		return false;

	/*
	 * Create the device and register it at the core. Enabling (setting
	 * it online is done through the modem_enable() callback.
	 */
	device = connman_device_create("ofono", CONNMAN_DEVICE_TYPE_CELLULAR);
	if (!device) {
		connman_error("Failed to create device for modem on path: %s",
				modem->path);
		return false;
	}

	DBG("created device %p", device);

	if (!connman_dbus_validate_ident(modem->imsi))
		ident = connman_dbus_encode_string(modem->imsi);
	else
		ident = g_strdup(modem->imsi);

	connman_device_set_ident(device, ident);
	g_free(ident);

	connman_device_set_string(device, "Path", modem->path);
	connman_device_set_data(device, modem);

	if (connman_device_register(device) < 0) {
		connman_error("Failed to register cellular device %p", device);
		connman_device_unref(device);
		return false;
	}

	modem->device = device;
	connman_device_set_powered(modem->device, modem->online);

	/*
	 * The order in which the LTE and Connection Manager interfaces
	 * arrive may not always be the same.
	 *
	 * If the latter arrived before the former and a modem device was
	 * successfully created, this represents a secondary opportunity
	 * to get Connection Manager properties and contexts, both
	 * essential to successfully creating a Connection Manager
	 * Cellular network and service.
	 *
	 * The primary opportunity would have been with api_added in
	 * modem_update_interfaces; however, that would have been skipped
	 * since the LTE interface did not yet exist in such a scenario
	 * and this call would exit early due to the LTE_CAPABLE and
	 * OFONO_API_LTE conditional checks above.
	 */
	if (has_interface(modem->interfaces, OFONO_API_CM)) {
		cm_get_properties(modem);
		cm_get_contexts(modem);
	}

	return true;
}

static void destroy_device(struct modem_data *modem)
{
	DBG("%s", modem->path);

	connman_device_set_powered(modem->device, false);

	connman_device_unregister(modem->device);
	connman_device_unref(modem->device);

	modem->device = NULL;
}

static void add_network(struct modem_data *modem,
			struct network_context *context)
{
	const char *group;

	DBG("modem %p path %s context %p path %s network %p",
		modem, modem->path,
		context, context->path, context->network);

	if (context->network)
		return;

	context->network = connman_network_create(context->path,
					CONNMAN_NETWORK_TYPE_CELLULAR);
	if (!context->network)
		return;

	DBG("network %p", context->network);

	connman_network_set_data(context->network, modem);

	connman_network_set_string(context->network, "Path",
					context->path);

	if (modem->name)
		connman_network_set_name(context->network, modem->name);
	else
		connman_network_set_name(context->network, "");

	connman_network_set_strength(context->network, modem->strength);

	group = get_ident(context->path);
	connman_network_set_group(context->network, group);

	connman_network_set_bool(context->network, "Roaming",
				modem->roaming);

	if (connman_device_add_network(modem->device, context->network) < 0) {
		connman_network_unref(context->network);
		context->network = NULL;
		return;
	}
}

static void remove_network(struct modem_data *modem,
		struct network_context *context)
{
	DBG("%s", modem->path);

	if (!context || !context->network)
		return;

	DBG("network %p", context->network);

	if (modem->device)
		connman_device_remove_network(modem->device, context->network);
	connman_network_unref(context->network);
	context->network = NULL;
}

static int set_context_ipconfig(struct network_context *context,
				const char *protocol)
{
	DBG("context %p protocol %s", context, protocol);

	if (!context || !protocol)
		return -EINVAL;

	if (g_str_equal(protocol, "ip")) {
		if (context->ipv4_method == CONNMAN_IPCONFIG_METHOD_OFF)
			context->ipv4_method = CONNMAN_IPCONFIG_METHOD_UNKNOWN;

		context->ipv6_method = CONNMAN_IPCONFIG_METHOD_OFF;

		connman_ipaddress_free(context->ipv6_address);
		context->ipv6_address = NULL;

	} else if (g_str_equal(protocol, "ipv6")) {
		if (context->ipv6_method == CONNMAN_IPCONFIG_METHOD_OFF)
			context->ipv6_method = CONNMAN_IPCONFIG_METHOD_UNKNOWN;

		context->ipv4_method = CONNMAN_IPCONFIG_METHOD_OFF;

		connman_ipaddress_free(context->ipv4_address);
		context->ipv4_address = NULL;

	} else if (g_str_equal(protocol, "dual")) {
		if (context->ipv4_method == CONNMAN_IPCONFIG_METHOD_OFF)
			context->ipv4_method = CONNMAN_IPCONFIG_METHOD_UNKNOWN;

		if (context->ipv6_method == CONNMAN_IPCONFIG_METHOD_OFF)
			context->ipv6_method = CONNMAN_IPCONFIG_METHOD_UNKNOWN;
	}

	DBG("ipv4 method %d ipv6 method %d", context->ipv4_method,
		context->ipv6_method);

	return 0;
}

/**
 *  @brief
 *    Attempt to add an oFono context.
 *
 *  This evaluates the oFono context with the specified path and
 *  dictionary. If it finds one of type 'internet', it adds the
 *  context to the modem context list; adds the modem to the context
 *  hash, keyed off the context path; and, if the context has a valid
 *  APN, the modem is attached, and has a network registration
 *  interface, a connman network object is created for the context.
 *
 *  @param[in,out]  modem         A pointer to the mutable modem data
 *                                instance associated with @a
 *                                context_path.
 *  @param[in]      context_path  A pointer to an immutable, null-
 *                                terminated C string containing the
 *                                path of the context to evaluate.
 *  @param[in]      dict          A pointer to a D-Bus message iterator
 *                                for the dictionary associated with
 *                                the context to evaluate.
 *
 *  @retval  0        If successful.
 *  @retval  -ENOMEM  If memory could not be allocated for a context
 *                    instance.
 *  @retval  -EINVAL  If the context was not of type 'internet'.
 *
 *  @private
 *
 */
static int add_cm_context(struct modem_data *modem, const char *context_path,
				DBusMessageIter *dict)
{
	const char *context_type = NULL;
	struct network_context *context = NULL;
	dbus_bool_t active = FALSE;
	const char *ip_protocol = NULL;
	int err = 0;

	DBG("%s context path %s", modem->path, context_path);

	context = network_context_alloc(context_path);
	if (!context) {
		err = -ENOMEM;
		goto done;
	}

	while (dbus_message_iter_get_arg_type(dict) == DBUS_TYPE_DICT_ENTRY) {
		DBusMessageIter entry, value;
		const char *key;

		dbus_message_iter_recurse(dict, &entry);
		dbus_message_iter_get_basic(&entry, &key);

		dbus_message_iter_next(&entry);
		dbus_message_iter_recurse(&entry, &value);

		if (g_str_equal(key, "Type")) {
			dbus_message_iter_get_basic(&value, &context_type);

			DBG("%s context %s type %s", modem->path,
				context_path, context_type);
		} else if (g_str_equal(key, "Settings")) {
			DBG("%s Settings", modem->path);

			extract_ipv4_settings(&value, context);
		} else if (g_str_equal(key, "IPv6.Settings")) {
			DBG("%s IPv6.Settings", modem->path);

			extract_ipv6_settings(&value, context);
		} else if (g_str_equal(key, "Active")) {
			dbus_message_iter_get_basic(&value, &active);

			DBG("%s Active %d", modem->path, active);
		} else if (g_str_equal(key, "AccessPointName")) {
			const char *apn;

			dbus_message_iter_get_basic(&value, &apn);
			if (apn && strlen(apn) > 0)
				context->valid_apn = true;
			else
				context->valid_apn = false;

			DBG("%s AccessPointName '%s'", modem->path, apn);
		}  else if (g_str_equal(key, "Protocol") &&
			dbus_message_iter_get_arg_type(&value) == DBUS_TYPE_STRING ) {

			dbus_message_iter_get_basic(&value, &ip_protocol);

			DBG("%s Protocol %s", modem->path, ip_protocol);
		}

		dbus_message_iter_next(dict);
	}

	if (g_strcmp0(context_type, "internet") != 0) {
		network_context_unref(context);
		err = -EINVAL;
		goto done;
	}

	if (ip_protocol)
		set_context_ipconfig(context, ip_protocol);

	context->active = active;

	modem->context_list = g_slist_prepend(modem->context_list, context);
	g_hash_table_replace(context_hash, g_strdup(context_path), modem);

	if (context->valid_apn && modem->attached &&
	    has_interface(modem->interfaces, OFONO_API_NETREG))
		add_network(modem, context);

done:
	DBG("err %d", err);

	return err;
}

static void remove_cm_context(struct modem_data *modem,
				struct network_context *context)
{
	if (!modem->context_list)
		return;
	if (!context)
		return;

	g_hash_table_remove(context_hash, context->path);

	if (context->network)
		remove_network(modem, context);
	modem->context_list = g_slist_remove(modem->context_list, context);

	network_context_unref(context);
	context = NULL;
}

static void remove_all_contexts(struct modem_data *modem)
{
	GSList *list = NULL;

	DBG("");

	if (modem->context_list == NULL)
		return;

	list = modem->context_list;
	while (list) {
		struct network_context *context = list->data;

		remove_cm_context(modem, context);

		list = modem->context_list;
	}
	g_slist_free(modem->context_list);
	modem->context_list = NULL;
}

static void remove_all_networks(struct modem_data *modem)
{
	GSList *list;

	for (list = modem->context_list; list; list = list->next) {
		struct network_context *context = list->data;

		remove_network(modem, context);
	}
}

static gboolean context_changed(DBusConnection *conn,
				DBusMessage *message,
				void *user_data)
{
	struct network_context *context = NULL;
	const char *context_path = dbus_message_get_path(message);
	struct modem_data *modem = NULL;
	DBusMessageIter iter, value;
	const char *key;

	DBG("context_path %s", context_path);

	/*
	 * If there is no modem in the context hash for the associated
	 * context path, then there is nothing to do at the moment as
	 * the context must first be added before we can process changes
	 * to it.
	 */
	modem = g_hash_table_lookup(context_hash, context_path);
	if (!modem)
		return TRUE;

	DBG("modem %p", modem);

	context = get_context_with_path(modem->context_list, context_path);
	if (!context)
		return TRUE;

	DBG("context %p", context);

	if (!dbus_message_iter_init(message, &iter))
		return TRUE;

	dbus_message_iter_get_basic(&iter, &key);

	dbus_message_iter_next(&iter);
	dbus_message_iter_recurse(&iter, &value);

	/*
	 * oFono guarantees the ordering of Settings and
	 * Active. Settings will always be send before Active = True.
	 * That means we don't have to order here.
	 */
	if (g_str_equal(key, "Settings")) {
		DBG("%s Settings", modem->path);

		extract_ipv4_settings(&value, context);
	} else if (g_str_equal(key, "IPv6.Settings")) {
		DBG("%s IPv6.Settings", modem->path);

		extract_ipv6_settings(&value, context);
	} else if (g_str_equal(key, "Active")) {
		dbus_bool_t active;

		dbus_message_iter_get_basic(&value, &active);
		context->active = active;

		DBG("%s Active %d", modem->path, context->active);

		if (context->active)
			set_connected(modem, context);
		else
			set_disconnected(context);
	} else if (g_str_equal(key, "AccessPointName")) {
		const char *apn;

		dbus_message_iter_get_basic(&value, &apn);

		DBG("%s AccessPointName %s", modem->path, apn);

		if (apn && strlen(apn) > 0) {
			context->valid_apn = true;

			if (context->network)
				return TRUE;

			if (!modem->attached)
				return TRUE;

			if (!has_interface(modem->interfaces,
						OFONO_API_NETREG))
				return TRUE;

			add_network(modem, context);

			if (context->active)
				set_connected(modem, context);
		} else {
			context->valid_apn = false;

			if (!context->network)
				return TRUE;

			remove_network(modem, context);
		}

	} else if (g_str_equal(key, "Protocol") &&
		dbus_message_iter_get_arg_type(&value) == DBUS_TYPE_STRING ) {
		const char *ip_protocol;

		dbus_message_iter_get_basic(&value, &ip_protocol);

		DBG("%s Protocol %s", modem->path, ip_protocol);

		set_context_ipconfig(context, ip_protocol);
	}

	return TRUE;
}

static void cm_get_contexts_reply(DBusPendingCall *call, void *user_data)
{
	struct modem_data *modem = user_data;
	DBusMessageIter array, dict, entry, value;
	DBusMessage *reply;
	DBusError error;

	DBG("%s", modem->path);

	modem->call_get_contexts = NULL;

	reply = dbus_pending_call_steal_reply(call);

	dbus_error_init(&error);

	if (dbus_set_error_from_message(&error, reply)) {
		connman_error("%s", error.message);
		dbus_error_free(&error);
		goto done;
	}

	if (!dbus_message_iter_init(reply, &array))
		goto done;

	if (dbus_message_iter_get_arg_type(&array) != DBUS_TYPE_ARRAY)
		goto done;

	dbus_message_iter_recurse(&array, &dict);

	while (dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_STRUCT) {
		const char *context_path;

		dbus_message_iter_recurse(&dict, &entry);
		dbus_message_iter_get_basic(&entry, &context_path);

		dbus_message_iter_next(&entry);
		dbus_message_iter_recurse(&entry, &value);

		/*
		 * If a context of type 'internet' is found, stop iterating;
		 * we have the desired context.
		 */
		if (add_cm_context(modem, context_path, &value) == 0)
			break;

		dbus_message_iter_next(&dict);
	}

done:
	dbus_message_unref(reply);

	dbus_pending_call_unref(call);
}

static int cm_get_contexts(struct modem_data *modem)
{
	DBusMessage *message;

	DBG("%s", modem->path);

	if (modem->call_get_contexts)
		return -EBUSY;

	message = dbus_message_new_method_call(OFONO_SERVICE, modem->path,
					OFONO_CM_INTERFACE, GET_CONTEXTS);
	if (!message)
		return -ENOMEM;

	if (!dbus_connection_send_with_reply(connection, message,
					&modem->call_get_contexts, TIMEOUT)) {
		connman_error("Failed to call GetContexts()");
		dbus_message_unref(message);
		return -EINVAL;
	}

	if (!modem->call_get_contexts) {
		connman_error("D-Bus connection not available");
		dbus_message_unref(message);
		return -EINVAL;
	}

	dbus_pending_call_set_notify(modem->call_get_contexts,
					cm_get_contexts_reply,
					modem, NULL);

	dbus_message_unref(message);

	return -EINPROGRESS;
}

static gboolean cm_context_added(DBusConnection *conn,
					DBusMessage *message,
					void *user_data)
{
	const char *path = dbus_message_get_path(message);
	char *context_path;
	struct modem_data *modem;
	DBusMessageIter iter, properties, dict;

	DBG("%s", path);

	modem = g_hash_table_lookup(modem_hash, path);
	if (!modem)
		return TRUE;

	if (!dbus_message_iter_init(message, &iter))
		return TRUE;

	dbus_message_iter_get_basic(&iter, &context_path);

	dbus_message_iter_next(&iter);
	dbus_message_iter_recurse(&iter, &properties);

	/* Sometimes, we get an array instead of dict */
	if (dbus_message_iter_get_arg_type(&properties) == DBUS_TYPE_ARRAY) {
		/* Must recurse again */
		dbus_message_iter_recurse(&properties, &dict);
		if (add_cm_context(modem, context_path, &dict) != 0)
			return TRUE;
	}
	if (add_cm_context(modem, context_path, &properties) != 0)
		return TRUE;

	return TRUE;
}

static gboolean cm_context_removed(DBusConnection *conn,
					DBusMessage *message,
					void *user_data)
{
	const char *path = dbus_message_get_path(message);
	const char *context_path;
	struct modem_data *modem;
	struct network_context *context;
	DBusMessageIter iter;

	DBG("context path %s", path);

	if (!dbus_message_iter_init(message, &iter))
		return TRUE;

	dbus_message_iter_get_basic(&iter, &context_path);

	modem = g_hash_table_lookup(context_hash, context_path);
	if (!modem)
		return TRUE;

	DBG("modem %p", modem);

	context = get_context_with_path(modem->context_list, context_path);

	DBG("context %p", context);

	remove_cm_context(modem, context);

	return TRUE;
}

static void netreg_update_name(struct modem_data *modem,
				DBusMessageIter* value)
{
	char *name;
	GSList *list;

	dbus_message_iter_get_basic(value, &name);

	DBG("%s Name %s", modem->path, name);

	g_free(modem->name);
	modem->name = g_strdup(name);

	if (!modem->context_list)
		return;

	/* For all the context */
	for (list = modem->context_list; list; list = list->next) {
		struct network_context *context = list->data;

		if (context->network) {
			connman_network_set_name(context->network, modem->name);
			connman_network_update(context->network);
		}
	}
}

static void netreg_update_strength(struct modem_data *modem,
					DBusMessageIter *value)
{
	GSList *list;

	dbus_message_iter_get_basic(value, &modem->strength);

	DBG("%s Strength %d", modem->path, modem->strength);

	if (!modem->context_list)
		return;

	/* For all the context */
	for (list = modem->context_list; list; list = list->next) {
		struct network_context *context = list->data;

		if (context->network) {
			connman_network_set_strength(context->network,
							modem->strength);
			connman_network_update(context->network);
		}
	}
}

static void netreg_update_status(struct modem_data *modem,
					DBusMessageIter *value)
{
	char *status;
	bool roaming;
	GSList *list;

	dbus_message_iter_get_basic(value, &status);

	roaming = g_str_equal(status, "roaming");
	modem->registered = roaming || g_str_equal(status, "registered");

	if (roaming == modem->roaming)
		return;

	modem->roaming = roaming;

	if (!modem->context_list)
		return;

	/* For all the context */
	for (list = modem->context_list; list; list = list->next) {
		struct network_context *context = list->data;

		if (context->network) {
			connman_network_set_bool(context->network,
				"Roaming", modem->roaming);
			connman_network_update(context->network);
		}
	}
}

static void netreg_update_regdom(struct modem_data *modem,
				DBusMessageIter *value)
{
	char *mobile_country_code;
	char *alpha2;
	int mcc;

	/* Do not change regdom here if it is set to follow timezone. */
	if (connman_setting_get_bool("RegdomFollowsTimezone"))
		return;

	dbus_message_iter_get_basic(value, &mobile_country_code);

	DBG("%s MobileContryCode %s", modem->path, mobile_country_code);


	mcc = atoi(mobile_country_code);
	if (mcc > 799 || mcc < 200)
		return;

	alpha2 = mcc_country_codes[mcc - 200];
	if (alpha2)
		connman_technology_set_regdom(alpha2);
}

static gboolean netreg_changed(DBusConnection *conn, DBusMessage *message,
				void *user_data)
{
	const char *path = dbus_message_get_path(message);
	struct modem_data *modem;
	DBusMessageIter iter, value;
	const char *key;

	modem = g_hash_table_lookup(modem_hash, path);
	if (!modem)
		return TRUE;

	if (modem->ignore)
		return TRUE;

	if (!dbus_message_iter_init(message, &iter))
		return TRUE;

	dbus_message_iter_get_basic(&iter, &key);

	dbus_message_iter_next(&iter);
	dbus_message_iter_recurse(&iter, &value);

	if (g_str_equal(key, "Name"))
		netreg_update_name(modem, &value);
	else if (g_str_equal(key, "Strength"))
		netreg_update_strength(modem, &value);
	else if (g_str_equal(key, "Status"))
		netreg_update_status(modem, &value);
	else if (g_str_equal(key, "MobileCountryCode"))
		netreg_update_regdom(modem, &value);

	return TRUE;
}

static void netreg_properties_reply(struct modem_data *modem,
					DBusMessageIter *dict)
{
	GSList *list = NULL;

	DBG("%s", modem->path);

	while (dbus_message_iter_get_arg_type(dict) == DBUS_TYPE_DICT_ENTRY) {
		DBusMessageIter entry, value;
		const char *key;

		dbus_message_iter_recurse(dict, &entry);
		dbus_message_iter_get_basic(&entry, &key);

		dbus_message_iter_next(&entry);
		dbus_message_iter_recurse(&entry, &value);

		if (g_str_equal(key, "Name"))
			netreg_update_name(modem, &value);
		else if (g_str_equal(key, "Strength"))
			netreg_update_strength(modem, &value);
		else if (g_str_equal(key, "Status"))
			netreg_update_status(modem, &value);
		else if (g_str_equal(key, "MobileCountryCode"))
			netreg_update_regdom(modem, &value);

		dbus_message_iter_next(dict);
	}

	if (!modem->context_list) {
		/*
		 * netgreg_get_properties() was issued after we got
		 * cm_get_contexts_reply() where we create the
		 * context. Though before we got the
		 * netreg_properties_reply the context was removed
		 * again. Therefore we have to skip the network
		 * creation.
		 */
		return;
	}
	/* Check for all contexts if they are valids and/or actives */
	for (list = modem->context_list; list; list = list->next) {
		struct network_context *context = list->data;

		if (context->valid_apn)
			add_network(modem, context);
		if (context->active)
			set_connected(modem, context);
	}
}

static int netreg_get_properties(struct modem_data *modem)
{
	return get_properties(modem->path, OFONO_NETREG_INTERFACE,
			netreg_properties_reply, modem);
}

static void cm_update_attached(struct modem_data *modem,
				DBusMessageIter *value)
{
	dbus_bool_t attached;

	dbus_message_iter_get_basic(value, &attached);
	modem->attached = attached;

	DBG("%s Attached %d", modem->path, modem->attached);

	if (!modem->attached) {
		remove_all_networks(modem);
		return;
	}

	if (!has_interface(modem->interfaces, OFONO_API_NETREG))
		return;

	netreg_get_properties(modem);
}

static void cm_update_powered(struct modem_data *modem,
				DBusMessageIter *value)
{
	dbus_bool_t cm_powered;

	dbus_message_iter_get_basic(value, &cm_powered);
	modem->cm_powered = cm_powered;

	DBG("%s ConnnectionManager Powered %d", modem->path,
		modem->cm_powered);

	if (modem->cm_powered)
		return;

	cm_set_powered(modem, TRUE);
}

static gboolean cm_changed(DBusConnection *conn, DBusMessage *message,
				void *user_data)
{
	const char *path = dbus_message_get_path(message);
	struct modem_data *modem;
	DBusMessageIter iter, value;
	const char *key;

	DBG("");

	modem = g_hash_table_lookup(modem_hash, path);
	if (!modem)
		return TRUE;

	DBG("modem %p modem->ignore %u", modem, modem->ignore);

	if (modem->ignore)
		return TRUE;

	if (!dbus_message_iter_init(message, &iter))
		return TRUE;

	dbus_message_iter_get_basic(&iter, &key);

	dbus_message_iter_next(&iter);
	dbus_message_iter_recurse(&iter, &value);

	if (g_str_equal(key, "Attached"))
		cm_update_attached(modem, &value);
	else if (g_str_equal(key, "Powered"))
		cm_update_powered(modem, &value);

	return TRUE;
}

static void cm_properties_reply(struct modem_data *modem, DBusMessageIter *dict)
{
	DBG("%s", modem->path);

	while (dbus_message_iter_get_arg_type(dict) == DBUS_TYPE_DICT_ENTRY) {
		DBusMessageIter entry, value;
		const char *key;

		dbus_message_iter_recurse(dict, &entry);
		dbus_message_iter_get_basic(&entry, &key);

		dbus_message_iter_next(&entry);
		dbus_message_iter_recurse(&entry, &value);

		if (g_str_equal(key, "Attached"))
			cm_update_attached(modem, &value);
		else if (g_str_equal(key, "Powered"))
			cm_update_powered(modem, &value);

		dbus_message_iter_next(dict);
	}
}

static int cm_get_properties(struct modem_data *modem)
{
	return get_properties(modem->path, OFONO_CM_INTERFACE,
				cm_properties_reply, modem);
}

static void sim_update_imsi(struct modem_data *modem,
				DBusMessageIter *value)
{
	char *imsi;

	dbus_message_iter_get_basic(value, &imsi);

	DBG("%s imsi %s", modem->path, imsi);

	g_free(modem->imsi);
	modem->imsi = g_strdup(imsi);
}

static gboolean sim_changed(DBusConnection *conn, DBusMessage *message,
				void *user_data)
{
	const char *path = dbus_message_get_path(message);
	struct modem_data *modem;
	DBusMessageIter iter, value;
	const char *key;

	modem = g_hash_table_lookup(modem_hash, path);
	if (!modem)
		return TRUE;

	DBG("modem %p modem->ignore %u", modem, modem->ignore);

	if (modem->ignore)
		return TRUE;

	if (!dbus_message_iter_init(message, &iter))
		return TRUE;

	dbus_message_iter_get_basic(&iter, &key);

	dbus_message_iter_next(&iter);
	dbus_message_iter_recurse(&iter, &value);

	DBG("key %s", key);

	if (g_str_equal(key, "SubscriberIdentity")) {
		sim_update_imsi(modem, &value);
		try_create_device(modem);
	}

	return TRUE;
}

static void sim_properties_reply(struct modem_data *modem,
					DBusMessageIter *dict)
{
	DBG("%s", modem->path);

	while (dbus_message_iter_get_arg_type(dict) == DBUS_TYPE_DICT_ENTRY) {
		DBusMessageIter entry, value;
		const char *key;

		dbus_message_iter_recurse(dict, &entry);
		dbus_message_iter_get_basic(&entry, &key);

		dbus_message_iter_next(&entry);
		dbus_message_iter_recurse(&entry, &value);

		if (g_str_equal(key, "SubscriberIdentity")) {
			sim_update_imsi(modem, &value);

			if (!try_create_device(modem))
				return;

			if (!modem->online)
				return;

			/*
			 * The modem is already online and we have the CM interface.
			 * There will be no interface update and therefore our
			 * state machine will not go to next step. We have to
			 * trigger it from here.
			 */
			if (has_interface(modem->interfaces, OFONO_API_CM)) {
				cm_get_properties(modem);
				cm_get_contexts(modem);
			}
			return;
		}

		dbus_message_iter_next(dict);
	}
}

static int sim_get_properties(struct modem_data *modem)
{
	return get_properties(modem->path, OFONO_SIM_INTERFACE,
				sim_properties_reply, modem);
}

static bool api_added(uint8_t old_iface, uint8_t new_iface,
				enum ofono_api api)
{
	if (!has_interface(old_iface, api) &&
			has_interface(new_iface, api)) {
		DBG("%s added", api2string(api));
		return true;
	}

	return false;
}

static bool api_removed(uint8_t old_iface, uint8_t new_iface,
				enum ofono_api api)
{
	if (has_interface(old_iface, api) &&
			!has_interface(new_iface, api)) {
		DBG("%s removed", api2string(api));
		return true;
	}

	return false;
}

static void modem_update_interfaces(struct modem_data *modem,
				uint8_t old_ifaces,
				uint8_t new_ifaces)
{
	DBG("modem %p path %s", modem, modem->path);

	if (api_added(old_ifaces, new_ifaces, OFONO_API_SIM)) {
		DBG("modem->imsi %p modem->set_powered %u",
				modem->imsi, modem->set_powered);

		if (!modem->imsi &&
				!modem->set_powered) {
			/*
			 * Only use do GetProperties() when
			 * device has not been powered up.
			 */
			sim_get_properties(modem);
		}
	}

	if (api_added(old_ifaces, new_ifaces, OFONO_API_CM)) {
		DBG("modem->device %p", modem->device);

		if (modem->device) {
			cm_get_properties(modem);
			cm_get_contexts(modem);
		}
	}

	if (api_added(old_ifaces, new_ifaces, OFONO_API_NETREG)) {
		if (modem->attached)
			netreg_get_properties(modem);
	}

	if (api_added(old_ifaces, new_ifaces, OFONO_API_LTE))
		try_create_device(modem);

	if (api_removed(old_ifaces, new_ifaces, OFONO_API_CM)) {
		if (modem->call_get_contexts) {
			DBG("cancelling pending GetContexts call");
			dbus_pending_call_cancel(modem->call_get_contexts);
			dbus_pending_call_unref(modem->call_get_contexts);
			modem->call_get_contexts = NULL;
		}
		remove_all_contexts(modem);
	}

	if (api_removed(old_ifaces, new_ifaces, OFONO_API_NETREG))
		remove_all_networks(modem);
}

static gboolean modem_changed(DBusConnection *conn, DBusMessage *message,
				void *user_data)
{
	const char *path = dbus_message_get_path(message);
	struct modem_data *modem;
	DBusMessageIter iter, value;
	const char *key;

	modem = g_hash_table_lookup(modem_hash, path);
	if (!modem)
		return TRUE;

	if (modem->ignore)
		return TRUE;

	if (!dbus_message_iter_init(message, &iter))
		return TRUE;

	dbus_message_iter_get_basic(&iter, &key);

	dbus_message_iter_next(&iter);
	dbus_message_iter_recurse(&iter, &value);

	if (g_str_equal(key, "Powered")) {
		dbus_bool_t powered;

		dbus_message_iter_get_basic(&value, &powered);

		modem->powered = powered;
		modem->set_powered = powered;

		DBG("%s Powered %d", modem->path, modem->powered);
	} else if (g_str_equal(key, "Online")) {
		dbus_bool_t online;

		dbus_message_iter_get_basic(&value, &online);
		modem->online = online;

		DBG("%s Online %d", modem->path, modem->online);

		if (!modem->device)
			return TRUE;

		connman_device_set_powered(modem->device, modem->online);
	} else if (g_str_equal(key, "Interfaces")) {
		uint8_t new_interfaces = extract_interfaces(&value);
		uint8_t old_interfaces = modem->interfaces;

		if (new_interfaces == old_interfaces)
			return TRUE;

		DBG("%s Interfaces 0x%02x", modem->path, new_interfaces);
		modem->interfaces = new_interfaces;
		modem_update_interfaces(modem, old_interfaces, new_interfaces);
	} else if (g_str_equal(key, "Serial")) {
		char *serial;

		dbus_message_iter_get_basic(&value, &serial);

		g_free(modem->serial);
		modem->serial = g_strdup(serial);

		DBG("%s Serial %s", modem->path, modem->serial);
	} else if (g_str_equal(key, "Capabilities")) {
		modem->capabilities = extract_capabilities(&value);
		return TRUE;
	}

	return TRUE;
}

static void add_modem(const char *path, DBusMessageIter *prop)
{
	struct modem_data *modem;

	DBG("%s", path);

	modem = g_hash_table_lookup(modem_hash, path);
	if (modem) {
		/*
		 * When oFono powers up we ask for the modems and oFono is
		 * reporting with modem_added signal the modems. Only
		 * handle them once.
		 */
		return;
	}

	modem = g_try_new0(struct modem_data, 1);
	if (!modem)
		return;

	modem->path = g_strdup(path);

	g_hash_table_insert(modem_hash, g_strdup(path), modem);

	while (dbus_message_iter_get_arg_type(prop) == DBUS_TYPE_DICT_ENTRY) {
		DBusMessageIter entry, value;
		const char *key;

		dbus_message_iter_recurse(prop, &entry);
		dbus_message_iter_get_basic(&entry, &key);

		dbus_message_iter_next(&entry);
		dbus_message_iter_recurse(&entry, &value);

		if (g_str_equal(key, "Powered")) {
			dbus_bool_t powered;

			dbus_message_iter_get_basic(&value, &powered);
			modem->powered = powered;

			DBG("%s Powered %d", modem->path, modem->powered);
		} else if (g_str_equal(key, "Online")) {
			dbus_bool_t online;

			dbus_message_iter_get_basic(&value, &online);
			modem->online = online;

			DBG("%s Online %d", modem->path, modem->online);
		} else if (g_str_equal(key, "Interfaces")) {
			modem->interfaces = extract_interfaces(&value);

			DBG("%s Interfaces 0x%02x", modem->path,
				modem->interfaces);
		} else if (g_str_equal(key, "Serial")) {
			char *serial;

			dbus_message_iter_get_basic(&value, &serial);
			modem->serial = g_strdup(serial);

			DBG("%s Serial %s", modem->path, modem->serial);
		} else if (g_str_equal(key, "Type")) {
			char *type;

			dbus_message_iter_get_basic(&value, &type);

			DBG("%s Type %s", modem->path, type);
			if (g_strcmp0(type, "hardware") != 0) {
				DBG("%s Ignore this modem", modem->path);
				modem->ignore = true;
			}
		} else if (g_str_equal(key, "Capabilities")) {
			modem->capabilities = extract_capabilities(&value);

			DBG("lte capable: %s",
					modem->capabilities & LTE_CAPABLE ?
					"yes" : "no");
		}

		dbus_message_iter_next(prop);
	}

	if (modem->ignore)
		return;

	if (!modem->powered) {
		modem_set_powered(modem, TRUE);
		return;
	}

	modem_update_interfaces(modem, 0, modem->interfaces);
}

static void modem_power_down(gpointer key, gpointer value, gpointer user_data)
{
	struct modem_data *modem = value;

	DBG("%s", modem->path);

	if (modem->ignore)
		return;

	modem_set_powered(modem, FALSE);
}

static void remove_modem(gpointer data)
{
	struct modem_data *modem = data;

	DBG("%s", modem->path);

	if (modem->call_set_property) {
		dbus_pending_call_cancel(modem->call_set_property);
		dbus_pending_call_unref(modem->call_set_property);
		modem->call_set_property = NULL;
	}

	if (modem->call_get_properties) {
		dbus_pending_call_cancel(modem->call_get_properties);
		dbus_pending_call_unref(modem->call_get_properties);
		modem->call_get_properties = NULL;
	}

	if (modem->call_get_contexts) {
		dbus_pending_call_cancel(modem->call_get_contexts);
		dbus_pending_call_unref(modem->call_get_contexts);
		modem->call_get_contexts = NULL;
	}

	/* Must remove the contexts before the device */
	if (modem->context_list)
		remove_all_contexts(modem);

	if (modem->device)
		destroy_device(modem);

	g_free(modem->serial);
	g_free(modem->name);
	g_free(modem->imsi);
	g_free(modem->path);

	g_free(modem);
}

static gboolean modem_added(DBusConnection *conn,
				DBusMessage *message, void *user_data)
{
	DBusMessageIter iter, properties;
	const char *path;

	DBG("");

	if (!dbus_message_iter_init(message, &iter))
		return TRUE;

	dbus_message_iter_get_basic(&iter, &path);

	dbus_message_iter_next(&iter);
	dbus_message_iter_recurse(&iter, &properties);

	add_modem(path, &properties);

	return TRUE;
}

static gboolean modem_removed(DBusConnection *conn,
				DBusMessage *message, void *user_data)
{
	DBusMessageIter iter;
	const char *path;

	DBG("");

	if (!dbus_message_iter_init(message, &iter))
		return TRUE;

	dbus_message_iter_get_basic(&iter, &path);

	g_hash_table_remove(modem_hash, path);

	return TRUE;
}

static void manager_get_modems_reply(DBusPendingCall *call, void *user_data)
{
	DBusMessage *reply;
	DBusError error;
	DBusMessageIter array, dict;

	DBG("");

	reply = dbus_pending_call_steal_reply(call);

	dbus_error_init(&error);

	if (dbus_set_error_from_message(&error, reply)) {
		connman_error("%s", error.message);
		dbus_error_free(&error);
		goto done;
	}

	if (!dbus_message_iter_init(reply, &array))
		goto done;

	dbus_message_iter_recurse(&array, &dict);

	while (dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_STRUCT) {
		DBusMessageIter value, properties;
		const char *path;

		dbus_message_iter_recurse(&dict, &value);
		dbus_message_iter_get_basic(&value, &path);

		dbus_message_iter_next(&value);
		dbus_message_iter_recurse(&value, &properties);

		add_modem(path, &properties);

		dbus_message_iter_next(&dict);
	}

done:
	dbus_message_unref(reply);

	dbus_pending_call_unref(call);
}

static int manager_get_modems(void)
{
	DBusMessage *message;
	DBusPendingCall *call;

	DBG("");

	message = dbus_message_new_method_call(OFONO_SERVICE, "/",
					OFONO_MANAGER_INTERFACE, GET_MODEMS);
	if (!message)
		return -ENOMEM;

	if (!dbus_connection_send_with_reply(connection, message,
						&call, TIMEOUT)) {
		connman_error("Failed to call GetModems()");
		dbus_message_unref(message);
		return -EINVAL;
	}

	if (!call) {
		connman_error("D-Bus connection not available");
		dbus_message_unref(message);
		return -EINVAL;
	}

	dbus_pending_call_set_notify(call, manager_get_modems_reply,
					NULL, NULL);

	dbus_message_unref(message);

	return -EINPROGRESS;
}

static void ofono_connect(DBusConnection *conn, void *user_data)
{
	DBG("");

	modem_hash = g_hash_table_new_full(g_str_hash, g_str_equal,
						g_free, remove_modem);
	if (!modem_hash)
		return;

	context_hash = g_hash_table_new_full(g_str_hash, g_str_equal,
						g_free, NULL);
	if (!context_hash) {
		g_hash_table_destroy(modem_hash);
		return;
	}

	manager_get_modems();
}

static void ofono_disconnect(DBusConnection *conn, void *user_data)
{
	DBG("");

	if (!modem_hash || !context_hash)
		return;

	g_hash_table_destroy(modem_hash);
	modem_hash = NULL;

	g_hash_table_destroy(context_hash);
	context_hash = NULL;
}

static int network_probe(struct connman_network *network)
{
	struct modem_data *modem = connman_network_get_data(network);

	DBG("%s network %p", modem->path, network);

	return 0;
}

static void network_remove(struct connman_network *network)
{
	struct modem_data *modem = connman_network_get_data(network);

	DBG("%s network %p", modem->path, network);
}

static int network_connect(struct connman_network *network)
{
	struct network_context *context;
	struct modem_data *modem = connman_network_get_data(network);

	DBG("%s network %p", modem->path, network);

	if (!g_hash_table_lookup(modem_hash, modem->path))
		return -ENODEV;

	context = get_context_with_network(modem->context_list, network);
	if (!context)
		return -ENODEV;

	if (has_interface(modem->interfaces, OFONO_API_CM))
		return context_set_active(modem, context, TRUE);

	connman_error("Connection manager interface not available");

	return -ENOSYS;
}

static int network_disconnect(struct connman_network *network)
{
	struct network_context *context;
	struct modem_data *modem = connman_network_get_data(network);

	DBG("%s network %p", modem->path, network);

	if (!g_hash_table_lookup(modem_hash, modem->path))
		return -ENODEV;

	context = get_context_with_network(modem->context_list, network);
	if (!context)
		return -ENODEV;

	if (has_interface(modem->interfaces, OFONO_API_CM))
		return context_set_active(modem, context, FALSE);

	connman_error("Connection manager interface not available");

	return -ENOSYS;
}

static struct connman_network_driver network_driver = {
	.name		= "cellular",
	.type		= CONNMAN_NETWORK_TYPE_CELLULAR,
	.probe		= network_probe,
	.remove		= network_remove,
	.connect	= network_connect,
	.disconnect	= network_disconnect,
};

static int modem_probe(struct connman_device *device)
{
	struct modem_data *modem = connman_device_get_data(device);

	DBG("%s device %p", modem->path, device);

	return 0;
}

static void modem_remove(struct connman_device *device)
{
	struct modem_data *modem = connman_device_get_data(device);

	DBG("%s device %p", modem->path, device);
}

static int modem_enable(struct connman_device *device)
{
	struct modem_data *modem = connman_device_get_data(device);

	DBG("%s device %p", modem->path, device);

	if (modem->online)
		return 0;

	return modem_set_online(modem, TRUE);
}

static int modem_disable(struct connman_device *device)
{
	struct modem_data *modem = connman_device_get_data(device);

	DBG("%s device %p", modem->path, device);

	if (!modem->online)
		return 0;

	return modem_set_online(modem, FALSE);
}

static struct connman_device_driver modem_driver = {
	.name		= "modem",
	.type		= CONNMAN_DEVICE_TYPE_CELLULAR,
	.probe		= modem_probe,
	.remove		= modem_remove,
	.enable		= modem_enable,
	.disable	= modem_disable,
};

static int tech_probe(struct connman_technology *technology)
{
	return 0;
}

static void tech_remove(struct connman_technology *technology)
{
}

static struct connman_technology_driver tech_driver = {
	.name		= "cellular",
	.type		= CONNMAN_SERVICE_TYPE_CELLULAR,
	.probe		= tech_probe,
	.remove		= tech_remove,
};

static guint watch;
static guint modem_added_watch;
static guint modem_removed_watch;
static guint modem_watch;
static guint cm_watch;
static guint sim_watch;
static guint context_added_watch;
static guint context_removed_watch;
static guint netreg_watch;
static guint context_watch;

static int ofono_init(void)
{
	int err;

	DBG("");

	connection = connman_dbus_get_connection();
	if (!connection)
		return -EIO;

	watch = g_dbus_add_service_watch(connection,
					OFONO_SERVICE, ofono_connect,
					ofono_disconnect, NULL, NULL);

	modem_added_watch = g_dbus_add_signal_watch(connection, OFONO_SERVICE,
						NULL, OFONO_MANAGER_INTERFACE,
						MODEM_ADDED,
						modem_added,
						NULL, NULL);

	modem_removed_watch = g_dbus_add_signal_watch(connection,
						OFONO_SERVICE, NULL,
						OFONO_MANAGER_INTERFACE,
						MODEM_REMOVED,
						modem_removed,
						NULL, NULL);

	modem_watch = g_dbus_add_signal_watch(connection, OFONO_SERVICE, NULL,
						OFONO_MODEM_INTERFACE,
						PROPERTY_CHANGED,
						modem_changed,
						NULL, NULL);

	cm_watch = g_dbus_add_signal_watch(connection, OFONO_SERVICE, NULL,
						OFONO_CM_INTERFACE,
						PROPERTY_CHANGED,
						cm_changed,
						NULL, NULL);

	sim_watch = g_dbus_add_signal_watch(connection, OFONO_SERVICE, NULL,
						OFONO_SIM_INTERFACE,
						PROPERTY_CHANGED,
						sim_changed,
						NULL, NULL);

	context_added_watch = g_dbus_add_signal_watch(connection,
						OFONO_SERVICE, NULL,
						OFONO_CM_INTERFACE,
						CONTEXT_ADDED,
						cm_context_added,
						NULL, NULL);

	context_removed_watch = g_dbus_add_signal_watch(connection,
						OFONO_SERVICE, NULL,
						OFONO_CM_INTERFACE,
						CONTEXT_REMOVED,
						cm_context_removed,
						NULL, NULL);

	context_watch = g_dbus_add_signal_watch(connection, OFONO_SERVICE,
						NULL, OFONO_CONTEXT_INTERFACE,
						PROPERTY_CHANGED,
						context_changed,
						NULL, NULL);

	netreg_watch = g_dbus_add_signal_watch(connection, OFONO_SERVICE, NULL,
						OFONO_NETREG_INTERFACE,
						PROPERTY_CHANGED,
						netreg_changed,
						NULL, NULL);

	if (watch == 0 || modem_added_watch == 0 || modem_removed_watch == 0 ||
			modem_watch == 0 || cm_watch == 0 || sim_watch == 0 ||
			context_added_watch == 0 ||
			context_removed_watch == 0 ||
			context_watch == 0 || netreg_watch == 0) {
		err = -EIO;
		goto remove;
	}

	err = connman_network_driver_register(&network_driver);
	if (err < 0)
		goto remove;

	err = connman_device_driver_register(&modem_driver);
	if (err < 0) {
		connman_network_driver_unregister(&network_driver);
		goto remove;
	}

	err = connman_technology_driver_register(&tech_driver);
	if (err < 0) {
		connman_device_driver_unregister(&modem_driver);
		connman_network_driver_unregister(&network_driver);
		goto remove;
	}

	return 0;

remove:
	g_dbus_remove_watch(connection, netreg_watch);
	g_dbus_remove_watch(connection, context_watch);
	g_dbus_remove_watch(connection, context_removed_watch);
	g_dbus_remove_watch(connection, context_added_watch);
	g_dbus_remove_watch(connection, sim_watch);
	g_dbus_remove_watch(connection, cm_watch);
	g_dbus_remove_watch(connection, modem_watch);
	g_dbus_remove_watch(connection, modem_removed_watch);
	g_dbus_remove_watch(connection, modem_added_watch);
	g_dbus_remove_watch(connection, watch);
	dbus_connection_unref(connection);

	return err;
}

static void ofono_exit(void)
{
	DBG("");

	if (modem_hash) {
		/*
		 * We should probably wait for the SetProperty() reply
		 * message, because ...
		 */
		g_hash_table_foreach(modem_hash, modem_power_down, NULL);

		/*
		 * ... here we will cancel the call.
		 */
		g_hash_table_destroy(modem_hash);
		modem_hash = NULL;
	}

	if (context_hash) {
		g_hash_table_destroy(context_hash);
		context_hash = NULL;
	}

	connman_technology_driver_unregister(&tech_driver);
	connman_device_driver_unregister(&modem_driver);
	connman_network_driver_unregister(&network_driver);

	g_dbus_remove_watch(connection, netreg_watch);
	g_dbus_remove_watch(connection, context_watch);
	g_dbus_remove_watch(connection, context_removed_watch);
	g_dbus_remove_watch(connection, context_added_watch);
	g_dbus_remove_watch(connection, sim_watch);
	g_dbus_remove_watch(connection, cm_watch);
	g_dbus_remove_watch(connection, modem_watch);
	g_dbus_remove_watch(connection, modem_added_watch);
	g_dbus_remove_watch(connection, modem_removed_watch);
	g_dbus_remove_watch(connection, watch);

	dbus_connection_unref(connection);
}

CONNMAN_PLUGIN_DEFINE(ofono, "oFono telephony plugin", VERSION,
		CONNMAN_PLUGIN_PRIORITY_DEFAULT, ofono_init, ofono_exit)
