/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* cc-wwan-data.c
 *
 * Copyright 2019 Purism SPC
 *
 * 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 3 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, see <http://www.gnu.org/licenses/>.
 *
 * Author(s):
 *   Mohammed Sadiq <sadiq@sadiqpk.org>
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

#undef G_LOG_DOMAIN
#define G_LOG_DOMAIN "cc-wwan-data"

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

#define _GNU_SOURCE
#include <string.h>
#include <glib/gi18n.h>
#include <nma-mobile-providers.h>

#include "cc-wwan-data.h"

/**
 * @short_description: Device Internet Data Object
 * @include: "cc-wwan-device-data.h"
 *
 * #CcWwanData represents the data object of the given
 * #CcWwanDevice.  Please note that while #CcWWanDevice
 * is bound to the hardware device, #CcWwanData may also
 * depend on the inserted SIM (if supported). So the state
 * of #CcWwanData changes when SIM is changed.
 */

/*
 * Priority for connections. The larger the number, the lower the priority
 * https://developer.gnome.org/NetworkManager/stable/nm-settings.html:
 *
 *   A lower value is better (higher priority). Zero selects a globally
 *   configured default value. If the latter is missing or zero too, it
 *   defaults to 50 for VPNs and 100 for other connections.
 *
 * Since WiFi and other network connections will likely get the default
 * setting of 100, set WWAN DNS priorities higher than the default, with
 * room to allow multiple modems to set priority above/below each other.
 */
#define CC_WWAN_DNS_PRIORITY_LOW  (120)
#define CC_WWAN_DNS_PRIORITY_HIGH (115)

/* These are to be set as route metric */
#define CC_WWAN_ROUTE_PRIORITY_LOW  (1050)
#define CC_WWAN_ROUTE_PRIORITY_HIGH (1040)

struct _CcWwanData
{
  GObject      parent_instance;

  MMObject    *mm_object;
  MMModem     *modem;
  MMSim       *sim;
  gchar       *sim_id;

  gchar       *operator_code; /* MCCMNC */
  GError      *error;

  NMClient           *nm_client;
  NMDevice           *nm_device;
  NMAMobileProvidersDatabase *apn_db;
  NMAMobileProvider  *apn_provider;
  CcWwanDataApn      *default_apn;
  CcWwanDataApn      *old_default_apn;
  GListStore         *apn_list;
  NMActiveConnection *active_connection;

  gint     priority;
  gboolean data_enabled; /* autoconnect enabled */
  gboolean home_only;    /* Data roaming */
};

G_DEFINE_TYPE (CcWwanData, cc_wwan_data, G_TYPE_OBJECT)

/*
 * Default Access Point Settings Logic:
 * For a provided SIM, all the APNs available from NetworkManager
 * that matches the given SIM identifier (ICCID, available via
 * mm_sim_get_identifier() or similar gdbus API) is loaded for
 * the Device (In NetworkManager, it is saved as ‘sim-id’, if
 * present).  At a time, only one connection will be bound to
 * a device.  If there are more than one match, the item with
 * the highest ‘route-metric’ is taken.  If more matches are
 * still available, the first item is chosen.
 *
 * Populating All available APNs:
 * All Possible APNs for the given sim are populated the following
 * way (A list of all the following avoiding duplicates)
 * 1. The above mentioned “Default Access Point Settings Logic”
 * 2. Get All saved Network Manager connections with the
 *    provided MCCMNC of the given SIM
 * 3. Get All possible APNs for the MCCMNC from mobile-provider-info
 *
 * Testing if data is enabled:
 * Check if any of the items from step 1 have ‘autoconnect’ set
 *
 * Checking/Setting current SIM for data (in case of multiple SIM):
 * Since other networks (like wifi, ethernet) should have higher
 * priorities we use a negative number for priority.
 * 1. All APNs by default have priority CC_WWAN_APN_PRIORITY_LOW
 * 2. APN of selected SIM for active data have priority of
 *    CC_WWAN_APN_PRIORITY_HIGH
 *
 * XXX: Since users may create custom APNs via nmtui or like tools
 * we may have to check if there are some inconsistencies with APNs
 * available in NetworkManager, and ask user if they have to reset
 * the APNs that have invalid settings (basically, we care only APNs
 * that are set to have ‘autoconnect’ enabled, and all we need is to
 * disable autoconnect). We won’t interfere CDMA/EVDO networks.
 */
struct _CcWwanDataApn {
  GObject parent_instance;

  /* Set if the APN is from the mobile-provider-info database */
  NMAMobileAccessMethod *access_method;

  /* Set if the APN is saved in NetworkManager */
  NMConnection *nm_connection;
  NMRemoteConnection *remote_connection;

  gboolean modified;
};

G_DEFINE_TYPE (CcWwanDataApn, cc_wwan_data_apn, G_TYPE_OBJECT)

enum {
  PROP_0,
  PROP_ERROR,
  PROP_ENABLED,
  N_PROPS
};

static GParamSpec *properties[N_PROPS];

static void
wwan_data_apn_reset (CcWwanDataApn *apn)
{
  if (!apn)
    return;

  g_clear_object (&apn->nm_connection);
  g_clear_object (&apn->remote_connection);
}

static NMConnection *
wwan_data_get_nm_connection (CcWwanDataApn *apn)
{
  NMConnection *connection;
  NMSetting *setting;
  g_autofree gchar *uuid = NULL;

  if (apn->nm_connection)
    return apn->nm_connection;

  if (apn->remote_connection)
    return NM_CONNECTION (apn->remote_connection);

  connection = nm_simple_connection_new ();
  apn->nm_connection = connection;

  setting = nm_setting_connection_new ();
  uuid = nm_utils_uuid_generate ();
  g_object_set (setting,
                NM_SETTING_CONNECTION_UUID, uuid,
                NM_SETTING_CONNECTION_TYPE, NM_SETTING_GSM_SETTING_NAME,
                NULL);
  nm_connection_add_setting (connection, setting);

  setting = nm_setting_serial_new ();
  nm_connection_add_setting (connection, setting);

  setting = nm_setting_ip4_config_new ();
  g_object_set (setting, NM_SETTING_IP_CONFIG_METHOD, "auto", NULL);
  nm_connection_add_setting (connection, setting);

  nm_connection_add_setting (connection, nm_setting_gsm_new ());
  nm_connection_add_setting (connection, nm_setting_ppp_new ());

  return apn->nm_connection;
}

static gboolean
wwan_data_apn_are_same (CcWwanDataApn         *apn,
                        NMAMobileAccessMethod *access_method)
{
  NMConnection *connection;
  NMSetting *setting;

  if (!apn->remote_connection)
    return FALSE;

  connection = NM_CONNECTION (apn->remote_connection);
  setting = NM_SETTING (nm_connection_get_setting_gsm (connection));

  if (g_strcmp0 (nma_mobile_access_method_get_3gpp_apn (access_method),
                 nm_setting_gsm_get_apn (NM_SETTING_GSM (setting))) != 0)
    return FALSE;

  if (g_strcmp0 (nma_mobile_access_method_get_username (access_method),
                 nm_setting_gsm_get_username (NM_SETTING_GSM (setting))) != 0)
    return FALSE;

  if (g_strcmp0 (nma_mobile_access_method_get_password (access_method),
                 cc_wwan_data_apn_get_password (apn)) != 0)
    return FALSE;

  return TRUE;
}

static CcWwanDataApn *
wwan_data_find_matching_apn (CcWwanData            *self,
                             NMAMobileAccessMethod *access_method)
{
  CcWwanDataApn *apn;
  guint i, n_items;

  n_items = g_list_model_get_n_items (G_LIST_MODEL (self->apn_list));

  for (i = 0; i < n_items; i++)
    {
      apn = g_list_model_get_item (G_LIST_MODEL (self->apn_list), i);

      if (apn->access_method == access_method)
        return apn;

      if (wwan_data_apn_are_same (apn, access_method))
        return apn;

      g_object_unref (apn);
    }

  return NULL;
}

static gboolean
wwan_data_nma_method_is_mms (NMAMobileAccessMethod *method)
{
  const char *str;

  str = nma_mobile_access_method_get_3gpp_apn (method);
  if (str && strcasestr (str, "mms"))
    return TRUE;

  str = nma_mobile_access_method_get_name (method);
  if (str && strcasestr (str, "mms"))
    return TRUE;

  return FALSE;
}

static void
wwan_data_update_apn_list_db (CcWwanData *self)
{
  GSList *apn_methods = NULL, *l;
  g_autoptr(GError) error = NULL;
  guint i = 0;

  if (!self->sim || !self->operator_code)
    return;

  if (!self->apn_list)
    self->apn_list = g_list_store_new (CC_TYPE_WWAN_DATA_APN);

  if (!self->apn_db)
    self->apn_db = nma_mobile_providers_database_new_sync (NULL, NULL, NULL, &error);

  if (error)
    {
      g_warning ("%s", error->message);
      return;
    }

  if (!self->apn_provider)
    self->apn_provider = nma_mobile_providers_database_lookup_3gpp_mcc_mnc (self->apn_db,
                                                                            self->operator_code);

  if (self->apn_provider)
    apn_methods = nma_mobile_provider_get_methods (self->apn_provider);

  for (l = apn_methods; l; l = l->next, i++)
    {
      g_autoptr(CcWwanDataApn) apn = NULL;

      /* We don’t list MMS APNs */
      if (wwan_data_nma_method_is_mms (l->data))
        continue;

      apn = wwan_data_find_matching_apn (self, l->data);

      /* Prepend the item in order */
      if (!apn)
        {
          apn = cc_wwan_data_apn_new ();
          g_list_store_insert (self->apn_list, i, apn);
        }

      apn->access_method = l->data;
    }
}

static void
wwan_data_update_apn_list (CcWwanData *self)
{
  const GPtrArray *nm_connections;
  guint i;

  if (self->apn_list || !self->sim)
    return;

  if (!self->apn_list)
    self->apn_list = g_list_store_new (CC_TYPE_WWAN_DATA_APN);

  if (self->nm_device)
    {
      nm_connections = nm_device_get_available_connections (self->nm_device);

      for (i = 0; i < nm_connections->len; i++)
        {
          g_autoptr(CcWwanDataApn) apn = NULL;

          apn = cc_wwan_data_apn_new ();
          apn->remote_connection = g_object_ref (nm_connections->pdata[i]);
          g_list_store_append (self->apn_list, apn);

          /* Load the default APN */
          if (!self->default_apn && self->sim_id)
            {
              NMSettingConnection *connection_setting;
              NMSettingIPConfig *ip_setting;
              NMSettingGsm *setting;
              NMConnection *connection;
              const gchar *sim_id;

              connection = NM_CONNECTION (apn->remote_connection);
              setting = nm_connection_get_setting_gsm (connection);
              connection_setting = nm_connection_get_setting_connection (connection);
              sim_id = nm_setting_gsm_get_sim_id (setting);

              if (sim_id && *sim_id && g_str_equal (sim_id, self->sim_id))
                {
                  self->default_apn = apn;
                  self->home_only = nm_setting_gsm_get_home_only (setting);
                  self->data_enabled = nm_setting_connection_get_autoconnect (connection_setting);

                  /* If any of the APN has a high priority, the device have high priority */
                  ip_setting = nm_connection_get_setting_ip4_config (connection);
                  if (nm_setting_ip_config_get_route_metric (ip_setting) == CC_WWAN_ROUTE_PRIORITY_HIGH)
                    self->priority = CC_WWAN_APN_PRIORITY_HIGH;
                }
            }
        }
    }
}

static void
wwan_device_state_changed_cb (CcWwanData *self)
{
  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ENABLED]);
}

static void
cc_wwan_data_get_property (GObject    *object,
                           guint       prop_id,
                           GValue     *value,
                           GParamSpec *pspec)
{
  CcWwanData *self = (CcWwanData *)object;

  switch (prop_id)
    {
    case PROP_ERROR:
      g_value_set_boolean (value, self->error != NULL);
      break;

    case PROP_ENABLED:
      g_value_set_boolean (value, cc_wwan_data_get_enabled (self));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
cc_wwan_data_dispose (GObject *object)
{
  CcWwanData *self = (CcWwanData *)object;

  g_clear_pointer (&self->sim_id, g_free);
  g_clear_pointer (&self->operator_code, g_free);
  g_clear_error (&self->error);
  g_clear_object (&self->apn_list);
  g_clear_object (&self->modem);
  g_clear_object (&self->mm_object);
  g_clear_object (&self->nm_client);
  g_clear_object (&self->active_connection);
  g_clear_object (&self->apn_db);

  G_OBJECT_CLASS (cc_wwan_data_parent_class)->dispose (object);
}

static void
cc_wwan_data_class_init (CcWwanDataClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->get_property = cc_wwan_data_get_property;
  object_class->dispose = cc_wwan_data_dispose;

  properties[PROP_ERROR] =
    g_param_spec_boolean ("error",
                          "Error",
                          "Set if some Error occurs",
                          FALSE,
                          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

  properties[PROP_ENABLED] =
    g_param_spec_boolean ("enabled",
                          "Enabled",
                          "Get if the data is enabled",
                          FALSE,
                          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

  g_object_class_install_properties (object_class, N_PROPS, properties);
}

static void
cc_wwan_data_init (CcWwanData *self)
{
  self->home_only = TRUE;
  self->priority = CC_WWAN_APN_PRIORITY_LOW;
}

/**
 * cc_wwan_data_new:
 * @mm_object: An #MMObject
 * @nm_client: An #NMClient
 *
 * Create a new device data representing the given
 * @mm_object. If @mm_object isn’t a 3G/CDMA/LTE
 * modem, %NULL will be returned
 *
 * Returns: A #CcWwanData or %NULL.
 */
CcWwanData *
cc_wwan_data_new (MMObject *mm_object,
                  NMClient *nm_client)
{
  CcWwanData *self;
  NMDevice *nm_device = NULL;
  g_autoptr(MMModem) modem = NULL;
  NMDeviceModemCapabilities capabilities = 0;

  g_return_val_if_fail (MM_IS_OBJECT (mm_object), NULL);
  g_return_val_if_fail (NM_CLIENT (nm_client), NULL);

  modem = mm_object_get_modem (mm_object);

  if (modem)
    nm_device = nm_client_get_device_by_iface (nm_client,
                                               mm_modem_get_primary_port (modem));

  if (NM_IS_DEVICE_MODEM (nm_device))
    capabilities = nm_device_modem_get_current_capabilities (NM_DEVICE_MODEM (nm_device));

  if (!(capabilities & (NM_DEVICE_MODEM_CAPABILITY_GSM_UMTS
                        | NM_DEVICE_MODEM_CAPABILITY_LTE)))
    return NULL;

  self = g_object_new (CC_TYPE_WWAN_DATA, NULL);

  self->nm_client = g_object_ref (nm_client);
  self->mm_object = g_object_ref (mm_object);
  self->modem = g_steal_pointer (&modem);
  self->sim = mm_modem_get_sim_sync (self->modem, NULL, NULL);
  self->sim_id = mm_sim_dup_identifier (self->sim);
  self->operator_code = mm_sim_dup_operator_identifier (self->sim);
  self->nm_device = g_object_ref (nm_device);
  self->active_connection = nm_device_get_active_connection (nm_device);

  if (!self->operator_code)
    {
      MMModem3gpp *modem_3gpp;

      modem_3gpp = mm_object_peek_modem_3gpp (mm_object);
      if (modem_3gpp)
        self->operator_code = mm_modem_3gpp_dup_operator_code (modem_3gpp);
    }

  if (self->active_connection)
    g_object_ref (self->active_connection);

  g_signal_connect_object (self->nm_device, "notify::state",
                           G_CALLBACK (wwan_device_state_changed_cb),
                           self, G_CONNECT_SWAPPED);

  wwan_data_update_apn_list (self);
  wwan_data_update_apn_list_db (self);

  return self;
}

GError *
cc_wwan_data_get_error (CcWwanData *self)
{
  g_return_val_if_fail (CC_IS_WWAN_DATA (self), NULL);

  return self->error;
}

const gchar *
cc_wwan_data_get_simple_html_error (CcWwanData *self)
{
  g_return_val_if_fail (CC_IS_WWAN_DATA (self), NULL);

  if (!self->error)
    return NULL;

  if (g_error_matches (self->error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
    return _("Operation Cancelled");

  if (g_error_matches (self->error, G_DBUS_ERROR, G_DBUS_ERROR_ACCESS_DENIED))
    return _("<b>Error:</b> Access denied changing settings");

  if (self->error->domain == MM_MOBILE_EQUIPMENT_ERROR)
    return _("<b>Error:</b> Mobile Equipment Error");

  return NULL;
}

GListModel *
cc_wwan_data_get_apn_list (CcWwanData *self)
{
  g_return_val_if_fail (CC_IS_WWAN_DATA (self), NULL);

  if (!self->apn_list)
    wwan_data_update_apn_list (self);

  return G_LIST_MODEL (self->apn_list);
}

static gboolean
wwan_data_apn_is_new (CcWwanDataApn *apn)
{
  return apn->remote_connection == NULL;
}

static void
wwan_data_update_apn (CcWwanData    *self,
                      CcWwanDataApn *apn,
                      NMConnection  *connection)
{
  NMSetting *setting;
  const gchar *name, *username, *password, *apn_name;
  gint dns_priority, route_metric;

  setting = NM_SETTING (nm_connection_get_setting_connection (connection));

  g_object_set (setting,
                NM_SETTING_CONNECTION_AUTOCONNECT, self->data_enabled,
                NULL);

  setting = NM_SETTING (nm_connection_get_setting_gsm (connection));

  g_object_set (setting,
                NM_SETTING_GSM_HOME_ONLY, self->home_only,
                NULL);

  setting = NM_SETTING (nm_connection_get_setting_ip4_config (connection));
  if (self->priority == CC_WWAN_APN_PRIORITY_HIGH &&
      self->default_apn == apn)
    {
      dns_priority = CC_WWAN_DNS_PRIORITY_HIGH;
      route_metric = CC_WWAN_ROUTE_PRIORITY_HIGH;
    }
  else
    {
      dns_priority = CC_WWAN_DNS_PRIORITY_LOW;
      route_metric = CC_WWAN_ROUTE_PRIORITY_LOW;
    }

  g_object_set (setting,
                NM_SETTING_IP_CONFIG_DNS_PRIORITY, dns_priority,
                NM_SETTING_IP_CONFIG_ROUTE_METRIC, (gint64)route_metric,
                NULL);

  if (apn->access_method && !apn->remote_connection)
    {
      name = nma_mobile_access_method_get_name (apn->access_method);
      username = nma_mobile_access_method_get_username (apn->access_method);
      password = nma_mobile_access_method_get_password (apn->access_method);
      apn_name = nma_mobile_access_method_get_3gpp_apn (apn->access_method);
    }
  else
    {
      return;
    }

  setting = NM_SETTING (nm_connection_get_setting_gsm (connection));
  g_object_set (setting,
                NM_SETTING_GSM_USERNAME, username,
                NM_SETTING_GSM_PASSWORD, password,
                NM_SETTING_GSM_APN, apn_name,
                NULL);

  setting = NM_SETTING (nm_connection_get_setting_connection (connection));

  g_object_set (setting,
                NM_SETTING_CONNECTION_ID, name,
                NULL);
}

static gint
wwan_data_get_apn_index (CcWwanData    *self,
                         CcWwanDataApn *apn)
{
  GListModel *model;
  guint i, n_items;

  model = G_LIST_MODEL (self->apn_list);
  n_items = g_list_model_get_n_items (model);

  for (i = 0; i < n_items; i++)
    {
      g_autoptr(CcWwanDataApn) cached_apn = NULL;

      cached_apn = g_list_model_get_item (model, i);

      if (apn == cached_apn)
        return i;
    }

  return -1;
}

static void
cc_wwan_data_connection_updated_cb (GObject      *object,
                                    GAsyncResult *result,
                                    gpointer      user_data)
{
  CcWwanData *self;
  CcWwanDataApn *apn;
  g_autoptr(GTask) task = user_data;
  g_autoptr(GError) error = NULL;

  self = g_task_get_source_object (G_TASK (task));
  apn = g_task_get_task_data (G_TASK (task));

  nm_remote_connection_commit_changes_finish (apn->remote_connection,
                                              result, &error);
  if (!error)
    {
      guint apn_index;
      apn_index = wwan_data_get_apn_index (self, apn);

      if (apn_index >= 0)
        g_list_model_items_changed (G_LIST_MODEL (self->apn_list),
                                    apn_index, 1, 1);
      else
        g_warning ("APN ‘%s’ not in APN list",
                   cc_wwan_data_apn_get_name (apn));

      apn->modified = FALSE;
      g_task_return_boolean (task, TRUE);
    }
  else
    {
      g_task_return_error (task, g_steal_pointer (&error));
    }
}

static void
cc_wwan_data_new_connection_added_cb (GObject      *object,
                                      GAsyncResult *result,
                                      gpointer      user_data)
{
  CcWwanData *self;
  CcWwanDataApn *apn;
  g_autoptr(GTask) task = user_data;
  g_autoptr(GError) error = NULL;

  self = g_task_get_source_object (G_TASK (task));
  apn = g_task_get_task_data (G_TASK (task));
  apn->remote_connection = nm_client_add_connection_finish (self->nm_client,
                                                            result, &error);
  if (!error)
    {
      apn->modified = FALSE;

      /* If APN has access method, it’s already on the list */
      if (!apn->access_method)
        {
          g_list_store_append (self->apn_list, apn);
          g_object_unref (apn);
        }

      g_task_return_pointer (task, apn, NULL);
    }
  else
    {
      g_task_return_error (task, g_steal_pointer (&error));
    }
}

void
cc_wwan_data_save_apn (CcWwanData          *self,
                       CcWwanDataApn       *apn,
                       GCancellable        *cancellable,
                       GAsyncReadyCallback  callback,
                       gpointer             user_data)
{
  NMConnection *connection = NULL;
  g_autoptr(GTask) task = NULL;

  g_return_if_fail (CC_IS_WWAN_DATA (self));
  g_return_if_fail (CC_IS_WWAN_DATA_APN (apn));
  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));

  task = g_task_new (self, cancellable, callback, user_data);
  g_task_set_task_data (task, apn, NULL);

  connection = wwan_data_get_nm_connection (apn);

  /* If the item has a remote connection, it should already be saved.
   * We should save it again only if it got modified */
  if (apn->remote_connection && !apn->modified)
    {
      g_task_return_pointer (task, apn, NULL);
      return;
    }

  wwan_data_update_apn (self, apn, connection);
  if (wwan_data_apn_is_new (apn))
    {
      nm_client_add_connection_async (self->nm_client, apn->nm_connection,
                                      TRUE, cancellable,
                                      cc_wwan_data_new_connection_added_cb,
                                      g_steal_pointer (&task));
    }
  else
    {
      nm_remote_connection_commit_changes_async (apn->remote_connection, TRUE,
                                                 cancellable,
                                                 cc_wwan_data_connection_updated_cb,
                                                 g_steal_pointer (&task));
    }
}

CcWwanDataApn *
cc_wwan_data_save_apn_finish (CcWwanData    *self,
                              GAsyncResult  *result,
                              GError       **error)
{
  g_return_val_if_fail (CC_IS_WWAN_DATA (self), NULL);
  g_return_val_if_fail (G_IS_TASK (result), NULL);

  return g_task_propagate_pointer (G_TASK (result), error);
}

static void
cc_wwan_data_activated_cb (GObject      *object,
                           GAsyncResult *result,
                           gpointer      user_data)
{
  CcWwanData *self;
  NMActiveConnection *connection;
  g_autoptr(GTask) task = user_data;
  g_autoptr(GError) error = NULL;

  self = g_task_get_source_object (G_TASK (task));
  connection = nm_client_activate_connection_finish (self->nm_client,
                                                     result, &error);
  if (connection)
    {
      g_set_object (&self->active_connection, connection);
      g_task_return_boolean (task, TRUE);
    }
  else
    {
      g_task_return_error (task, g_steal_pointer (&error));
    }

  if (error)
    g_warning ("Error: %s", error->message);
}

static void
cc_wwan_data_disconnect_cb (GObject      *object,
                            GAsyncResult *result,
                            gpointer      user_data)
{
  CcWwanData *self;
  g_autoptr(GTask) task = user_data;
  g_autoptr(GError) error = NULL;

  self = g_task_get_source_object (G_TASK (task));
  if (nm_device_disconnect_finish (self->nm_device, result, &error))
    {
      g_clear_object (&self->active_connection);
      g_task_return_boolean (task, TRUE);
    }
  else
    {
      g_task_return_error (task, g_steal_pointer (&error));
    }

  if (error)
    g_warning ("Error: %s", error->message);
}

static void
cc_wwan_data_settings_saved_cb (GObject      *object,
                                GAsyncResult *result,
                                gpointer      user_data)
{
  CcWwanData *self;
  GCancellable *cancellable;
  g_autoptr(GTask) task = user_data;
  g_autoptr(GError) error = NULL;

  self = g_task_get_source_object (G_TASK (task));
  cancellable = g_task_get_cancellable (G_TASK (task));

  if (!cc_wwan_data_save_apn_finish (self, result, &error))
    {
      g_task_return_error (task, g_steal_pointer (&error));
      return;
    }

  self->default_apn->modified = FALSE;

  if (self->data_enabled)
    {
      nm_client_activate_connection_async (self->nm_client,
                                           NM_CONNECTION (self->default_apn->remote_connection),
                                           self->nm_device,
                                           NULL, cancellable,
                                           cc_wwan_data_activated_cb,
                                           g_steal_pointer (&task));
    }
  else
    {
      nm_device_disconnect_async (self->nm_device,
                                  cancellable,
                                  cc_wwan_data_disconnect_cb,
                                  g_steal_pointer (&task));
    }
}

/**
 * cc_wwan_data_save_settings:
 * @cancellable: (nullable): a #GCancellable or %NULL
 * @callback: a #GAsyncReadyCallback, or %NULL
 * @user_data: closure data for @callback
 *
 * Save default settings to disk and apply changes.
 * If the default APN has data enabled, the data is
 * activated after the settings are saved.
 *
 * It’s a programmer error to call this function without
 * a default APN set.
 * Finish with cc_wwan_data_save_settings_finish().
 */
void
cc_wwan_data_save_settings (CcWwanData          *self,
                            GCancellable        *cancellable,
                            GAsyncReadyCallback  callback,
                            gpointer             user_data)
{
  NMConnection *connection;
  NMSetting *setting;
  g_autoptr(GTask) task = NULL;

  g_return_if_fail (CC_IS_WWAN_DATA (self));
  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
  g_return_if_fail (self->default_apn != NULL);

  task = g_task_new (self, cancellable, callback, user_data);

  /* Reset old settings to default value */
  if (self->old_default_apn && self->old_default_apn->remote_connection)
    {
      connection = NM_CONNECTION (self->old_default_apn->remote_connection);

      setting = NM_SETTING (nm_connection_get_setting_gsm (connection));
      g_object_set (G_OBJECT (setting),
                    NM_SETTING_GSM_HOME_ONLY, TRUE,
                    NM_SETTING_GSM_SIM_ID, NULL,
                    NULL);

      setting = NM_SETTING (nm_connection_get_setting_ip4_config (connection));
      g_object_set (setting,
                    NM_SETTING_IP_CONFIG_DNS_PRIORITY, CC_WWAN_DNS_PRIORITY_LOW,
                    NM_SETTING_IP_CONFIG_ROUTE_METRIC, (gint64)CC_WWAN_ROUTE_PRIORITY_LOW,
                    NULL);

      setting = NM_SETTING (nm_connection_get_setting_connection (connection));
      g_object_set (G_OBJECT (setting),
                    NM_SETTING_CONNECTION_AUTOCONNECT, FALSE,
                    NULL);

      nm_remote_connection_commit_changes (NM_REMOTE_CONNECTION (connection),
                                           TRUE, cancellable, NULL);
      self->old_default_apn->modified = FALSE;
      self->old_default_apn = NULL;
    }

  self->default_apn->modified = TRUE;
  connection = wwan_data_get_nm_connection (self->default_apn);

  setting = NM_SETTING (nm_connection_get_setting_gsm (connection));
  g_object_set (G_OBJECT (setting),
                NM_SETTING_GSM_HOME_ONLY, self->home_only,
                NM_SETTING_GSM_SIM_ID, self->sim_id,
                NULL);

  cc_wwan_data_save_apn (self, self->default_apn, cancellable,
                         cc_wwan_data_settings_saved_cb,
                         g_steal_pointer (&task));
}

gboolean
cc_wwan_data_save_settings_finish (CcWwanData    *self,
                                   GAsyncResult  *result,
                                   GError       **error)
{
  g_return_val_if_fail (CC_IS_WWAN_DATA (self), FALSE);
  g_return_val_if_fail (G_IS_TASK (result), FALSE);

  return g_task_propagate_boolean (G_TASK (result), error);
}

gboolean
cc_wwan_data_delete_apn (CcWwanData     *self,
                         CcWwanDataApn  *apn,
                         GCancellable   *cancellable,
                         GError        **error)
{
  NMRemoteConnection *connection = NULL;
  gboolean ret = FALSE;
  gint apn_index;

  g_return_val_if_fail (CC_IS_WWAN_DATA (self), FALSE);
  g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE);
  g_return_val_if_fail (CC_IS_WWAN_DATA_APN (apn), FALSE);
  g_return_val_if_fail (error != NULL, FALSE);

  apn_index = wwan_data_get_apn_index (self, apn);
  if (apn_index == -1)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
                   "APN not found for the connection");
      return FALSE;
    }

  connection = g_steal_pointer (&apn->remote_connection);
  wwan_data_apn_reset (apn);

  if (connection)
    ret = nm_remote_connection_delete (connection, cancellable, error);

  if (!ret)
    {
      apn->remote_connection = connection;
      *error = g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED,
                            "Deleting APN from NetworkManager failed");
      return ret;
    }

  g_object_unref (connection);

  /* We remove the item only if it's not in the mobile provider database */
  if (!apn->access_method)
    {
      if (self->default_apn == apn)
        self->default_apn = NULL;

      g_list_store_remove (self->apn_list, apn_index);

      return TRUE;
    }

  *error = g_error_new (G_IO_ERROR, G_IO_ERROR_READ_ONLY,
                        "Deleting APN from NetworkManager failed");
  return FALSE;
}

CcWwanDataApn *
cc_wwan_data_get_default_apn (CcWwanData *self)
{
  g_return_val_if_fail (CC_IS_WWAN_DATA (self), NULL);

  return self->default_apn;
}

gboolean
cc_wwan_data_set_default_apn (CcWwanData    *self,
                              CcWwanDataApn *apn)
{
  NMConnection *connection;
  NMSetting *setting;

  g_return_val_if_fail (CC_IS_WWAN_DATA (self), FALSE);
  g_return_val_if_fail (CC_IS_WWAN_DATA_APN (apn), FALSE);

  if (self->default_apn == apn)
    return FALSE;

  /*
   * APNs are bound to the SIM, not the modem device.
   * This will let the APN work if the same SIM inserted
   * in a different device, and not enable data if a
   * different SIM is inserted to the modem.
   */
  apn->modified = TRUE;
  self->old_default_apn = self->default_apn;
  self->default_apn = apn;
  connection = wwan_data_get_nm_connection (apn);
  setting = NM_SETTING (nm_connection_get_setting_gsm (connection));

  if (self->sim_id)
    g_object_set (G_OBJECT (setting),
                  NM_SETTING_GSM_SIM_ID, self->sim_id, NULL);

  return TRUE;
}

gboolean
cc_wwan_data_get_enabled (CcWwanData *self)
{
  NMSettingConnection *setting;
  NMConnection *connection;
  NMDeviceState state;

  g_return_val_if_fail (CC_IS_WWAN_DATA (self), FALSE);

  state = nm_device_get_state (self->nm_device);

  if (state == NM_DEVICE_STATE_DISCONNECTED ||
      state == NM_DEVICE_STATE_DEACTIVATING)
    if (nm_device_get_state_reason (self->nm_device) == NM_DEVICE_STATE_REASON_USER_REQUESTED)
      return FALSE;

  if (nm_device_get_active_connection (self->nm_device) != NULL)
    return TRUE;

  if (!self->default_apn || !self->default_apn->remote_connection)
    return FALSE;

  connection = NM_CONNECTION (self->default_apn->remote_connection);
  setting = nm_connection_get_setting_connection (connection);

  return nm_setting_connection_get_autoconnect (setting);
}

/**
 * cc_wwan_data_set_enabled:
 * @self: A #CcWwanData
 * @enable_data: whether to enable data
 *
 * Enable data for the device.  The settings is
 * saved to disk only after a default APN is set.
 *
 * If the data is enabled, the device will automatically
 * turn data on everytime the same SIM is available.
 * The data set is bound to the SIM, not the modem device.
 *
 * Use @cc_wwan_data_save_apn() with the default APN
 * to save the changes and really enable/disable data.
 */
void
cc_wwan_data_set_enabled (CcWwanData *self,
                          gboolean    enable_data)
{
  g_return_if_fail (CC_IS_WWAN_DATA (self));

  self->data_enabled = !!enable_data;

  if (self->default_apn)
    self->default_apn->modified = TRUE;
}

gboolean
cc_wwan_data_get_roaming_enabled (CcWwanData *self)
{
  g_return_val_if_fail (CC_IS_WWAN_DATA (self), FALSE);

  if (!self->default_apn)
    return FALSE;

  return !self->home_only;
}

/**
 * cc_wwan_data_apn_set_roaming_enabled:
 * @self: A #CcWwanData
 * @enable_roaming: whether to enable roaming or not
 *
 * Enable roaming for the device.  The settings is
 * saved to disk only after a default APN is set.
 *
 * Use @cc_wwan_data_save_apn() with the default APN
 * to save the changes and really enable/disable data.
 */
void
cc_wwan_data_set_roaming_enabled (CcWwanData *self,
                                  gboolean    enable_roaming)
{
  g_return_if_fail (CC_IS_WWAN_DATA (self));

  self->home_only = !enable_roaming;

  if (self->default_apn)
    self->default_apn->modified = TRUE;
}

static void
cc_wwan_data_apn_finalize (GObject *object)
{
  CcWwanDataApn *apn = CC_WWAN_DATA_APN (object);

  wwan_data_apn_reset (apn);
  g_clear_pointer (&apn->access_method,
                   nma_mobile_access_method_unref);

  G_OBJECT_CLASS (cc_wwan_data_parent_class)->finalize (object);
}

static void
cc_wwan_data_apn_class_init (CcWwanDataApnClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->finalize = cc_wwan_data_apn_finalize;
}

static void
cc_wwan_data_apn_init (CcWwanDataApn *apn)
{
}

CcWwanDataApn *
cc_wwan_data_apn_new (void)
{
  return g_object_new (CC_TYPE_WWAN_DATA_APN, NULL);
}

/**
 * cc_wwan_data_apn_get_name:
 * @apn: A #CcWwanDataApn
 *
 * Get the Name of @apn
 *
 * Returns: (transfer none): The Name of @apn
 */
const gchar *
cc_wwan_data_apn_get_name (CcWwanDataApn *apn)
{
  g_return_val_if_fail (CC_IS_WWAN_DATA_APN (apn), "");

  if (apn->remote_connection)
    return nm_connection_get_id (NM_CONNECTION (apn->remote_connection));

  if (apn->access_method)
    return nma_mobile_access_method_get_name (apn->access_method);

  return "";
}

/**
 * cc_wwan_data_apn_set_name:
 * @apn: A #CcWwanDataApn
 * @name: The name to be given for APN, should not
 * be empty
 *
 * Set the name of @apn to be @name.
 *
 * @apn is only modified, use @cc_wwan_data_save_apn()
 * to save the changes.
 */
void
cc_wwan_data_apn_set_name (CcWwanDataApn *apn,
                           const gchar   *name)
{
  NMConnection *connection;
  NMSettingConnection *setting;

  g_return_if_fail (CC_IS_WWAN_DATA_APN (apn));
  g_return_if_fail (name != NULL);
  g_return_if_fail (*name != '\0');

  if (g_str_equal (cc_wwan_data_apn_get_name (apn), name))
    return;

  apn->modified = TRUE;
  connection = wwan_data_get_nm_connection (apn);
  setting = nm_connection_get_setting_connection (connection);
  g_object_set (G_OBJECT (setting),
                NM_SETTING_CONNECTION_ID, name,
                NULL);
}

/**
 * cc_wwan_data_apn_get_apn:
 * @apn: A #CcWwanDataApn
 *
 * Get the APN of @apn
 *
 * Returns: (transfer none): The APN of @apn
 */
const gchar *
cc_wwan_data_apn_get_apn (CcWwanDataApn *apn)
{
  const gchar *apn_name = "";

  g_return_val_if_fail (CC_IS_WWAN_DATA_APN (apn), "");

  if (apn->remote_connection)
    {
      NMSettingGsm *setting;

      setting = nm_connection_get_setting_gsm (NM_CONNECTION (apn->remote_connection));
      apn_name = nm_setting_gsm_get_apn (setting);
    }
  else if (apn->access_method)
    {
      apn_name = nma_mobile_access_method_get_3gpp_apn (apn->access_method);
    }

  return apn_name ? apn_name : "";
}

/**
 * cc_wwan_data_apn_set_apn:
 * @apn: A #CcWwanDataApn
 * @apn_name: The apn to be used, should not be
 * empty
 *
 * Set the APN of @apn to @apn_name. @apn_name is
 * usually a URL like “example.com” or a simple string
 * like “internet”
 *
 * @apn is only modified, use @cc_wwan_data_save_apn()
 * to save the changes.
 */
void
cc_wwan_data_apn_set_apn (CcWwanDataApn *apn,
                          const gchar   *apn_name)
{
  NMConnection *connection;
  NMSettingGsm *setting;

  g_return_if_fail (CC_IS_WWAN_DATA_APN (apn));
  g_return_if_fail (apn_name != NULL);
  g_return_if_fail (*apn_name != '\0');

  if (g_str_equal (cc_wwan_data_apn_get_apn (apn), apn_name))
    return;

  apn->modified = TRUE;
  connection = wwan_data_get_nm_connection (apn);
  setting = nm_connection_get_setting_gsm (connection);
  g_object_set (G_OBJECT (setting),
                NM_SETTING_GSM_APN, apn_name,
                NULL);
}

/**
 * cc_wwan_data_apn_get_username:
 * @apn: A #CcWwanDataApn
 *
 * Get the Username of @apn
 *
 * Returns: (transfer none): The Username of @apn
 */
const gchar *
cc_wwan_data_apn_get_username (CcWwanDataApn *apn)
{
  const gchar *username = "";

  g_return_val_if_fail (CC_IS_WWAN_DATA_APN (apn), "");

  if (apn->remote_connection)
    {
      NMSettingGsm *setting;

      setting = nm_connection_get_setting_gsm (NM_CONNECTION (apn->remote_connection));
      username = nm_setting_gsm_get_username (setting);
    }
  else if (apn->access_method)
    {
      username = nma_mobile_access_method_get_username (apn->access_method);
    }

  return username ? username : "";
}

/**
 * cc_wwan_data_apn_set_username:
 * @apn: A #CcWwanDataAPN
 * @username: The username to be used
 *
 * Set the Username of @apn to @username.
 *
 * @apn is only modified, use @cc_wwan_data_save_apn()
 * to save the changes.
 */
void
cc_wwan_data_apn_set_username (CcWwanDataApn *apn,
                               const gchar   *username)
{
  NMConnection *connection;
  NMSettingGsm *setting;

  g_return_if_fail (CC_IS_WWAN_DATA_APN (apn));

  if (username && !*username)
    username = NULL;

  if (g_strcmp0 (cc_wwan_data_apn_get_username (apn), username) == 0)
    return;

  apn->modified = TRUE;
  connection = wwan_data_get_nm_connection (apn);
  setting = nm_connection_get_setting_gsm (connection);
  g_object_set (G_OBJECT (setting),
                NM_SETTING_GSM_USERNAME, username,
                NULL);
}

/**
 * cc_wwan_data_apn_get_password:
 * @apn: A #CcWwanDataApn
 *
 * Get the Password of @apn
 *
 * Returns: (transfer none): The Password of @apn
 */
const gchar *
cc_wwan_data_apn_get_password (CcWwanDataApn *apn)
{
  const gchar *password = "";

  g_return_val_if_fail (CC_IS_WWAN_DATA_APN (apn), "");

  if (NM_IS_REMOTE_CONNECTION (apn->remote_connection))
    {
      g_autoptr(GVariant) secrets = NULL;
      g_autoptr(GError) error = NULL;

      secrets = nm_remote_connection_get_secrets (NM_REMOTE_CONNECTION (apn->remote_connection),
                                                  "gsm", NULL, &error);

      if (!error)
        nm_connection_update_secrets (NM_CONNECTION (apn->remote_connection),
                                      "gsm", secrets, &error);

      if (error)
        {
          g_warning ("Error: %s", error->message);
          return "";
        }
    }

  if (apn->remote_connection)
    {
      NMSettingGsm *setting;

      setting = nm_connection_get_setting_gsm (NM_CONNECTION (apn->remote_connection));
      password = nm_setting_gsm_get_password (setting);
    }
  else if (apn->access_method)
    {
      password = nma_mobile_access_method_get_password (apn->access_method);
    }

  return password ? password : "";
}

/**
 * cc_wwan_data_apn_set_password:
 * @apn: A #CcWwanDataApn
 * @password: The password to be used
 *
 * Set the Password of @apn to @password.
 *
 * @apn is only modified, use @cc_wwan_data_save_apn()
 * to save the changes.
 */
void
cc_wwan_data_apn_set_password (CcWwanDataApn *apn,
                               const gchar   *password)
{
  NMConnection *connection;
  NMSettingGsm *setting;

  g_return_if_fail (CC_IS_WWAN_DATA_APN (apn));

  if (password && !*password)
    password = NULL;

  if (g_strcmp0 (cc_wwan_data_apn_get_password (apn), password) == 0)
    return;

  apn->modified = TRUE;
  connection = wwan_data_get_nm_connection (apn);
  setting = nm_connection_get_setting_gsm (connection);
  g_object_set (G_OBJECT (setting),
                NM_SETTING_GSM_PASSWORD, password,
                NULL);
}

gint
cc_wwan_data_get_priority (CcWwanData *self)
{
  CcWwanDataApn *apn;
  NMSettingIPConfig *setting;

  g_return_val_if_fail (CC_IS_WWAN_DATA (self),
                        CC_WWAN_APN_PRIORITY_LOW);

  apn = self->default_apn;

  if (!apn || !apn->remote_connection)
    return CC_WWAN_APN_PRIORITY_LOW;

  setting = nm_connection_get_setting_ip4_config (NM_CONNECTION (apn->remote_connection));

  /* Lower the number, higher the priority */
  if (nm_setting_ip_config_get_route_metric (setting) <= CC_WWAN_ROUTE_PRIORITY_HIGH)
    return CC_WWAN_APN_PRIORITY_HIGH;
  else
    return CC_WWAN_APN_PRIORITY_LOW;
}

void
cc_wwan_data_set_priority (CcWwanData *self,
                           int         priority)
{
  g_return_if_fail (CC_IS_WWAN_DATA (self));
  g_return_if_fail (priority == CC_WWAN_APN_PRIORITY_LOW ||
                    priority == CC_WWAN_APN_PRIORITY_HIGH);

  if (self->priority == priority)
    return;

  self->priority = priority;

  if (self->default_apn)
    self->default_apn->modified = TRUE;
}
