/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* cc-wwan-device.c
 *
 * Copyright 2019-2020 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-device"

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include <glib/gi18n.h>
#include <polkit/polkit.h>
#if defined(HAVE_NETWORK_MANAGER) && defined(BUILD_NETWORK)
# include <NetworkManager.h>
# include <nma-mobile-providers.h>
#endif

#include "cc-wwan-errors-private.h"
#include "cc-wwan-device.h"

/**
 * @short_description: Device Object
 * @include: "cc-wwan-device.h"
 */

struct _CcWwanDevice
{
  GObject      parent_instance;

  MMObject    *mm_object;
  MMModem     *modem;
  MMSim       *sim;
  MMModem3gpp *modem_3gpp;

  const char  *operator_code; /* MCCMNC */
  GError      *error;

  /* Building with NetworkManager is optional,
   * so #NMclient type can’t be used here.
   */
  GObject      *nm_client; /* An #NMClient */
  CcWwanData   *wwan_data;

  gulong      modem_3gpp_id;
  gulong      modem_3gpp_locks_id;

  /* Enabled locks like PIN, PIN2, PUK, etc. */
  MMModem3gppFacility locks;

  CcWwanState  registration_state;
  gboolean     network_is_manual;
};

G_DEFINE_TYPE (CcWwanDevice, cc_wwan_device, G_TYPE_OBJECT)


enum {
  PROP_0,
  PROP_OPERATOR_NAME,
  PROP_ENABLED_LOCKS,
  PROP_ERROR,
  PROP_HAS_DATA,
  PROP_NETWORK_MODE,
  PROP_REGISTRATION_STATE,
  PROP_SIGNAL,
  PROP_UNLOCK_REQUIRED,
  N_PROPS
};

static GParamSpec *properties[N_PROPS];

static void
cc_wwan_device_state_changed_cb (CcWwanDevice *self)
{
  MMModem3gppRegistrationState state;

  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_OPERATOR_NAME]);

  state = mm_modem_3gpp_get_registration_state (self->modem_3gpp);

  switch (state)
    {
    case MM_MODEM_3GPP_REGISTRATION_STATE_UNKNOWN:
      self->registration_state = CC_WWAN_REGISTRATION_STATE_UNKNOWN;
      break;

    case MM_MODEM_3GPP_REGISTRATION_STATE_DENIED:
      self->registration_state = CC_WWAN_REGISTRATION_STATE_DENIED;
      break;

    case MM_MODEM_3GPP_REGISTRATION_STATE_IDLE:
      self->registration_state = CC_WWAN_REGISTRATION_STATE_IDLE;
      break;

    case MM_MODEM_3GPP_REGISTRATION_STATE_SEARCHING:
      self->registration_state = CC_WWAN_REGISTRATION_STATE_SEARCHING;
      break;

    case MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING:
      self->registration_state = CC_WWAN_REGISTRATION_STATE_ROAMING;
      break;

    default:
      self->registration_state = CC_WWAN_REGISTRATION_STATE_REGISTERED;
      break;
    }

  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_REGISTRATION_STATE]);
}

static void
cc_wwan_device_locks_changed_cb (CcWwanDevice *self)
{
  self->locks = mm_modem_3gpp_get_enabled_facility_locks (self->modem_3gpp);

  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ENABLED_LOCKS]);
}

static void
cc_wwan_device_3gpp_changed_cb (CcWwanDevice *self)
{
  gulong handler_id = 0;

  if (self->modem_3gpp_id)
    g_signal_handler_disconnect (self->modem_3gpp, self->modem_3gpp_id);
  self->modem_3gpp_id = 0;

  if (self->modem_3gpp_locks_id)
    g_signal_handler_disconnect (self->modem_3gpp, self->modem_3gpp_locks_id);
  self->modem_3gpp_locks_id = 0;

  g_clear_object (&self->modem_3gpp);
  self->modem_3gpp = mm_object_get_modem_3gpp (self->mm_object);

  if (self->modem_3gpp)
    {
      handler_id = g_signal_connect_object (self->modem_3gpp, "notify::registration-state",
                                            G_CALLBACK (cc_wwan_device_state_changed_cb),
                                            self, G_CONNECT_SWAPPED);
      self->modem_3gpp_id = handler_id;

      handler_id = g_signal_connect_object (self->modem_3gpp, "notify::enabled-facility-locks",
                                            G_CALLBACK (cc_wwan_device_locks_changed_cb),
                                            self, G_CONNECT_SWAPPED);
      self->modem_3gpp_locks_id = handler_id;
      cc_wwan_device_locks_changed_cb (self);
      cc_wwan_device_state_changed_cb (self);
    }
}

static void
cc_wwan_device_signal_quality_changed_cb (CcWwanDevice *self)
{
  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SIGNAL]);
}

static void
cc_wwan_device_mode_changed_cb (CcWwanDevice *self)
{
  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_NETWORK_MODE]);
}

static void
cc_wwan_device_emit_data_changed (CcWwanDevice *self)
{
  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_HAS_DATA]);
}

static void
cc_wwan_device_unlock_required_cb (CcWwanDevice *self)
{
  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_UNLOCK_REQUIRED]);
}

#if defined(HAVE_NETWORK_MANAGER) && defined(BUILD_NETWORK)
static void
cc_wwan_device_nm_changed_cb (CcWwanDevice *self,
                              GParamSpec   *pspec,
                              NMClient     *client)
{
  gboolean nm_is_running;

  nm_is_running = nm_client_get_nm_running (client);

  if (!nm_is_running && self->wwan_data != NULL)
    {
      g_clear_object (&self->wwan_data);
      cc_wwan_device_emit_data_changed (self);
    }
}

static void
cc_wwan_device_nm_device_added_cb (CcWwanDevice *self,
                                   NMDevice     *nm_device)
{
  if (!NM_IS_DEVICE_MODEM (nm_device))
    return;

  if(!self->sim || !cc_wwan_device_is_nm_device (self, G_OBJECT (nm_device)))
    return;

  self->wwan_data = cc_wwan_data_new (self->mm_object,
                                      NM_CLIENT (self->nm_client));

  if (self->wwan_data)
    {
      g_signal_connect_object (self->wwan_data, "notify::enabled",
                               G_CALLBACK (cc_wwan_device_emit_data_changed),
                               self, G_CONNECT_SWAPPED);
      cc_wwan_device_emit_data_changed (self);
    }
}
#endif

static void
cc_wwan_device_get_property (GObject    *object,
                             guint       prop_id,
                             GValue     *value,
                             GParamSpec *pspec)
{
  CcWwanDevice *self = (CcWwanDevice *)object;
  MMModemMode allowed, preferred;

  switch (prop_id)
    {
    case PROP_OPERATOR_NAME:
      g_value_set_string (value, cc_wwan_device_get_operator_name (self));
      break;

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

    case PROP_HAS_DATA:
      g_value_set_boolean (value, self->wwan_data != NULL);
      break;

    case PROP_ENABLED_LOCKS:
      g_value_set_int (value, self->locks);
      break;

    case PROP_NETWORK_MODE:
      if (cc_wwan_device_get_current_mode (self, &allowed, &preferred))
        g_value_take_string (value, cc_wwan_device_get_string_from_mode (self, allowed, preferred));
      break;

    case PROP_REGISTRATION_STATE:
      g_value_set_int (value, self->registration_state);
      break;

    case PROP_UNLOCK_REQUIRED:
      g_value_set_int (value, cc_wwan_device_get_lock (self));
      break;

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

static void
cc_wwan_device_dispose (GObject *object)
{
  CcWwanDevice *self = (CcWwanDevice *)object;

  g_clear_error (&self->error);
  g_clear_object (&self->modem);
  g_clear_object (&self->mm_object);
  g_clear_object (&self->sim);
  g_clear_object (&self->modem_3gpp);

  g_clear_object (&self->nm_client);
  g_clear_object (&self->wwan_data);

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

static void
cc_wwan_device_class_init (CcWwanDeviceClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->get_property = cc_wwan_device_get_property;
  object_class->dispose = cc_wwan_device_dispose;

  properties[PROP_OPERATOR_NAME] =
    g_param_spec_string ("operator-name",
                         "Operator Name",
                         "Operator Name the device is connected to",
                         NULL,
                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

  properties[PROP_ENABLED_LOCKS] =
    g_param_spec_int ("enabled-locks",
                      "Enabled Locks",
                      "Locks Enabled in Modem",
                      MM_MODEM_3GPP_FACILITY_NONE,
                      MM_MODEM_3GPP_FACILITY_CORP_PERS,
                      MM_MODEM_3GPP_FACILITY_NONE,
                      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

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

  properties[PROP_HAS_DATA] =
    g_param_spec_boolean ("has-data",
                          "has-data",
                          "Data for the device",
                          FALSE,
                          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

  properties[PROP_NETWORK_MODE] =
    g_param_spec_string ("network-mode",
                         "Network Mode",
                         "A String representing preferred network mode",
                         NULL,
                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

  properties[PROP_REGISTRATION_STATE] =
    g_param_spec_int ("registration-state",
                      "Registration State",
                      "The current network registration state",
                      CC_WWAN_REGISTRATION_STATE_UNKNOWN,
                      CC_WWAN_REGISTRATION_STATE_DENIED,
                      CC_WWAN_REGISTRATION_STATE_UNKNOWN,
                      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

  properties[PROP_UNLOCK_REQUIRED] =
    g_param_spec_int ("unlock-required",
                      "Unlock Required",
                      "The Modem lock status changed",
                      MM_MODEM_LOCK_UNKNOWN,
                      MM_MODEM_LOCK_PH_NETSUB_PUK,
                      MM_MODEM_LOCK_UNKNOWN,
                      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

  properties[PROP_SIGNAL] =
    g_param_spec_int ("signal",
                      "Signal",
                      "Get Device Signal",
                      0, 100, 0,
                      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

  g_object_class_install_properties (object_class, N_PROPS, properties);
}

static void
cc_wwan_device_init (CcWwanDevice *self)
{
}

/**
 * cc_wwan_device_new:
 * @mm_object: (transfer full): An #MMObject
 *
 * Create a new device representing the given
 * @mm_object.
 *
 * Returns: A #CcWwanDevice
 */
CcWwanDevice *
cc_wwan_device_new (MMObject *mm_object,
                    GObject  *nm_client)
{
  CcWwanDevice *self;

  g_return_val_if_fail (MM_IS_OBJECT (mm_object), NULL);
#if defined(HAVE_NETWORK_MANAGER) && defined(BUILD_NETWORK)
  g_return_val_if_fail (NM_IS_CLIENT (nm_client), NULL);
#else
  g_return_val_if_fail (!nm_client, NULL);
#endif

  self = g_object_new (CC_TYPE_WWAN_DEVICE, NULL);

  self->mm_object = g_object_ref (mm_object);
  self->modem = mm_object_get_modem (mm_object);
  self->sim = mm_modem_get_sim_sync (self->modem, NULL, NULL);
  g_set_object (&self->nm_client, nm_client);
  if (self->sim)
    {
      self->operator_code = mm_sim_get_operator_identifier (self->sim);
#if defined(HAVE_NETWORK_MANAGER) && defined(BUILD_NETWORK)
      self->wwan_data = cc_wwan_data_new (mm_object,
                                          NM_CLIENT (self->nm_client));
#endif
    }

  g_signal_connect_object (self->mm_object, "notify::unlock-required",
                           G_CALLBACK (cc_wwan_device_unlock_required_cb),
                           self, G_CONNECT_SWAPPED);
  if (self->wwan_data)
    g_signal_connect_object (self->wwan_data, "notify::enabled",
                             G_CALLBACK (cc_wwan_device_emit_data_changed),
                             self, G_CONNECT_SWAPPED);

#if defined(HAVE_NETWORK_MANAGER) && defined(BUILD_NETWORK)
  g_signal_connect_object (self->nm_client, "notify::nm-running" ,
                           G_CALLBACK (cc_wwan_device_nm_changed_cb), self,
                           G_CONNECT_SWAPPED);

  g_signal_connect_object (self->nm_client, "device-added",
                           G_CALLBACK (cc_wwan_device_nm_device_added_cb),
                           self, G_CONNECT_SWAPPED);
#endif

  g_signal_connect_object (self->mm_object, "notify::modem3gpp",
                           G_CALLBACK (cc_wwan_device_3gpp_changed_cb),
                           self, G_CONNECT_SWAPPED);
  g_signal_connect_object (self->modem, "notify::signal-quality",
                           G_CALLBACK (cc_wwan_device_signal_quality_changed_cb),
                           self, G_CONNECT_SWAPPED);

  cc_wwan_device_3gpp_changed_cb (self);
  g_signal_connect_object (self->modem, "notify::current-modes",
                           G_CALLBACK (cc_wwan_device_mode_changed_cb),
                           self, G_CONNECT_SWAPPED);

  return self;
}

gboolean
cc_wwan_device_has_sim (CcWwanDevice *self)
{
  MMModemStateFailedReason state_reason;

  g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), FALSE);

  state_reason = mm_modem_get_state_failed_reason (self->modem);

  if (state_reason == MM_MODEM_STATE_FAILED_REASON_SIM_MISSING)
    return FALSE;

  return TRUE;
}

/**
 * cc_wwan_device_get_lock:
 * @self: a #CcWwanDevice
 *
 * Get the active device lock that is required to
 * be unlocked for accessing device features.
 *
 * Returns: %TRUE if PIN enabled, %FALSE otherwise.
 */
MMModemLock
cc_wwan_device_get_lock (CcWwanDevice *self)
{
  g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), MM_MODEM_LOCK_UNKNOWN);

  return mm_modem_get_unlock_required (self->modem);
}


/**
 * cc_wwan_device_get_sim_lock:
 * @self: a #CcWwanDevice
 *
 * Get if SIM lock with PIN is enabled.  SIM PIN
 * enabled doesn’t mean that SIM is locked.
 * See cc_wwan_device_get_lock().
 *
 * Returns: %TRUE if PIN enabled, %FALSE otherwise.
 */
gboolean
cc_wwan_device_get_sim_lock (CcWwanDevice *self)
{
  gboolean sim_lock;

  g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), FALSE);

  sim_lock = self->locks & MM_MODEM_3GPP_FACILITY_SIM;

  return !!sim_lock;
}

guint
cc_wwan_device_get_unlock_retries (CcWwanDevice *self,
                                   MMModemLock   lock)
{
  MMUnlockRetries *retries;

  g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), 0);

  retries = mm_modem_peek_unlock_retries (self->modem);

  return mm_unlock_retries_get (retries, lock);
}

static void
cc_wwan_device_pin_sent_cb (GObject      *object,
                            GAsyncResult *result,
                            gpointer      user_data)
{
  CcWwanDevice *self;
  MMSim *sim = (MMSim *)object;
  g_autoptr(GTask) task = user_data;
  g_autoptr(GError) error = NULL;

  if (!mm_sim_send_pin_finish (sim, result, &error))
    {
      self = g_task_get_source_object (G_TASK (task));

      g_clear_error (&self->error);
      self->error = g_error_copy (error);
      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ERROR]);

      g_task_return_error (task, g_steal_pointer (&error));
    }
  else
    g_task_return_boolean (task, TRUE);
}

void
cc_wwan_device_send_pin (CcWwanDevice        *self,
                         const gchar         *pin,
                         GCancellable        *cancellable,
                         GAsyncReadyCallback  callback,
                         gpointer             user_data)
{
  g_autoptr(GTask) task = NULL;

  g_return_if_fail (CC_IS_WWAN_DEVICE (self));
  g_return_if_fail (MM_IS_SIM (self->sim));
  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
  g_return_if_fail (pin && *pin);

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

  mm_sim_send_pin (self->sim, pin, cancellable,
                   cc_wwan_device_pin_sent_cb,
                   g_steal_pointer (&task));
}

gboolean
cc_wwan_device_send_pin_finish (CcWwanDevice  *self,
                                GAsyncResult  *result,
                                GError       **error)
{
  g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), FALSE);
  g_return_val_if_fail (G_IS_TASK (result), FALSE);

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

static void
cc_wwan_device_puk_sent_cb (GObject      *object,
                            GAsyncResult *result,
                            gpointer      user_data)
{
  CcWwanDevice *self;
  MMSim *sim = (MMSim *)object;
  g_autoptr(GTask) task = user_data;
  g_autoptr(GError) error = NULL;

  if (!mm_sim_send_puk_finish (sim, result, &error))
    {
      self = g_task_get_source_object (G_TASK (task));

      g_clear_error (&self->error);
      self->error = g_error_copy (error);
      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ERROR]);

      g_task_return_error (task, g_steal_pointer (&error));
    }
  else
    g_task_return_boolean (task, TRUE);
}

void
cc_wwan_device_send_puk (CcWwanDevice        *self,
                         const gchar         *puk,
                         const gchar         *pin,
                         GCancellable        *cancellable,
                         GAsyncReadyCallback  callback,
                         gpointer             user_data)
{
  g_autoptr(GTask) task = NULL;

  g_return_if_fail (CC_IS_WWAN_DEVICE (self));
  g_return_if_fail (MM_IS_SIM (self->sim));
  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
  g_return_if_fail (puk && *puk);
  g_return_if_fail (pin && *pin);

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

  mm_sim_send_puk (self->sim, puk, pin, cancellable,
                   cc_wwan_device_puk_sent_cb,
                   g_steal_pointer (&task));
}

gboolean
cc_wwan_device_send_puk_finish (CcWwanDevice  *self,
                                GAsyncResult  *result,
                                GError       **error)
{
  g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), FALSE);
  g_return_val_if_fail (G_IS_TASK (result), FALSE);

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

static void
cc_wwan_device_enable_pin_cb (GObject      *object,
                              GAsyncResult *result,
                              gpointer      user_data)
{
  CcWwanDevice *self;
  MMSim *sim = (MMSim *)object;
  g_autoptr(GTask) task = user_data;
  g_autoptr(GError) error = NULL;

  if (!mm_sim_enable_pin_finish (sim, result, &error))
    {
      self = g_task_get_source_object (G_TASK (task));

      g_clear_error (&self->error);
      self->error = g_error_copy (error);
      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ERROR]);

      g_task_return_error (task, g_steal_pointer (&error));
    }
  else
    g_task_return_boolean (task, TRUE);
}

void
cc_wwan_device_enable_pin (CcWwanDevice        *self,
                           const gchar         *pin,
                           GCancellable        *cancellable,
                           GAsyncReadyCallback  callback,
                           gpointer             user_data)
{
  g_autoptr(GTask) task = NULL;

  g_return_if_fail (CC_IS_WWAN_DEVICE (self));
  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
  g_return_if_fail (pin && *pin);

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

  mm_sim_enable_pin (self->sim, pin, cancellable,
                     cc_wwan_device_enable_pin_cb,
                     g_steal_pointer (&task));
}

gboolean
cc_wwan_device_enable_pin_finish (CcWwanDevice  *self,
                                  GAsyncResult  *result,
                                  GError       **error)
{
  g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), FALSE);
  g_return_val_if_fail (G_IS_TASK (result), FALSE);

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

static void
cc_wwan_device_disable_pin_cb (GObject      *object,
                               GAsyncResult *result,
                               gpointer      user_data)
{
  CcWwanDevice *self;
  MMSim *sim = (MMSim *)object;
  g_autoptr(GTask) task = user_data;
  g_autoptr(GError) error = NULL;

  if (!mm_sim_disable_pin_finish (sim, result, &error))
    {
      self = g_task_get_source_object (G_TASK (task));

      g_clear_error (&self->error);
      self->error = g_error_copy (error);
      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ERROR]);

      g_task_return_error (task, g_steal_pointer (&error));
    }
  else
    g_task_return_boolean (task, TRUE);
}

void
cc_wwan_device_disable_pin (CcWwanDevice        *self,
                            const gchar         *pin,
                            GCancellable        *cancellable,
                            GAsyncReadyCallback  callback,
                            gpointer             user_data)
{
  g_autoptr(GTask) task = NULL;

  g_return_if_fail (CC_IS_WWAN_DEVICE (self));
  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
  g_return_if_fail (pin && *pin);

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

  mm_sim_disable_pin (self->sim, pin, cancellable,
                      cc_wwan_device_disable_pin_cb,
                      g_steal_pointer (&task));
}

gboolean
cc_wwan_device_disable_pin_finish (CcWwanDevice *self,
                                   GAsyncResult *result,
                                   GError       **error)
{
  g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), FALSE);
  g_return_val_if_fail (G_IS_TASK (result), FALSE);

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

static void
cc_wwan_device_change_pin_cb (GObject      *object,
                              GAsyncResult *result,
                              gpointer      user_data)
{
  CcWwanDevice *self;
  MMSim *sim = (MMSim *)object;
  g_autoptr(GTask) task = user_data;
  g_autoptr(GError) error = NULL;

  if (!mm_sim_change_pin_finish (sim, result, &error))
    {
      self = g_task_get_source_object (G_TASK (task));

      g_clear_error (&self->error);
      self->error = g_error_copy (error);
      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ERROR]);

      g_task_return_error (task, g_steal_pointer (&error));
    }
  else
    g_task_return_boolean (task, TRUE);
}

void
cc_wwan_device_change_pin (CcWwanDevice        *self,
                           const gchar         *old_pin,
                           const gchar         *new_pin,
                           GCancellable        *cancellable,
                           GAsyncReadyCallback  callback,
                           gpointer             user_data)
{
  g_autoptr(GTask) task = NULL;

  g_return_if_fail (CC_IS_WWAN_DEVICE (self));
  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
  g_return_if_fail (old_pin && *old_pin);
  g_return_if_fail (new_pin && *new_pin);

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

  mm_sim_change_pin (self->sim, old_pin, new_pin, cancellable,
                     cc_wwan_device_change_pin_cb,
                     g_steal_pointer (&task));
}

gboolean
cc_wwan_device_change_pin_finish (CcWwanDevice  *self,
                                  GAsyncResult  *result,
                                  GError       **error)
{
  g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), FALSE);
  g_return_val_if_fail (G_IS_TASK (result), FALSE);

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

static void
cc_wwan_device_network_mode_set_cb (GObject      *object,
                                    GAsyncResult *result,
                                    gpointer      user_data)
{
  CcWwanDevice *self;
  MMModem *modem = (MMModem *)object;
  g_autoptr(GTask) task = user_data;
  g_autoptr(GError) error = NULL;

  if (!mm_modem_set_current_modes_finish (modem, result, &error))
    {
      self = g_task_get_source_object (G_TASK (task));

      g_clear_error (&self->error);
      self->error = g_error_copy (error);
      g_warning ("Error: %s", error->message);
      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ERROR]);

      g_task_return_error (task, g_steal_pointer (&error));
    }
  else
    g_task_return_boolean (task, TRUE);
}

/**
 * cc_wwan_device_set_network_mode:
 * @self: a #CcWwanDevice
 * @allowed: The allowed #MMModemModes
 * @preferred: The preferred #MMModemMode
 * @cancellable: (nullable): a #GCancellable or %NULL
 * @callback: (nullable): a #GAsyncReadyCallback or %NULL
 * @user_data: (nullable): closure data for @callback
 *
 * Asynchronously set preferred network mode.
 *
 * Call @cc_wwan_device_set_current_mode_finish()
 * in @callback to get the result of operation.
 */
void
cc_wwan_device_set_current_mode (CcWwanDevice        *self,
                                 MMModemMode          allowed,
                                 MMModemMode          preferred,
                                 GCancellable        *cancellable,
                                 GAsyncReadyCallback  callback,
                                 gpointer             user_data)
{
  g_autoptr(GTask) task = NULL;
  GPermission *permission;
  g_autoptr(GError) error = NULL;

  g_return_if_fail (CC_IS_WWAN_DEVICE (self));
  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));

  task = g_task_new (self, cancellable, callback, user_data);
  permission = polkit_permission_new_sync ("org.freedesktop.ModemManager1.Device.Control",
                                           NULL, cancellable, &error);
  g_task_set_task_data (task, permission, g_object_unref);

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

  if (error)
    g_task_return_error (task, g_steal_pointer (&error));
  else if (!g_permission_get_allowed (permission))
    {
      error = g_error_new (G_IO_ERROR,
                           G_IO_ERROR_PERMISSION_DENIED,
                           "Access Denied");
      g_clear_error (&self->error);
      self->error = g_error_copy (error);
      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ERROR]);

      g_task_return_error (task, g_steal_pointer (&error));
    }
  else
    mm_modem_set_current_modes (self->modem, allowed, preferred,
                                cancellable, cc_wwan_device_network_mode_set_cb,
                                g_steal_pointer (&task));
}

/**
 * cc_wwan_device_set_current_mode_finish:
 * @self: a #CcWwanDevice
 * @result: a #GAsyncResult
 * @error: a location for #GError or %NULL
 *
 * Get the status whether setting network mode
 * succeeded
 *
 * Returns: %TRUE if network mode was successfully set,
 * %FALSE otherwise.
 */
gboolean
cc_wwan_device_set_current_mode_finish (CcWwanDevice  *self,
                                        GAsyncResult  *result,
                                        GError       **error)
{
  g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), FALSE);
  g_return_val_if_fail (G_IS_TASK (result), FALSE);

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

gboolean
cc_wwan_device_get_current_mode (CcWwanDevice *self,
                                 MMModemMode  *allowed,
                                 MMModemMode  *preferred)
{
  g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), FALSE);

  return mm_modem_get_current_modes (self->modem, allowed, preferred);
}

gboolean
cc_wwan_device_is_auto_network (CcWwanDevice *self)
{
  /*
   * XXX: ModemManager Doesn’t have a true API to check
   * if registration is automatic or manual.  So Let’s
   * do some guess work.
   */
  if (self->registration_state == CC_WWAN_REGISTRATION_STATE_DENIED)
    return FALSE;

  return !self->network_is_manual;
}

CcWwanState
cc_wwan_device_get_network_state (CcWwanDevice *self)
{
  g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), 0);

  return self->registration_state;
}

gboolean
cc_wwan_device_get_supported_modes (CcWwanDevice *self,
                                    MMModemMode  *allowed,
                                    MMModemMode  *preferred)
{
  g_autofree MMModemModeCombination *modes = NULL;
  guint n_modes, i;

  g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), FALSE);

  if (!mm_modem_get_supported_modes (self->modem, &modes, &n_modes))
    return FALSE;

  if (allowed)
    *allowed = 0;
  if (preferred)
    *preferred = 0;

  for (i = 0; i < n_modes; i++)
    {
      if (allowed)
        *allowed = *allowed | modes[i].allowed;
      if (preferred)
        *preferred = *preferred | modes[i].preferred;
    }

  return TRUE;
}

#define APPEND_MODE_TO_STRING(_str, _now, _preferred, _mode_str) do { \
    if (_str->len > 0)                                                \
      g_string_append (_str, ", ");                                   \
    g_string_append (_str, _mode_str);                                \
    if (_preferred == _now)                                           \
      g_string_append (_str, _(" (Preferred)"));                      \
  } while (0)

gchar *
cc_wwan_device_get_string_from_mode (CcWwanDevice *self,
                                     MMModemMode   allowed,
                                     MMModemMode   preferred)
{
  GString *str;

  g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), NULL);
  g_return_val_if_fail (allowed != 0, NULL);

  if (allowed == MM_MODEM_MODE_2G)
    return g_strdup (_("2G Only"));
  if (allowed == MM_MODEM_MODE_3G)
    return g_strdup (_("3G Only"));
  if (allowed == MM_MODEM_MODE_4G)
    return g_strdup (_("4G Only"));

  str = g_string_sized_new (10);

  if (allowed & MM_MODEM_MODE_2G)
    APPEND_MODE_TO_STRING (str, MM_MODEM_MODE_2G, preferred, "2G");
  if (allowed & MM_MODEM_MODE_3G)
    APPEND_MODE_TO_STRING (str, MM_MODEM_MODE_3G, preferred, "3G");
  if (allowed & MM_MODEM_MODE_4G)
    APPEND_MODE_TO_STRING (str, MM_MODEM_MODE_4G, preferred, "4G");

  if (str->len == 0)
    return g_string_free (str, TRUE);
  else
    return g_string_free (str, FALSE);
}
#undef APPEND_MODE_TO_STRING

static void
wwan_network_list_free (GList *network_list)
{
  g_list_free_full (network_list, (GDestroyNotify)mm_modem_3gpp_network_free);
}

static void
cc_wwan_device_scan_complete_cb (GObject      *object,
                                 GAsyncResult *result,
                                 gpointer      user_data)
{
  MMModem3gpp *modem_3gpp = (MMModem3gpp *)object;
  g_autoptr(GTask) task = user_data;
  g_autoptr(GError) error = NULL;
  GList *network_list;

  network_list = mm_modem_3gpp_scan_finish (modem_3gpp, result, &error);

  if (error)
    g_task_return_error (task, g_steal_pointer (&error));
  else
    g_task_return_pointer (task, network_list, (GDestroyNotify)wwan_network_list_free);
}

void
cc_wwan_device_scan_networks (CcWwanDevice        *self,
                              GCancellable        *cancellable,
                              GAsyncReadyCallback  callback,
                              gpointer             user_data)
{
  g_autoptr(GTask) task = NULL;

  g_return_if_fail (CC_IS_WWAN_DEVICE (self));
  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));

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

  mm_modem_3gpp_scan (self->modem_3gpp, cancellable,
                      cc_wwan_device_scan_complete_cb,
                      g_steal_pointer (&task));
}

GList *
cc_wwan_device_scan_networks_finish (CcWwanDevice  *self,
                                     GAsyncResult  *result,
                                     GError       **error)
{
  g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), FALSE);
  g_return_val_if_fail (G_IS_TASK (result), FALSE);

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

static void
cc_wwan_device_register_network_complete_cb (GObject      *object,
                                             GAsyncResult *result,
                                             gpointer      user_data)
{
  CcWwanDevice *self;
  MMModem3gpp *modem_3gpp = (MMModem3gpp *)object;
  g_autoptr(GTask) task = user_data;
  g_autoptr(GError) error = NULL;

  if (!mm_modem_3gpp_register_finish (modem_3gpp, result, &error))
    {
      self = g_task_get_source_object (G_TASK (task));

      g_clear_error (&self->error);
      self->error = g_error_copy (error);
      g_warning ("Error: %s", error->message);
      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ERROR]);

      g_task_return_error (task, g_steal_pointer (&error));
    }
  else
    g_task_return_boolean (task, TRUE);
}

void
cc_wwan_device_register_network (CcWwanDevice        *self,
                                 const gchar         *network_id,
                                 GCancellable        *cancellable,
                                 GAsyncReadyCallback  callback,
                                 gpointer             user_data)
{
  g_autoptr(GTask) task = NULL;

  g_return_if_fail (CC_IS_WWAN_DEVICE (self));
  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));

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

  if (network_id && *network_id)
    self->network_is_manual = TRUE;
  else
    self->network_is_manual = FALSE;

  mm_modem_3gpp_register (self->modem_3gpp, network_id, cancellable,
                          cc_wwan_device_register_network_complete_cb,
                          g_steal_pointer (&task));
}

gboolean
cc_wwan_device_register_network_finish (CcWwanDevice *self,
                                        GAsyncResult *result,
                                        GError       **error)
{
  g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), FALSE);
  g_return_val_if_fail (G_IS_TASK (result), FALSE);

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

/**
 * cc_wwan_device_get_operator_name:
 * @self: a #CcWwanDevice
 *
 * Get the human readable network operator name
 * currently the device is connected to.
 *
 * Returns: (nullable): The operator name or %NULL
 */
const gchar *
cc_wwan_device_get_operator_name (CcWwanDevice *self)
{
  g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), NULL);

  if (!self->modem_3gpp)
    return NULL;

  return mm_modem_3gpp_get_operator_name (self->modem_3gpp);
}

gchar *
cc_wwan_device_dup_sim_identifier (CcWwanDevice *self)
{
  char *identifier;

  identifier = mm_sim_dup_operator_name (self->sim);
  if (identifier)
    return identifier;

  identifier = mm_sim_dup_operator_identifier (self->sim);
  if (identifier)
    return identifier;

  identifier = mm_sim_dup_identifier (self->sim);
  if (identifier)
    return identifier;

  return g_strdup ("");
}

gchar *
cc_wwan_device_dup_network_type_string (CcWwanDevice *self)
{
  MMModemAccessTechnology type;

  g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), NULL);

  type = mm_modem_get_access_technologies (self->modem);

  return mm_modem_access_technology_build_string_from_mask (type);
}

gchar *
cc_wwan_device_dup_signal_string (CcWwanDevice *self)
{
  MMModemSignal *modem_signal;
  MMSignal *signal;
  GString *str;
  gdouble value;
  gboolean recent;

  g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), NULL);

  modem_signal = mm_object_peek_modem_signal (self->mm_object);

  if (!modem_signal)
    return g_strdup_printf ("%d%%", mm_modem_get_signal_quality (self->modem, &recent));

  str = g_string_new ("");

  /* Adapted from ModemManager mmcli-modem-signal.c */
  signal = mm_modem_signal_peek_cdma (modem_signal);
  if (signal)
    {
      if ((value = mm_signal_get_rssi (signal)) != MM_SIGNAL_UNKNOWN)
        g_string_append_printf (str, "rssi: %.2g dBm ", value);
      if ((value = mm_signal_get_ecio (signal)) != MM_SIGNAL_UNKNOWN)
        g_string_append_printf (str, "ecio: %.2g dBm ", value);
    }

  signal = mm_modem_signal_peek_evdo (modem_signal);
  if (signal)
    {
      if ((value = mm_signal_get_rssi (signal)) != MM_SIGNAL_UNKNOWN)
        g_string_append_printf (str, "rssi: %.2g dBm ", value);
      if ((value = mm_signal_get_ecio (signal)) != MM_SIGNAL_UNKNOWN)
        g_string_append_printf (str, "ecio: %.2g dBm ", value);
      if ((value = mm_signal_get_sinr (signal)) != MM_SIGNAL_UNKNOWN)
        g_string_append_printf (str, "sinr: %.2g dB ", value);
      if ((value = mm_signal_get_io (signal)) != MM_SIGNAL_UNKNOWN)
        g_string_append_printf (str, "io: %.2g dBm ", value);
    }

  signal = mm_modem_signal_peek_gsm (modem_signal);
  if (signal)
    if ((value = mm_signal_get_rssi (signal)) != MM_SIGNAL_UNKNOWN)
      g_string_append_printf (str, "rssi: %.2g dBm ", value);

  signal = mm_modem_signal_peek_umts (modem_signal);
  if (signal)
    {
      if ((value = mm_signal_get_rssi (signal)) != MM_SIGNAL_UNKNOWN)
        g_string_append_printf (str, "rssi: %.2g dBm ", value);
      if ((value = mm_signal_get_rscp (signal)) != MM_SIGNAL_UNKNOWN)
        g_string_append_printf (str, "rscp: %.2g dBm ", value);
      if ((value = mm_signal_get_ecio (signal)) != MM_SIGNAL_UNKNOWN)
        g_string_append_printf (str, "ecio: %.2g dBm ", value);
    }

  signal = mm_modem_signal_peek_lte (modem_signal);
  if (signal)
    {
      if ((value = mm_signal_get_rssi (signal)) != MM_SIGNAL_UNKNOWN)
        g_string_append_printf (str, "rssi: %.2g dBm ", value);
      if ((value = mm_signal_get_rsrq (signal)) != MM_SIGNAL_UNKNOWN)
        g_string_append_printf (str, "rsrq: %.2g dB ", value);
      if ((value = mm_signal_get_rsrp (signal)) != MM_SIGNAL_UNKNOWN)
        g_string_append_printf (str, "rsrp: %.2g dBm ", value);
      if ((value = mm_signal_get_snr (signal)) != MM_SIGNAL_UNKNOWN)
        g_string_append_printf (str, "snr: %.2g dB ", value);
    }

  return g_string_free (str, FALSE);
}

const gchar *
cc_wwan_device_get_manufacturer (CcWwanDevice *self)
{
  g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), NULL);

  return mm_modem_get_manufacturer (self->modem);
}

const gchar *
cc_wwan_device_get_model (CcWwanDevice *self)
{
  g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), NULL);

  return mm_modem_get_model (self->modem);
}

const gchar *
cc_wwan_device_get_firmware_version (CcWwanDevice *self)
{
  g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), NULL);

  return mm_modem_get_revision (self->modem);
}

const gchar *
cc_wwan_device_get_identifier (CcWwanDevice *self)
{
  g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), NULL);

  return mm_modem_get_equipment_identifier (self->modem);
}

const gchar *
cc_wwan_device_get_simple_error (CcWwanDevice *self)
{
  g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), NULL);

  if (!self->error)
    return NULL;

  return cc_wwan_error_get_message (self->error);
}

gboolean
cc_wwan_device_is_nm_device (CcWwanDevice *self,
                             GObject      *nm_device)
{
#if defined(HAVE_NETWORK_MANAGER) && defined(BUILD_NETWORK)
  g_return_val_if_fail (NM_IS_DEVICE (nm_device), FALSE);

  return g_str_equal (mm_modem_get_primary_port (self->modem),
                      nm_device_get_iface (NM_DEVICE (nm_device)));
#else
  return FALSE;
#endif
}

const gchar *
cc_wwan_device_get_path (CcWwanDevice *self)
{
  g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), "");

  return mm_object_get_path (self->mm_object);
}

CcWwanData *
cc_wwan_device_get_data (CcWwanDevice *self)
{
  g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), NULL);

  return self->wwan_data;
}

gboolean
cc_wwan_device_pin_valid (const gchar *password,
                          MMModemLock  lock)
{
  size_t len;

  g_return_val_if_fail (lock == MM_MODEM_LOCK_SIM_PIN  ||
                        lock == MM_MODEM_LOCK_SIM_PIN2 ||
                        lock == MM_MODEM_LOCK_SIM_PUK  ||
                        lock == MM_MODEM_LOCK_SIM_PUK2, FALSE);
  if (!password)
    return FALSE;

  len = strlen (password);

  if (len < 4 || len > 8)
    return FALSE;

  if (strspn (password, "0123456789") != len)
    return FALSE;

  /*
   * XXX: Can PUK code be something other than 8 digits?
   * 3GPP standard seems mum on this
   */
  if (lock == MM_MODEM_LOCK_SIM_PUK ||
      lock == MM_MODEM_LOCK_SIM_PUK2)
    if (len != 8)
      return FALSE;

  return TRUE;
}
