---
plugins/mobile-broadband-provider-info.c | 804 ++++++++++++++++++++++++++++++
1 files changed, 804 insertions(+), 0 deletions(-)
create mode 100644 plugins/mobile-broadband-provider-info.c
diff --git a/plugins/mobile-broadband-provider-info.c
b/plugins/mobile-broadband-provider-info.c
new file mode 100644
index 0000000..57a888f
--- /dev/null
+++ b/plugins/mobile-broadband-provider-info.c
@@ -0,0 +1,804 @@
+/*
+ *
+ * oFono - Open Source Telephony
+ *
+ * Copyright (C) 2008-2011 Intel Corporation. All rights reserved.
+ *
+ * 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 <sys/stat.h>
+#include <sys/mman.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <glib.h>
+
+#define OFONO_API_SUBJECT_TO_CHANGE
+#include <ofono/types.h>
+#include <ofono/log.h>
+#include <ofono/plugin.h>
+#include <ofono/modem.h>
+#include <ofono/gprs-provision.h>
+
+#define MAX_APS 4
+
+#define _(x) case x: return (#x)
+
+enum provider_info_flags {
+ PROVIDER_INFO_FLAG_MATCH_FOUND = 0x01,
+ PROVIDER_INFO_FLAG_AP_COMMIT = 0x02,
+ PROVIDER_INFO_FLAG_INTERNET = 0x04,
+ PROVIDER_INFO_FLAG_MMS = 0x08,
+ PROVIDER_INFO_FLAG_WAP = 0x10,
+};
+
+enum element_type {
+ ELEMENT_NONE,
+ ELEMENT_PROVIDER_NAME,
+ ELEMENT_SPN,
+ ELEMENT_GSM,
+ ELEMENT_NETWORK_ID,
+ ELEMENT_APN,
+ ELEMENT_USAGE,
+ ELEMENT_PLAN,
+ ELEMENT_NAME,
+ ELEMENT_USERNAME,
+ ELEMENT_PASSWORD,
+ ELEMENT_MAX,
+};
+
+enum parser_return_code {
+ PARSER_OK,
+ PARSER_ERROR,
+};
+
+enum parser_match_type {
+ PARSER_MATCH_ALL,
+ PARSER_MATCH_ONLY,
+};
+
+struct element_parser {
+ enum element_type type;
+ enum parser_match_type match_type;
+ GMarkupParser parser;
+};
+
+struct provider_info {
+ struct ofono_gprs_provision_data settings[MAX_APS];
+ struct ofono_gprs_provision_data *ap;
+ int ap_count;
+ char *spn;
+ const char *match_mcc;
+ const char *match_mnc;
+ const char *match_spn;
+ enum element_type element;
+ GQuark g_error_domain;
+ const struct element_parser *subparser;
+ guint8 flags;
+};
+
+static struct provider_info provider_info;
+
+static const char *ap_type_name(enum ofono_gprs_context_type type)
+{
+ switch (type) {
+ _(OFONO_GPRS_CONTEXT_TYPE_ANY);
+ _(OFONO_GPRS_CONTEXT_TYPE_INTERNET);
+ _(OFONO_GPRS_CONTEXT_TYPE_MMS);
+ _(OFONO_GPRS_CONTEXT_TYPE_WAP);
+ _(OFONO_GPRS_CONTEXT_TYPE_IMS);
+ }
+
+ return "OFONO_GPRS_CONTEXT_TYPE_<UNKNOWN>";
+}
+
+static enum element_type element_type(const gchar *element,
+ GMarkupParseContext *context)
+{
+ switch (element[0]) {
+ case 'a':
+ if (g_str_equal(element, "apn") == TRUE)
+ return ELEMENT_APN;
+ break;
+ case 'g':
+ if (g_str_equal(element, "gsm") == TRUE)
+ return ELEMENT_GSM;
+ break;
+ case 'n':
+ if (g_str_equal(element, "name") == TRUE) {
+ GSList *l = (GSList *)
+ g_markup_parse_context_get_element_stack
+ (context);
+
+ if (!g_slist_length(l))
+ break;
+
+ if (g_str_equal(g_slist_nth_data(l, 1),
+ "provider") == TRUE)
+ return ELEMENT_PROVIDER_NAME;
+ if (g_str_equal(g_slist_nth_data(l, 1),
+ "apn") == TRUE)
+ return ELEMENT_NAME;
+ break;
+ }
+ if (g_str_equal(element, "network-id") == TRUE)
+ return ELEMENT_NETWORK_ID;
+ break;
+ case 'p':
+ if (g_str_equal(element, "plan") == TRUE)
+ return ELEMENT_PLAN;
+ if (g_str_equal(element, "password") == TRUE)
+ return ELEMENT_PASSWORD;
+ break;
+ case 'u':
+ if (g_str_equal(element, "usage") == TRUE)
+ return ELEMENT_USAGE;
+ if (g_str_equal(element, "username") == TRUE)
+ return ELEMENT_USERNAME;
+ }
+
+ return ELEMENT_NONE;
+}
+
+static struct ofono_gprs_provision_data *ap_new(struct provider_info *data)
+{
+ if (data->ap_count < MAX_APS) {
+ struct ofono_gprs_provision_data *ap =
+ data->settings + data->ap_count++;
+
+ data->flags &= ~PROVIDER_INFO_FLAG_AP_COMMIT;
+
+ DBG("%p", ap);
+
+ return ap;
+
+ }
+
+ return NULL;
+}
+
+static void ap_value_free(char **value)
+{
+ g_free(*value);
+
+ *value = NULL;
+}
+
+static void ap_delete(struct provider_info *data)
+{
+ struct ofono_gprs_provision_data *ap = data->ap;
+
+ DBG("%p", ap);
+
+ ap_value_free(&ap->name);
+
+ ap_value_free(&ap->apn);
+
+ ap_value_free(&ap->username);
+
+ ap_value_free(&ap->password);
+
+ ap->type = OFONO_GPRS_CONTEXT_TYPE_ANY;
+
+ data->ap_count--;
+
+ data->ap--;
+}
+
+static void provider_info_free(struct provider_info *data)
+{
+ while (data->ap_count)
+ ap_delete(data);
+
+ g_free(data->spn);
+
+ data->spn = NULL;
+}
+
+static int ap_type_valid(struct ofono_gprs_provision_data *ap)
+{
+ switch (ap->type) {
+ case OFONO_GPRS_CONTEXT_TYPE_INTERNET:
+ case OFONO_GPRS_CONTEXT_TYPE_MMS:
+ case OFONO_GPRS_CONTEXT_TYPE_WAP:
+ return 1;
+ default:
+ break;
+ }
+
+ return 0;
+}
+
+static int ap_type_is_configured(struct provider_info *data,
+ enum ofono_gprs_context_type type)
+{
+ switch (type) {
+ case OFONO_GPRS_CONTEXT_TYPE_INTERNET:
+ return data->flags & PROVIDER_INFO_FLAG_INTERNET;
+ case OFONO_GPRS_CONTEXT_TYPE_MMS:
+ return data->flags & PROVIDER_INFO_FLAG_MMS;
+ case OFONO_GPRS_CONTEXT_TYPE_WAP:
+ return data->flags & PROVIDER_INFO_FLAG_WAP;
+ default:
+ break;
+ }
+
+ return 0;
+}
+
+static void ap_type_mark_configured(struct provider_info *data,
+ enum ofono_gprs_context_type type)
+{
+ switch (type) {
+ case OFONO_GPRS_CONTEXT_TYPE_INTERNET:
+ data->flags |= PROVIDER_INFO_FLAG_INTERNET;
+ break;
+ case OFONO_GPRS_CONTEXT_TYPE_MMS:
+ data->flags |= PROVIDER_INFO_FLAG_MMS;
+ break;
+ case OFONO_GPRS_CONTEXT_TYPE_WAP:
+ data->flags |= PROVIDER_INFO_FLAG_WAP;
+ break;
+ default:
+ break;
+ }
+}
+
+static inline int body_isprint(const gchar *text, gsize text_len)
+{
+ int print = 0;
+ int i;
+
+ for (i = 0; i < (int)text_len; i++) {
+ if (g_ascii_isprint(text[i])) {
+ print = 1;
+ break;
+ }
+ }
+
+ return print;
+}
+
+static inline int subparser_push(enum element_type from,
+ GMarkupParseContext *context,
+ const gchar *element_name,
+ const gchar **attribute_names,
+ const gchar **attribute_values,
+ gpointer user_data, GError **error)
+{
+ struct provider_info *data = user_data;
+ enum element_type type;
+
+ type = element_type(element_name, context);
+
+ if (type == from)
+ return 0;
+
+ if (!data->subparser[type].type ||
+ (data->subparser[type].match_type == PARSER_MATCH_ONLY
+ && !(data->flags & PROVIDER_INFO_FLAG_MATCH_FOUND)))
+ return 1;
+
+ DBG("%s", element_name);
+
+ if (data->subparser[type].parser.start_element)
+ data->subparser[type].parser.start_element(context,
+ element_name,
+ attribute_names,
+ attribute_values,
+ user_data, error);
+
+ g_markup_parse_context_push(context, &data->subparser[type].parser,
+ user_data);
+ return 1;
+}
+
+static inline int subparser_pop(enum element_type from,
+ GMarkupParseContext *context,
+ const gchar *element_name,
+ gpointer user_data, GError **error)
+{
+ struct provider_info *data = user_data;
+ enum element_type type;
+
+ type = element_type(element_name, context);
+
+ if (type == from)
+ return 0;
+
+ if (!data->subparser[type].type ||
+ (data->subparser[type].match_type == PARSER_MATCH_ONLY
+ && !(data->flags & PROVIDER_INFO_FLAG_MATCH_FOUND)))
+ return 1;
+
+ if (data->subparser[type].parser.end_element)
+ data->subparser[type].parser.end_element(context, element_name,
+ user_data, error);
+
+ DBG("%s", element_name);
+
+ g_markup_parse_context_pop(context);
+
+ return 1;
+}
+
+static void network_id_start(GMarkupParseContext *context,
+ const gchar *element_name,
+ const gchar **attribute_names,
+ const gchar **attribute_values,
+ gpointer user_data, GError **error)
+{
+ struct provider_info *data = user_data;
+ const char *mcc = NULL, *mnc = NULL;
+ int i;
+
+ DBG("%s", element_name);
+
+ for (i = 0; attribute_names[i]; i++) {
+ if (g_str_equal(attribute_names[i], "mcc") == TRUE)
+ mcc = attribute_values[i];
+ if (g_str_equal(attribute_names[i], "mnc") == TRUE)
+ mnc = attribute_values[i];
+ }
+
+ if (g_strcmp0(mcc, data->match_mcc) == 0 &&
+ g_strcmp0(mnc, data->match_mnc) == 0)
+ data->flags |= PROVIDER_INFO_FLAG_MATCH_FOUND;
+}
+
+static void gsm_start(GMarkupParseContext *context,
+ const gchar *element_name,
+ const gchar **attribute_names,
+ const gchar **attribute_values,
+ gpointer user_data, GError **error)
+{
+ if (subparser_push(ELEMENT_GSM, context, element_name,
+ attribute_names, attribute_values,
+ user_data, error))
+ return;
+
+ DBG("%s", element_name);
+}
+
+static void gsm_end(GMarkupParseContext *context,
+ const gchar *element_name,
+ gpointer user_data, GError **error)
+{
+ struct provider_info *data;
+
+ if (subparser_pop(ELEMENT_GSM, context, element_name, user_data,
+ error))
+ return;
+
+ DBG("%s", element_name);
+
+ data = user_data;
+
+ if (data->flags & PROVIDER_INFO_FLAG_MATCH_FOUND)
+ g_set_error(error, data->g_error_domain, PARSER_OK,
+ "end parsing");
+}
+
+static void apn_start(GMarkupParseContext *context,
+ const gchar *element_name,
+ const gchar **attribute_names,
+ const gchar **attribute_values,
+ gpointer user_data, GError **error)
+{
+ struct provider_info *data;
+ int i;
+
+ if (subparser_push(ELEMENT_APN, context, element_name,
+ attribute_names, attribute_values,
+ user_data, error))
+ return;
+
+ DBG("%s", element_name);
+
+ data = user_data;
+
+ data->ap = ap_new(data);
+ if (data->ap == NULL) {
+ g_set_error(error, data->g_error_domain, PARSER_ERROR,
+ "MAX_APS %d reached, end parsing", MAX_APS);
+ return;
+ }
+
+ for (i = 0; attribute_names[i]; i++)
+ if (g_str_equal(attribute_names[i], "value") == TRUE) {
+ data->ap->apn = g_strdup(attribute_values[i]);
+ break;
+ }
+}
+
+static void apn_end(GMarkupParseContext *context,
+ const gchar *element_name,
+ gpointer user_data, GError **error)
+{
+ struct provider_info *data;
+
+ if (subparser_pop(ELEMENT_APN, context, element_name, user_data,
+ error))
+ return;
+
+ DBG("%s", element_name);
+
+ data = user_data;
+
+ if (!(data->flags & PROVIDER_INFO_FLAG_AP_COMMIT)) {
+ ap_delete(data);
+ return;
+ }
+
+ if (!ap_type_valid(data->ap)) {
+ DBG("AP '%s', type %s not present, AP ignored",
+ data->ap->apn, ap_type_name(data->ap->type));
+ ap_delete(data);
+ return;
+ }
+
+ ap_type_mark_configured(data, data->ap->type);
+}
+
+static void plan_start(GMarkupParseContext *context,
+ const gchar *element_name,
+ const gchar **attribute_names,
+ const gchar **attribute_values,
+ gpointer user_data, GError **error)
+{
+ struct provider_info *data = user_data;
+ int i;
+
+ DBG("%s", element_name);
+
+ for (i = 0; attribute_names[i]; i++)
+ if (g_str_equal(attribute_names[i], "type") == TRUE) {
+ if (attribute_values[i] == NULL) {
+ DBG("AP '%s', plan is not present, AP ignored",
+ data->ap->apn);
+ break;
+ }
+
+ if (g_str_equal(attribute_values[i],
+ "postpaid") == TRUE ||
+ g_str_equal(attribute_values[i],
+ "prepaid") == TRUE) {
+ data->flags |= PROVIDER_INFO_FLAG_AP_COMMIT;
+ return;
+ }
+
+ DBG("AP '%s', plan '%s' is not supported, AP ignored",
+ data->ap->apn, attribute_values[i]);
+ break;
+ }
+}
+
+static void usage_start(GMarkupParseContext *context,
+ const gchar *element_name,
+ const gchar **attribute_names,
+ const gchar **attribute_values,
+ gpointer user_data, GError **error)
+{
+ const char *usage = NULL;
+ struct provider_info *data;
+ struct ofono_gprs_provision_data *ap;
+ enum ofono_gprs_context_type type;
+ int i;
+
+ DBG("%s", element_name);
+
+ data = user_data;
+ ap = data->ap;
+
+ for (i = 0; attribute_names[i]; i++)
+ if (g_str_equal(attribute_names[i], "type") == TRUE)
+ usage = attribute_values[i];
+
+ if (usage == NULL)
+ return;
+
+ if (g_str_equal(usage, "internet") == TRUE)
+ type = OFONO_GPRS_CONTEXT_TYPE_INTERNET;
+ else if (g_str_equal(usage, "mms") == TRUE)
+ type = OFONO_GPRS_CONTEXT_TYPE_MMS;
+ else if (g_str_equal(usage, "wap") == TRUE)
+ type = OFONO_GPRS_CONTEXT_TYPE_WAP;
+
+ if (ap_type_is_configured(data, type)) {
+ g_set_error(error, data->g_error_domain, PARSER_ERROR,
+ "AP '%s', multiple settings "
+ "for type '%s' found, fail provisioning",
+ ap->apn, ap_type_name(type));
+ ap_delete(data);
+ return;
+ }
+
+ ap->type = type;
+}
+
+static void element_text_set(GMarkupParseContext *context, const gchar *text,
+ gsize text_len, char **s)
+{
+ if (!body_isprint(text, text_len))
+ return;
+
+ DBG("%s '%*s'", g_markup_parse_context_get_element(context),
+ (int)text_len, text);
+
+ *s = g_strndup(text, text_len);
+}
+
+static void name_text(GMarkupParseContext *context, const gchar *text,
+ gsize text_len, gpointer user_data, GError **error)
+{
+ struct provider_info *data = user_data;
+
+ element_text_set(context, text, text_len, &data->ap->name);
+}
+
+static void username_text(GMarkupParseContext *context,
+ const gchar *text, gsize text_len,
+ gpointer user_data, GError **error)
+{
+ struct provider_info *data = user_data;
+
+ element_text_set(context, text, text_len, &data->ap->username);
+}
+
+static void password_text(GMarkupParseContext *context,
+ const gchar *text, gsize text_len,
+ gpointer user_data, GError **error)
+{
+ struct provider_info *data = user_data;
+
+ element_text_set(context, text, text_len, &data->ap->password);
+}
+
+static void element_start(GMarkupParseContext *context,
+ const gchar *element_name,
+ const gchar **attribute_names,
+ const gchar **attribute_values,
+ gpointer user_data, GError **error)
+{
+ struct provider_info *data = user_data;
+
+ if (subparser_push(ELEMENT_NONE, context, element_name, attribute_names,
+ attribute_values, user_data, error))
+ return;
+
+ if (data->flags & PROVIDER_INFO_FLAG_MATCH_FOUND)
+ DBG("%s", element_name);
+}
+
+static void element_end(GMarkupParseContext *context,
+ const gchar *element_name,
+ gpointer user_data, GError **error)
+{
+ struct provider_info *data = user_data;
+
+ if (subparser_pop(ELEMENT_NONE, context, element_name, user_data,
+ error))
+ return;
+
+ if (data->flags & PROVIDER_INFO_FLAG_MATCH_FOUND)
+ DBG("%s", element_name);
+}
+
+static void parser_error(GMarkupParseContext *context,
+ GError *error, gpointer user_data)
+{
+ struct provider_info *data = user_data;
+ gint line_number;
+ gint char_number;
+
+ g_markup_parse_context_get_position(context, &line_number,
+ &char_number);
+
+ if (error->domain != data->g_error_domain) {
+ ofono_error("Error parsing %s, line %d, char %d: %s",
+ PROVIDER_DATABASE, line_number,
+ char_number, error->message);
+ return;
+ }
+
+ if (error->code == PARSER_OK)
+ return;
+
+ ofono_warn("%s, line %d, char %d: %s", PROVIDER_DATABASE, line_number,
+ char_number, error->message);
+
+ provider_info_free(data);
+}
+
+#define _SP(_type, _match, _start, _end, _text) { \
+ ELEMENT_ ## _type, \
+ _match, \
+ { \
+ _start, \
+ _end, \
+ _text, \
+ NULL, \
+ NULL, \
+ } \
+},
+
+static const struct element_parser subparser[ELEMENT_MAX] = {
+ _SP(NONE, PARSER_MATCH_ALL, NULL, NULL, NULL)
+ _SP(PROVIDER_NAME, PARSER_MATCH_ALL, NULL, NULL, NULL)
+ _SP(SPN, PARSER_MATCH_ALL, NULL, NULL, NULL)
+ _SP(GSM, PARSER_MATCH_ALL, gsm_start, gsm_end, NULL)
+ _SP(NETWORK_ID, PARSER_MATCH_ALL, network_id_start, NULL, NULL)
+ _SP(APN, PARSER_MATCH_ONLY, apn_start, apn_end, NULL)
+ _SP(USAGE, PARSER_MATCH_ONLY, usage_start, NULL, NULL)
+ _SP(PLAN, PARSER_MATCH_ONLY, plan_start, NULL, NULL)
+ _SP(NAME, PARSER_MATCH_ONLY, NULL, NULL, name_text)
+ _SP(USERNAME, PARSER_MATCH_ONLY, NULL, NULL, username_text)
+ _SP(PASSWORD, PARSER_MATCH_ONLY, NULL, NULL, password_text)
+};
+
+static const GMarkupParser parser = {
+ element_start,
+ element_end,
+ NULL,
+ NULL,
+ parser_error,
+};
+
+static void parse_database(const char *data, ssize_t size,
+ struct provider_info *provider_info)
+{
+ GMarkupParseContext *context;
+ gboolean result;
+
+ provider_info->flags = 0;
+
+ context = g_markup_parse_context_new(&parser,
+ G_MARKUP_TREAT_CDATA_AS_TEXT,
+ provider_info, NULL);
+
+ result = g_markup_parse_context_parse(context, data, size, NULL);
+ if (result == TRUE)
+ result = g_markup_parse_context_end_parse(context, NULL);
+
+ g_markup_parse_context_free(context);
+}
+
+static int provider_info_lookup(struct provider_info *data, const char *mcc,
+ const char *mnc, const char *spn)
+{
+ struct stat st;
+ char *map;
+ int fd;
+
+ fd = open(PROVIDER_DATABASE, O_RDONLY);
+ if (fd < 0) {
+ ofono_error("Error: open(%s): %s", PROVIDER_DATABASE,
+ strerror(errno));
+ return -errno;
+ }
+
+ if (fstat(fd, &st) < 0) {
+ ofono_error("Error: fstat(%s): %s", PROVIDER_DATABASE,
+ strerror(errno));
+ close(fd);
+ return -errno;
+ }
+
+ map = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0);
+ if (map == MAP_FAILED) {
+ ofono_error("Error: mmap(%s): %s", PROVIDER_DATABASE,
+ strerror(errno));
+ close(fd);
+ return -errno;
+ }
+
+ memset(data, 0, sizeof(struct provider_info));
+
+ data->subparser = subparser;
+
+ data->match_mcc = mcc;
+ data->match_mnc = mnc;
+ data->match_spn = spn;
+
+ parse_database(map, st.st_size, data);
+
+ munmap(map, st.st_size);
+
+ close(fd);
+
+ return data->ap_count;
+}
+
+static int mobile_broadband_provider_info_get(const char *mcc,
+ const char *mnc, const char *spn,
+ struct ofono_gprs_provision_data **settings,
+ int *count)
+{
+ struct ofono_gprs_provision_data *aps;
+ int ap_count;
+ int i;
+
+ DBG("Provisioning for MCC %s, MNC %s, SPN '%s'", mcc, mnc, spn);
+
+ ap_count = provider_info_lookup(&provider_info, mcc, mnc, spn);
+ if (ap_count <= 0) {
+ provider_info_free(&provider_info);
+ return -ENOENT;
+ }
+
+ DBG("Found %d APs", ap_count);
+
+ aps = g_try_malloc_n(ap_count,
+ sizeof(struct ofono_gprs_provision_data));
+ if (aps == NULL) {
+ ofono_error("Error provisioning APNs: memory exhausted");
+ provider_info_free(&provider_info);
+ return -ENOMEM;
+ }
+
+ memcpy(aps, provider_info.settings,
+ sizeof(struct ofono_gprs_provision_data) * ap_count);
+
+ memset(provider_info.settings, 0,
+ sizeof(struct ofono_gprs_provision_data) * ap_count);
+
+ *settings = aps;
+ *count = ap_count;
+
+ for (i = 0; i < ap_count; aps++, i++) {
+ DBG("Name: '%s'", aps->name);
+ DBG("APN: '%s'", aps->apn);
+ DBG("Type: %s", ap_type_name(aps->type));
+ DBG("Username: '%s'", aps->username);
+ DBG("Password: '%s'", aps->password);
+ }
+
+ provider_info_free(&provider_info);
+
+ return 0;
+}
+
+static struct ofono_gprs_provision_driver provision_driver = {
+ .name = "Mobile Broadband Provider Info",
+ .get_settings = mobile_broadband_provider_info_get
+};
+
+static int mobile_broadband_provider_info_init(void)
+{
+ provider_info.g_error_domain = g_quark_from_string(
+ provision_driver.name);
+
+ return ofono_gprs_provision_driver_register(&provision_driver);
+}
+
+static void mobile_broadband_provider_info_exit(void)
+{
+ ofono_gprs_provision_driver_unregister(&provision_driver);
+}
+
+OFONO_PLUGIN_DEFINE(mobile_broadband_provider_info,
+ "Mobile Broadband Provider Info Plugin",
+ VERSION, OFONO_PLUGIN_PRIORITY_DEFAULT,
+ mobile_broadband_provider_info_init,
+ mobile_broadband_provider_info_exit)
--
1.7.4.1