/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details:
 *
 * Copyright (C) 2012 Google, Inc.
 * Copyright (C) 2016 Aleksander Morgado <aleksander@aleksander.es>
 */

#include <config.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>

#include <ModemManager.h>
#define _LIBMM_INSIDE_MM
#include <libmm-glib.h>

#include "mm-broadband-modem-qmi.h"
#include "mm-log-object.h"
#include "mm-sim-qmi.h"
#include "mm-modem-helpers-qmi.h"
#include "mm-shared-qmi.h"

G_DEFINE_TYPE (MMSimQmi, mm_sim_qmi, MM_TYPE_BASE_SIM)

enum {
    PROP_0,
    PROP_DMS_UIM_DEPRECATED,
    PROP_LAST
};

static GParamSpec *properties[PROP_LAST];

struct _MMSimQmiPrivate {
    gboolean dms_uim_deprecated;
};

static const guint16 mf_file_path[]  = { 0x3F00 };
static const guint16 adf_file_path[] = { 0x3F00, 0x7FFF };

/*****************************************************************************/

static gboolean
ensure_qmi_client (GTask       *task,
                   MMSimQmi    *self,
                   QmiService   service,
                   QmiClient  **o_client)
{
    MMBaseModem *modem = NULL;
    QmiClient *client;
    g_autoptr(GError) error = NULL;

    g_object_get (self,
                  MM_BASE_SIM_MODEM, &modem,
                  NULL);
    g_assert (MM_IS_BASE_MODEM (modem));

    g_assert (MM_IS_SHARED_QMI (modem));
    client = mm_shared_qmi_peek_client (MM_SHARED_QMI (modem),
                                        service,
                                        MM_PORT_QMI_FLAG_DEFAULT,
                                        &error);

    g_object_unref (modem);

    if (!client) {
        if (task) {
            g_task_return_error (task, g_steal_pointer (&error));
            g_object_unref (task);
        }
        return FALSE;
    }

    *o_client = client;
    return TRUE;
}

/*****************************************************************************/
/* Wait for SIM ready */

#define SIM_READY_CHECKS_MAX 5
#define SIM_READY_CHECKS_TIMEOUT_SECS 1

typedef struct {
    QmiClient *client_uim;
    guint      ready_checks_n;
} WaitSimReadyContext;

static void
wait_sim_ready_context_free (WaitSimReadyContext *ctx)
{
    g_clear_object (&ctx->client_uim);
    g_slice_free (WaitSimReadyContext, ctx);
}

static gboolean
wait_sim_ready_finish (MMBaseSim     *self,
                       GAsyncResult  *res,
                       GError       **error)
{
    return g_task_propagate_boolean (G_TASK (res), error);
}

static void sim_ready_check (GTask *task);

static gboolean
sim_ready_retry_cb (GTask *task)
{
    sim_ready_check (task);
    return G_SOURCE_REMOVE;
}

static void
sim_ready_retry (GTask *task)
{
    g_timeout_add_seconds (SIM_READY_CHECKS_TIMEOUT_SECS, (GSourceFunc) sim_ready_retry_cb, task);
}

static void
uim_get_card_status_ready (QmiClientUim *client,
                           GAsyncResult *res,
                           GTask        *task)
{
    g_autoptr(QmiMessageUimGetCardStatusOutput)  output = NULL;
    g_autoptr(GError)                            error = NULL;
    MMSimQmi                                    *self;

    self = g_task_get_source_object (task);

    output = qmi_client_uim_get_card_status_finish (client, res, &error);
    if (!output ||
        !qmi_message_uim_get_card_status_output_get_result (output, &error) ||
        (!mm_qmi_uim_get_card_status_output_parse (self, output, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &error) &&
         (g_error_matches (error, MM_MOBILE_EQUIPMENT_ERROR, MM_MOBILE_EQUIPMENT_ERROR_SIM_NOT_INSERTED) ||
          g_error_matches (error, MM_CORE_ERROR, MM_CORE_ERROR_RETRY)))) {
        mm_obj_dbg (self, "sim not yet considered ready... retrying");
        sim_ready_retry (task);
        return;
    }

    /* SIM is considered ready now */
    mm_obj_dbg (self, "sim is ready");
    g_task_return_boolean (task, TRUE);
    g_object_unref (task);
}

static void
sim_ready_check (GTask *task)
{
    WaitSimReadyContext *ctx;
    MMSimQmi            *self;

    self = g_task_get_source_object (task);
    ctx  = g_task_get_task_data (task);

    ctx->ready_checks_n++;
    if (ctx->ready_checks_n == SIM_READY_CHECKS_MAX) {
        g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                                 "failed waiting for SIM readiness");
        g_object_unref (task);
        return;
    }

    mm_obj_dbg (self, "checking SIM readiness");
    qmi_client_uim_get_card_status (QMI_CLIENT_UIM (ctx->client_uim),
                                    NULL,
                                    5,
                                    NULL,
                                    (GAsyncReadyCallback) uim_get_card_status_ready,
                                    task);
}

static void
wait_sim_ready (MMBaseSim           *_self,
                GAsyncReadyCallback  callback,
                gpointer             user_data)
{
    QmiClient           *client;
    MMSimQmi            *self;
    GTask               *task;
    WaitSimReadyContext *ctx;

    self = MM_SIM_QMI (_self);
    task = g_task_new (self, NULL, callback, user_data);

    mm_obj_dbg (self, "waiting for SIM to be ready...");
    if (!self->priv->dms_uim_deprecated) {
        g_task_return_boolean (task, TRUE);
        g_object_unref (task);
        return;
    }

    if (!ensure_qmi_client (task, self, QMI_SERVICE_UIM, &client))
        return;

    ctx = g_slice_new0 (WaitSimReadyContext);
    ctx->client_uim = g_object_ref (client);
    g_task_set_task_data (task, ctx, (GDestroyNotify) wait_sim_ready_context_free);

    sim_ready_check (task);
}

/*****************************************************************************/
/* Load SIM ID (ICCID) */

static GArray *
uim_read_finish (QmiClientUim  *client,
                 GAsyncResult  *res,
                 GError       **error)
{
    return g_task_propagate_pointer (G_TASK (res), error);
}

static void
uim_read_ready (QmiClientUim *client,
                GAsyncResult *res,
                GTask        *task)
{
    QmiMessageUimReadTransparentOutput *output;
    GError *error = NULL;

    output = qmi_client_uim_read_transparent_finish (client, res, &error);
    if (!output) {
        g_prefix_error (&error, "QMI operation failed: ");
        g_task_return_error (task, error);
    } else if (!qmi_message_uim_read_transparent_output_get_result (output, &error)) {
        g_prefix_error (&error, "Couldn't read data from UIM: ");
        g_task_return_error (task, error);
    } else {
        GArray *read_result = NULL;

        qmi_message_uim_read_transparent_output_get_read_result (output, &read_result, NULL);
        if (read_result)
            g_task_return_pointer (task,
                                   g_array_ref (read_result),
                                   (GDestroyNotify) g_array_unref);
        else
            g_task_return_new_error (task,
                                     MM_CORE_ERROR,
                                     MM_CORE_ERROR_FAILED,
                                     "Read malformed data from UIM");
    }

    if (output)
        qmi_message_uim_read_transparent_output_unref (output);

    g_object_unref (task);
}

static void
uim_read (MMSimQmi            *self,
          guint16              file_id,
          const guint16       *file_path,
          gsize                file_path_len,
          GAsyncReadyCallback  callback,
          gpointer             user_data)
{
    GTask *task;
    QmiClient *client = NULL;
    GArray *file_path_bytes;
    gsize i;
    QmiMessageUimReadTransparentInput *input;
    GArray *aid;

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

    if (!ensure_qmi_client (task,
                            self,
                            QMI_SERVICE_UIM, &client))
        return;

    file_path_bytes = g_array_sized_new (FALSE, FALSE, 1, file_path_len * 2);
    for (i = 0; i < file_path_len; ++i) {
        guint8 byte;

        byte = file_path[i] & 0xFF;
        g_array_append_val (file_path_bytes, byte);
        byte = (file_path[i] >> 8) & 0xFF;
        g_array_append_val (file_path_bytes, byte);
    }

    input = qmi_message_uim_read_transparent_input_new ();
    aid = g_array_new (FALSE, FALSE, sizeof (guint8)); /* empty AID */
    qmi_message_uim_read_transparent_input_set_session (
        input,
        QMI_UIM_SESSION_TYPE_PRIMARY_GW_PROVISIONING,
        aid,
        NULL);
    g_array_unref (aid);
    qmi_message_uim_read_transparent_input_set_file (input,
                                                     file_id,
                                                     file_path_bytes,
                                                     NULL);
    qmi_message_uim_read_transparent_input_set_read_information (input,
                                                                 0,
                                                                 0,
                                                                 NULL);
    g_array_unref (file_path_bytes);

    qmi_client_uim_read_transparent (QMI_CLIENT_UIM (client),
                                     input,
                                     10,
                                     NULL,
                                     (GAsyncReadyCallback)uim_read_ready,
                                     task);
    qmi_message_uim_read_transparent_input_unref (input);
}

static gchar *
load_sim_identifier_finish (MMBaseSim     *self,
                            GAsyncResult  *res,
                            GError       **error)
{
    return g_task_propagate_pointer (G_TASK (res), error);
}

static void
uim_get_iccid_ready (QmiClientUim *client,
                     GAsyncResult *res,
                     GTask        *task)
{
    GError            *error = NULL;
    g_autoptr(GArray)  read_result = NULL;
    g_autofree gchar  *raw_iccid = NULL;
    gchar             *iccid;

    read_result = uim_read_finish (client, res, &error);
    if (!read_result) {
        g_task_return_error (task, error);
        g_object_unref (task);
        return;
    }

    raw_iccid = mm_utils_bin2hexstr ((const guint8 *) read_result->data, read_result->len);
    g_assert (raw_iccid);
    iccid = mm_3gpp_parse_iccid (raw_iccid, &error);
    if (!iccid)
        g_task_return_error (task, error);
    else
        g_task_return_pointer (task, iccid, g_free);
    g_object_unref (task);
}

static void
uim_get_iccid (MMSimQmi *self,
               GTask    *task)
{
    uim_read (self,
              0x2FE2,
              mf_file_path,
              G_N_ELEMENTS (mf_file_path),
              (GAsyncReadyCallback)uim_get_iccid_ready,
              task);
}

static void
dms_uim_get_iccid_ready (QmiClientDms *client,
                         GAsyncResult *res,
                         GTask        *task)
{
    QmiMessageDmsUimGetIccidOutput *output = NULL;
    GError *error = NULL;

    output = qmi_client_dms_uim_get_iccid_finish (client, res, &error);
    if (!output) {
        g_prefix_error (&error, "QMI operation failed: ");
        g_task_return_error (task, error);
    } else if (!qmi_message_dms_uim_get_iccid_output_get_result (output, &error)) {
        g_prefix_error (&error, "Couldn't get UIM ICCID: ");
        g_task_return_error (task, error);
    } else {
        const gchar *str = NULL;

        qmi_message_dms_uim_get_iccid_output_get_iccid (output, &str, NULL);
        g_task_return_pointer (task, g_strdup (str), g_free);
    }

    if (output)
        qmi_message_dms_uim_get_iccid_output_unref (output);

    g_object_unref (task);
}

static void
dms_uim_get_iccid (MMSimQmi *self,
                   GTask    *task)
{
    QmiClient *client = NULL;

    if (!ensure_qmi_client (task,
                            self,
                            QMI_SERVICE_DMS, &client))
        return;

    qmi_client_dms_uim_get_iccid (QMI_CLIENT_DMS (client),
                                  NULL,
                                  5,
                                  NULL,
                                  (GAsyncReadyCallback)dms_uim_get_iccid_ready,
                                  task);
}

static void
load_sim_identifier (MMBaseSim           *_self,
                     GAsyncReadyCallback  callback,
                     gpointer             user_data)
{
    MMSimQmi *self;
    GTask *task;

    self = MM_SIM_QMI (_self);
    task = g_task_new (self, NULL, callback, user_data);

    mm_obj_dbg (self, "loading SIM identifier...");
    if (!self->priv->dms_uim_deprecated)
        dms_uim_get_iccid (self, task);
    else
        uim_get_iccid (self, task);
}

/*****************************************************************************/
/* Load IMSI */

static gchar *
load_imsi_finish (MMBaseSim     *self,
                  GAsyncResult  *res,
                  GError       **error)
{
    return g_task_propagate_pointer (G_TASK (res), error);
}

static void
uim_get_imsi_ready (QmiClientUim *client,
                    GAsyncResult *res,
                    GTask        *task)
{
    GError            *error = NULL;
    g_autoptr(GArray)  read_result = NULL;
    g_autofree gchar  *imsi = NULL;

    read_result = uim_read_finish (client, res, &error);
    if (!read_result) {
        g_task_return_error (task, error);
        g_object_unref (task);
        return;
    }

    imsi = mm_bcd_to_string ((const guint8 *) read_result->data, read_result->len,
                             TRUE /* low_nybble_first */);
    g_assert (imsi);
    if (strlen (imsi) < 3)
        g_task_return_new_error (task,
                                 MM_CORE_ERROR,
                                 MM_CORE_ERROR_FAILED,
                                 "IMSI is malformed");
    else
        /* EFimsi contains a length byte, follwed by a nibble for parity,
         * and then followed by the actual IMSI in BCD. After converting
         * the BCD into a decimal string, we simply skip the first 3
         * decimal digits to obtain the IMSI. */
        g_task_return_pointer (task, g_strdup (imsi + 3), g_free);
    g_object_unref (task);
}

static void
uim_get_imsi (MMSimQmi *self,
              GTask    *task)
{
    uim_read (self,
              0x6F07,
              adf_file_path,
              G_N_ELEMENTS (adf_file_path),
              (GAsyncReadyCallback)uim_get_imsi_ready,
              task);
}

static void
dms_uim_get_imsi_ready (QmiClientDms *client,
                        GAsyncResult *res,
                        GTask        *task)
{
    QmiMessageDmsUimGetImsiOutput *output = NULL;
    GError *error = NULL;

    output = qmi_client_dms_uim_get_imsi_finish (client, res, &error);
    if (!output) {
        g_prefix_error (&error, "QMI operation failed: ");
        g_task_return_error (task, error);
    } else if (!qmi_message_dms_uim_get_imsi_output_get_result (output, &error)) {
        g_prefix_error (&error, "Couldn't get UIM IMSI: ");
        g_task_return_error (task, error);
    } else {
        const gchar *str = NULL;

        qmi_message_dms_uim_get_imsi_output_get_imsi (output, &str, NULL);
        g_task_return_pointer (task, g_strdup (str), g_free);
    }

    if (output)
        qmi_message_dms_uim_get_imsi_output_unref (output);

    g_object_unref (task);
}

static void
dms_uim_get_imsi (MMSimQmi *self,
                  GTask    *task)
{
    QmiClient *client = NULL;

    if (!ensure_qmi_client (task,
                            self,
                            QMI_SERVICE_DMS, &client))
        return;

    qmi_client_dms_uim_get_imsi (QMI_CLIENT_DMS (client),
                                 NULL,
                                 5,
                                 NULL,
                                 (GAsyncReadyCallback)dms_uim_get_imsi_ready,
                                 task);
}

static void
load_imsi (MMBaseSim           *_self,
           GAsyncReadyCallback  callback,
           gpointer             user_data)
{
    MMSimQmi *self;
    GTask *task;

    self = MM_SIM_QMI (_self);
    task = g_task_new (self, NULL, callback, user_data);

    mm_obj_dbg (self, "loading IMSI...");
    if (!self->priv->dms_uim_deprecated)
        dms_uim_get_imsi (self, task);
    else
        uim_get_imsi (self, task);
}

/*****************************************************************************/
/* Load GID1 and GID2 */

static GByteArray *
common_load_gid_finish (MMBaseSim     *self,
                        GAsyncResult  *res,
                        GError       **error)
{
    return g_task_propagate_pointer (G_TASK (res), error);
}

static void
uim_get_gid_ready (QmiClientUim *client,
                   GAsyncResult *res,
                   GTask        *task)
{
    GError            *error = NULL;
    g_autoptr(GArray)  read_result = NULL;

    read_result = uim_read_finish (client, res, &error);
    if (!read_result)
        g_task_return_error (task, error);
    else
        g_task_return_pointer (task,
                               g_byte_array_append (g_byte_array_sized_new (read_result->len),
                                                    (const guint8 *)(read_result->data),
                                                    read_result->len),
                               (GDestroyNotify)g_byte_array_unref);
    g_object_unref (task);
}

static void
common_load_gid (MMBaseSim           *self,
                 guint16              file_id,
                 GAsyncReadyCallback  callback,
                 gpointer             user_data)
{
    GTask *task;

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

    uim_read (MM_SIM_QMI (self),
              file_id,
              adf_file_path,
              G_N_ELEMENTS (adf_file_path),
              (GAsyncReadyCallback)uim_get_gid_ready,
              task);
}

static GByteArray *
load_gid1_finish (MMBaseSim     *self,
                  GAsyncResult  *res,
                  GError       **error)
{
    return common_load_gid_finish (self, res, error);
}

static void
load_gid1 (MMBaseSim           *self,
           GAsyncReadyCallback  callback,
           gpointer             user_data)
{
    common_load_gid (self, 0x6F3E, callback, user_data);
}

static GByteArray *
load_gid2_finish (MMBaseSim     *self,
                  GAsyncResult  *res,
                  GError       **error)
{
    return common_load_gid_finish (self, res, error);
}

static void
load_gid2 (MMBaseSim           *self,
           GAsyncReadyCallback  callback,
           gpointer             user_data)
{
    common_load_gid (self, 0x6F3F, callback, user_data);
}

/*****************************************************************************/
/* Load operator identifier */

static gboolean
get_home_network (QmiClientNas  *client,
                  GAsyncResult  *res,
                  guint16       *out_mcc,
                  guint16       *out_mnc,
                  gboolean      *out_mnc_with_pcs,
                  gchar        **out_operator_name,
                  GError       **error)
{
    QmiMessageNasGetHomeNetworkOutput *output = NULL;
    gboolean success = FALSE;

    output = qmi_client_nas_get_home_network_finish (client, res, error);
    if (!output) {
        g_prefix_error (error, "QMI operation failed: ");
    } else if (!qmi_message_nas_get_home_network_output_get_result (output, error)) {
        g_prefix_error (error, "Couldn't get home network: ");
    } else {
        const gchar *name = NULL;

        qmi_message_nas_get_home_network_output_get_home_network (
            output,
            out_mcc,
            out_mnc,
            &name,
            NULL);
        if (out_operator_name)
            *out_operator_name = g_strdup (name);

        if (out_mnc_with_pcs) {
            gboolean is_3gpp;
            gboolean mnc_includes_pcs_digit;

            if (qmi_message_nas_get_home_network_output_get_home_network_3gpp_mnc (
                    output,
                    &is_3gpp,
                    &mnc_includes_pcs_digit,
                    NULL) &&
                is_3gpp &&
                mnc_includes_pcs_digit) {
                /* MNC should include PCS digit */
                *out_mnc_with_pcs = TRUE;
            } else {
                /* We default to NO PCS digit, unless of course the MNC is already > 99 */
                *out_mnc_with_pcs = FALSE;
            }
        }

        success = TRUE;
    }

    if (output)
        qmi_message_nas_get_home_network_output_unref (output);

    return success;
}

static gchar *
load_operator_identifier_finish (MMBaseSim     *self,
                                 GAsyncResult  *res,
                                 GError       **error)
{
    return g_task_propagate_pointer (G_TASK (res), error);
}

static void
load_operator_identifier_ready (QmiClientNas *client,
                                GAsyncResult *res,
                                GTask        *task)
{
    guint16 mcc, mnc;
    gboolean mnc_with_pcs;
    GError *error = NULL;
    GString *aux;

    if (!get_home_network (client, res, &mcc, &mnc, &mnc_with_pcs, NULL, &error)) {
        g_task_return_error (task, error);
        g_object_unref (task);
        return;
    }

    aux = g_string_new ("");
    /* MCC always 3 digits */
    g_string_append_printf (aux, "%.3" G_GUINT16_FORMAT, mcc);
    /* Guess about MNC, if < 100 assume it's 2 digits, no PCS info here */
    if (mnc >= 100 || mnc_with_pcs)
        g_string_append_printf (aux, "%.3" G_GUINT16_FORMAT, mnc);
    else
        g_string_append_printf (aux, "%.2" G_GUINT16_FORMAT, mnc);
    g_task_return_pointer (task, g_string_free (aux, FALSE), g_free);
    g_object_unref (task);
}

static void
load_operator_identifier (MMBaseSim           *self,
                          GAsyncReadyCallback  callback,
                          gpointer             user_data)
{
    GTask *task;
    QmiClient *client = NULL;

    task = g_task_new (self, NULL, callback, user_data);
    if (!ensure_qmi_client (task,
                            MM_SIM_QMI (self),
                            QMI_SERVICE_NAS, &client))
        return;

    mm_obj_dbg (self, "loading SIM operator identifier...");
    qmi_client_nas_get_home_network (QMI_CLIENT_NAS (client),
                                     NULL,
                                     5,
                                     NULL,
                                     (GAsyncReadyCallback)load_operator_identifier_ready,
                                     task);
}

/*****************************************************************************/
/* Load operator name */

static gchar *
load_operator_name_finish (MMBaseSim     *self,
                           GAsyncResult  *res,
                           GError       **error)
{
    return g_task_propagate_pointer (G_TASK (res), error);
}

static void
load_operator_name_ready (QmiClientNas *client,
                          GAsyncResult *res,
                          GTask        *task)
{
    gchar *operator_name = NULL;
    GError *error = NULL;

    if (!get_home_network (client, res, NULL, NULL, NULL, &operator_name, &error))
        g_task_return_error (task, error);
    else
        g_task_return_pointer (task, operator_name, g_free);
    g_object_unref (task);
}

static void
load_operator_name (MMBaseSim           *self,
                    GAsyncReadyCallback  callback,
                    gpointer             user_data)
{
    GTask *task;
    QmiClient *client = NULL;

    task = g_task_new (self, NULL, callback, user_data);
    if (!ensure_qmi_client (task,
                            MM_SIM_QMI (self),
                            QMI_SERVICE_NAS, &client))
        return;

    mm_obj_dbg (self, "loading SIM operator name...");
    qmi_client_nas_get_home_network (QMI_CLIENT_NAS (client),
                                     NULL,
                                     5,
                                     NULL,
                                     (GAsyncReadyCallback)load_operator_name_ready,
                                     task);
}

/*****************************************************************************/
/* Load preferred networks */

static GList *
load_preferred_networks_finish (MMBaseSim     *self,
                                GAsyncResult  *res,
                                GError       **error)
{
    return g_task_propagate_pointer (G_TASK (res), error);
}

static GList *
parse_get_preferred_networks (QmiMessageNasGetPreferredNetworksOutput *output)
{
    GList  *result = NULL;
    GArray *preferred_nets_array = NULL;
    GArray *preferred_nets_mnc_pcs_digit_array = NULL;
    guint   i;

    if (qmi_message_nas_get_preferred_networks_output_get_preferred_networks (output,
                                                                              &preferred_nets_array,
                                                                              NULL)) {
        qmi_message_nas_get_preferred_networks_output_get_mnc_pcs_digit_include_status (output,
                                                                                        &preferred_nets_mnc_pcs_digit_array,
                                                                                        NULL);
        for (i = 0; i < preferred_nets_array->len; i++) {
            QmiMessageNasGetPreferredNetworksOutputPreferredNetworksElement        *net;
            QmiMessageNasGetPreferredNetworksOutputMncPcsDigitIncludeStatusElement *mnc_pcs_digit = NULL;
            MMSimPreferredNetwork   *new_item;
            g_autofree gchar        *operator_code = NULL;
            MMModemAccessTechnology  act = MM_MODEM_ACCESS_TECHNOLOGY_UNKNOWN;

            net = &g_array_index (preferred_nets_array,
                                  QmiMessageNasGetPreferredNetworksOutputPreferredNetworksElement, i);
            if (preferred_nets_mnc_pcs_digit_array && i < preferred_nets_mnc_pcs_digit_array->len)
                mnc_pcs_digit = &g_array_index (preferred_nets_mnc_pcs_digit_array,
                                                QmiMessageNasGetPreferredNetworksOutputMncPcsDigitIncludeStatusElement, i);

            new_item = mm_sim_preferred_network_new ();

            if (net->mnc > 99 || (mnc_pcs_digit != NULL && mnc_pcs_digit->includes_pcs_digit))
                operator_code = g_strdup_printf ("%03d%03d", net->mcc, net->mnc);
            else
                operator_code = g_strdup_printf ("%03d%02d", net->mcc, net->mnc);
            mm_sim_preferred_network_set_operator_code (new_item, operator_code);

            if (net->radio_access_technology & QMI_NAS_PLMN_ACCESS_TECHNOLOGY_IDENTIFIER_GSM)
                act |= MM_MODEM_ACCESS_TECHNOLOGY_GSM;
            if (net->radio_access_technology & QMI_NAS_PLMN_ACCESS_TECHNOLOGY_IDENTIFIER_GSM_COMPACT)
                act |= MM_MODEM_ACCESS_TECHNOLOGY_GSM_COMPACT;
            if (net->radio_access_technology & QMI_NAS_PLMN_ACCESS_TECHNOLOGY_IDENTIFIER_UTRAN)
                act |= MM_MODEM_ACCESS_TECHNOLOGY_UMTS;
            if (net->radio_access_technology & QMI_NAS_PLMN_ACCESS_TECHNOLOGY_IDENTIFIER_EUTRAN)
                act |= MM_MODEM_ACCESS_TECHNOLOGY_LTE;
            if (net->radio_access_technology & QMI_NAS_PLMN_ACCESS_TECHNOLOGY_IDENTIFIER_NGRAN)
                act |= MM_MODEM_ACCESS_TECHNOLOGY_5GNR;
            mm_sim_preferred_network_set_access_technology (new_item, act);

            result = g_list_append (result, new_item);
        }
    }

    return result;
}

static void
load_preferred_networks_ready (QmiClientNas *client,
                               GAsyncResult *res,
                               GTask        *task)
{
    QmiMessageNasGetPreferredNetworksOutput *output;
    GError *error = NULL;

    output = qmi_client_nas_get_preferred_networks_finish (client, res, &error);
    if (!output) {
        g_prefix_error (&error, "QMI operation failed: ");
        g_task_return_error (task, error);
    } else if (!qmi_message_nas_get_preferred_networks_output_get_result (output, &error)) {
        g_prefix_error (&error, "Couldn't get preferred networks: ");
        g_task_return_error (task, error);
    } else
        g_task_return_pointer (task, parse_get_preferred_networks (output), (GDestroyNotify) mm_sim_preferred_network_list_free);

    if (output)
        qmi_message_nas_get_preferred_networks_output_unref (output);
    g_object_unref (task);
}

static void
load_preferred_networks (MMBaseSim           *self,
                         GAsyncReadyCallback  callback,
                         gpointer             user_data)
{
    GTask     *task;
    QmiClient *client = NULL;

    task = g_task_new (self, NULL, callback, user_data);
    if (!ensure_qmi_client (task,
                            MM_SIM_QMI (self),
                            QMI_SERVICE_NAS, &client))
        return;

    mm_obj_dbg (self, "loading preferred network list...");
    qmi_client_nas_get_preferred_networks (QMI_CLIENT_NAS (client),
                                           NULL,
                                           5,
                                           NULL,
                                           (GAsyncReadyCallback)load_preferred_networks_ready,
                                           task);
}

/*****************************************************************************/
/* Set preferred networks */

typedef struct {
    /* Preferred network list to be set, used for after-check comparison */
    GList    *set_list;
} SetPreferredNetworksContext;

static void
set_preferred_network_context_free (SetPreferredNetworksContext *ctx)
{
    g_list_free_full (ctx->set_list, (GDestroyNotify) mm_sim_preferred_network_free);
    g_slice_free (SetPreferredNetworksContext, ctx);
}

static gboolean
set_preferred_networks_finish (MMBaseSim *self,
                               GAsyncResult *res,
                               GError **error)
{
    return g_task_propagate_boolean (G_TASK (res), error);
}

static void
set_preferred_networks_reload_ready (MMBaseSim    *self,
                                     GAsyncResult *res,
                                     GTask        *task)
{
    GError                      *error = NULL;
    GList                       *loaded_list;
    GList                       *loaded_iter;
    GList                       *set_iter;
    SetPreferredNetworksContext *ctx;

    ctx = g_task_get_task_data (task);

    loaded_list = load_preferred_networks_finish (self, res, &error);
    if (error) {
        mm_obj_warn (self, "couldn't reload list of preferred networks: %s", error->message);
        g_task_return_error (task, error);
        g_object_unref (task);
        return;
    }

    /* Compare the set and loaded network list for differences */
    loaded_iter = loaded_list;
    set_iter = ctx->set_list;

    while (loaded_iter && set_iter) {
        const gchar             *loaded_op_code;
        const gchar             *set_op_code;
        MMModemAccessTechnology  loaded_act;
        MMModemAccessTechnology  set_act;

        loaded_op_code = mm_sim_preferred_network_get_operator_code (loaded_iter->data);
        set_op_code = mm_sim_preferred_network_get_operator_code (set_iter->data);
        loaded_act = mm_sim_preferred_network_get_access_technology (loaded_iter->data);
        set_act = mm_sim_preferred_network_get_access_technology (set_iter->data);

        /* Operator code mismatch is never expected, but check it just in case */
        if (g_strcmp0 (loaded_op_code, set_op_code)) {
            mm_obj_warn (self, "operator code mismatch, expected '%s' loaded '%s'",
                         set_op_code, loaded_op_code);
            error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                                 "Mismatch in requested and set operator code");
            break;
        }
        /* Check if there are access technology bits requested but unset */
        if ((loaded_act & set_act) != set_act) {
            MMModemAccessTechnology unset = set_act & ~loaded_act;

            mm_obj_warn (self, "access technologies '%s' not set for operator code '%s'",
                         mm_modem_access_technology_build_string_from_mask (unset),
                         set_op_code);
            error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_UNSUPPORTED,
                                 "Access technology unsupported by modem or SIM");
            break;
        }
        loaded_iter = g_list_next (loaded_iter);
        set_iter = g_list_next (set_iter);
    }
    if (!error && loaded_iter == NULL && set_iter != NULL) {
        /* Not all networks were written; some modems silently discard networks
         * that exceed the SIM card capacity.
         */
        error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_TOO_MANY,
                             "Too many networks; %u networks written",
                             g_list_length (loaded_list));
    }

    if (error) {
        /* Update the PreferredNetworks property to real SIM contents */
        mm_gdbus_sim_set_preferred_networks (MM_GDBUS_SIM (self),
                                             mm_sim_preferred_network_list_get_variant (loaded_list));
        g_task_return_error (task, error);
    } else
        g_task_return_boolean (task, TRUE);

    g_list_free_full (loaded_list, (GDestroyNotify) mm_sim_preferred_network_free);
    g_object_unref (task);
}

static void
set_preferred_networks_ready (QmiClientNas *client,
                              GAsyncResult *res,
                              GTask        *task)
{
    QmiMessageNasSetPreferredNetworksOutput *output;
    GError                                  *error = NULL;
    MMBaseSim                               *self;

    self = g_task_get_source_object (task);

    output = qmi_client_nas_set_preferred_networks_finish (client, res, &error);
    if (!output) {
        g_prefix_error (&error, "QMI operation failed: ");
        g_task_return_error (task, error);
    } else if (!qmi_message_nas_set_preferred_networks_output_get_result (output, &error)) {
        g_prefix_error (&error, "Couldn't set preferred networks: ");
        g_task_return_error (task, error);
    } else {
        /* Reload the networks from modem to check whether everything was written */
        load_preferred_networks (self, (GAsyncReadyCallback) set_preferred_networks_reload_ready, task);
        qmi_message_nas_set_preferred_networks_output_unref (output);
        return;
    }

    if (output)
        qmi_message_nas_set_preferred_networks_output_unref (output);
    g_object_unref (task);
}

static void
set_preferred_networks (MMBaseSim *self,
                        GList *preferred_network_list,
                        GAsyncReadyCallback callback,
                        gpointer user_data)
{
    GTask                                  *task;
    QmiMessageNasSetPreferredNetworksInput *input;
    QmiClient                              *client = NULL;
    GArray                                 *preferred_nets_array;
    GArray                                 *preferred_nets_mnc_pcs_digit_array;
    SetPreferredNetworksContext            *ctx;

    task = g_task_new (self, NULL, callback, user_data);
    if (!ensure_qmi_client (task,
                            MM_SIM_QMI (self),
                            QMI_SERVICE_NAS, &client))
        return;

    ctx = g_slice_new0 (SetPreferredNetworksContext);
    ctx->set_list = mm_sim_preferred_network_list_copy (preferred_network_list);
    g_task_set_task_data (task, ctx, (GDestroyNotify) set_preferred_network_context_free);

    mm_obj_dbg (self, "setting preferred networks...");

    input = qmi_message_nas_set_preferred_networks_input_new ();

    preferred_nets_array = g_array_new (FALSE, TRUE, sizeof (QmiMessageNasSetPreferredNetworksInputPreferredNetworksElement));
    preferred_nets_mnc_pcs_digit_array = g_array_new (FALSE, TRUE, sizeof (QmiMessageNasSetPreferredNetworksInputMncPcsDigitIncludeStatusElement));

    while (preferred_network_list) {
        QmiMessageNasSetPreferredNetworksInputPreferredNetworksElement         preferred_nets_element;
        QmiMessageNasSetPreferredNetworksInputMncPcsDigitIncludeStatusElement  pcs_digit_element;
        const gchar                                                           *operator_code;
        MMModemAccessTechnology                                                act;

        memset (&preferred_nets_element, 0, sizeof (preferred_nets_element));
        memset (&pcs_digit_element, 0, sizeof (pcs_digit_element));

        operator_code = mm_sim_preferred_network_get_operator_code (preferred_network_list->data);
        act = mm_sim_preferred_network_get_access_technology (preferred_network_list->data);
        if (mm_3gpp_parse_operator_id (operator_code, &preferred_nets_element.mcc, &preferred_nets_element.mnc,
                                       &pcs_digit_element.includes_pcs_digit, NULL)) {
            pcs_digit_element.mcc = preferred_nets_element.mcc;
            pcs_digit_element.mnc = preferred_nets_element.mnc;

            preferred_nets_element.radio_access_technology = QMI_NAS_PLMN_ACCESS_TECHNOLOGY_IDENTIFIER_UNSPECIFIED;
            if (act & MM_MODEM_ACCESS_TECHNOLOGY_GSM)
                preferred_nets_element.radio_access_technology |= QMI_NAS_PLMN_ACCESS_TECHNOLOGY_IDENTIFIER_GSM;
            if (act & MM_MODEM_ACCESS_TECHNOLOGY_GSM_COMPACT)
                preferred_nets_element.radio_access_technology |= QMI_NAS_PLMN_ACCESS_TECHNOLOGY_IDENTIFIER_GSM_COMPACT;
            if (act & MM_MODEM_ACCESS_TECHNOLOGY_UMTS)
                preferred_nets_element.radio_access_technology |= QMI_NAS_PLMN_ACCESS_TECHNOLOGY_IDENTIFIER_UTRAN;
            if (act & MM_MODEM_ACCESS_TECHNOLOGY_LTE)
                preferred_nets_element.radio_access_technology |= QMI_NAS_PLMN_ACCESS_TECHNOLOGY_IDENTIFIER_EUTRAN;
            if (act & MM_MODEM_ACCESS_TECHNOLOGY_5GNR)
                preferred_nets_element.radio_access_technology |= QMI_NAS_PLMN_ACCESS_TECHNOLOGY_IDENTIFIER_NGRAN;

            g_array_append_val (preferred_nets_array, preferred_nets_element);
            g_array_append_val (preferred_nets_mnc_pcs_digit_array, pcs_digit_element);
        }

        preferred_network_list = g_list_next (preferred_network_list);
    }

    qmi_message_nas_set_preferred_networks_input_set_preferred_networks (input, preferred_nets_array, NULL);
    qmi_message_nas_set_preferred_networks_input_set_mnc_pcs_digit_include_status (input, preferred_nets_mnc_pcs_digit_array, NULL);
    /* Always clear any pre-existing networks */
    qmi_message_nas_set_preferred_networks_input_set_clear_previous_preferred_networks (input, TRUE, NULL);

    qmi_client_nas_set_preferred_networks (QMI_CLIENT_NAS (client),
                                           input,
                                           5,
                                           NULL,
                                           (GAsyncReadyCallback)set_preferred_networks_ready,
                                           task);

    qmi_message_nas_set_preferred_networks_input_unref (input);
    g_array_unref (preferred_nets_array);
    g_array_unref (preferred_nets_mnc_pcs_digit_array);
}

/*****************************************************************************/
/* Send PIN */

static GError *
pin_qmi_error_to_mobile_equipment_error (GError *qmi_error)
{
    GError *me_error = NULL;

    if (g_error_matches (qmi_error,
                         QMI_PROTOCOL_ERROR,
                         QMI_PROTOCOL_ERROR_INCORRECT_PIN)) {
        me_error = g_error_new_literal (MM_MOBILE_EQUIPMENT_ERROR,
                                        MM_MOBILE_EQUIPMENT_ERROR_INCORRECT_PASSWORD,
                                        qmi_error->message);
    } else if (g_error_matches (qmi_error,
                                QMI_PROTOCOL_ERROR,
                                QMI_PROTOCOL_ERROR_PIN_BLOCKED)) {
        me_error = g_error_new_literal (MM_MOBILE_EQUIPMENT_ERROR,
                                        MM_MOBILE_EQUIPMENT_ERROR_SIM_PUK,
                                        qmi_error->message);
    }

    if (me_error) {
        g_error_free (qmi_error);
        return me_error;
    }

    return qmi_error;
}

static gboolean
send_pin_finish (MMBaseSim     *self,
                 GAsyncResult  *res,
                 GError       **error)
{
    return g_task_propagate_boolean (G_TASK (res), error);
}

static void
uim_verify_pin_ready (QmiClientUim *client,
                      GAsyncResult *res,
                      GTask        *task)
{
    QmiMessageUimVerifyPinOutput *output = NULL;
    GError *error = NULL;

    output = qmi_client_uim_verify_pin_finish (client, res, &error);
    if (!output) {
        g_prefix_error (&error, "QMI operation failed: ");
        g_task_return_error (task, error);
    } else if (!qmi_message_uim_verify_pin_output_get_result (output, &error)) {
        g_prefix_error (&error, "Couldn't verify PIN: ");
        g_task_return_error (task, pin_qmi_error_to_mobile_equipment_error (error));
    } else
        g_task_return_boolean (task, TRUE);

    if (output)
        qmi_message_uim_verify_pin_output_unref (output);
    g_object_unref (task);
}

static void
uim_verify_pin (MMSimQmi *self,
                GTask    *task)
{
    QmiMessageUimVerifyPinInput *input;
    QmiClient *client = NULL;
    GArray *aid;

    if (!ensure_qmi_client (task,
                            self,
                            QMI_SERVICE_UIM, &client))
        return;

    input = qmi_message_uim_verify_pin_input_new ();
    qmi_message_uim_verify_pin_input_set_info (
        input,
        QMI_UIM_PIN_ID_PIN1,
        g_task_get_task_data (task),
        NULL);
    aid = g_array_new (FALSE, FALSE, sizeof (guint8)); /* empty AID */
    qmi_message_uim_verify_pin_input_set_session (
        input,
        QMI_UIM_SESSION_TYPE_CARD_SLOT_1,
        aid,
        NULL);
    g_array_unref (aid);
    qmi_client_uim_verify_pin (QMI_CLIENT_UIM (client),
                               input,
                               5,
                               NULL,
                               (GAsyncReadyCallback) uim_verify_pin_ready,
                               task);
    qmi_message_uim_verify_pin_input_unref (input);
}

static void
dms_uim_verify_pin_ready (QmiClientDms *client,
                          GAsyncResult *res,
                          GTask        *task)
{
    QmiMessageDmsUimVerifyPinOutput *output = NULL;
    GError *error = NULL;

    output = qmi_client_dms_uim_verify_pin_finish (client, res, &error);
    if (!output) {
        g_prefix_error (&error, "QMI operation failed: ");
        g_task_return_error (task, error);
    } else if (!qmi_message_dms_uim_verify_pin_output_get_result (output, &error)) {
        g_prefix_error (&error, "Couldn't verify PIN: ");
        g_task_return_error (task, pin_qmi_error_to_mobile_equipment_error (error));
    } else
        g_task_return_boolean (task, TRUE);

    if (output)
        qmi_message_dms_uim_verify_pin_output_unref (output);
    g_object_unref (task);
}

static void
dms_uim_verify_pin (MMSimQmi *self,
                    GTask    *task)
{
    QmiMessageDmsUimVerifyPinInput *input;
    QmiClient *client = NULL;

    if (!ensure_qmi_client (NULL,
                            self,
                            QMI_SERVICE_DMS, &client)) {
        /* Very unlikely that this will ever happen, but anyway, try with
         * UIM service instead */
        uim_verify_pin (self, task);
        return;
    }

    mm_obj_dbg (self, "sending PIN...");
    input = qmi_message_dms_uim_verify_pin_input_new ();
    qmi_message_dms_uim_verify_pin_input_set_info (
        input,
        QMI_DMS_UIM_PIN_ID_PIN,
        g_task_get_task_data (task),
        NULL);
    qmi_client_dms_uim_verify_pin (QMI_CLIENT_DMS (client),
                                   input,
                                   5,
                                   NULL,
                                   (GAsyncReadyCallback) dms_uim_verify_pin_ready,
                                   task);
    qmi_message_dms_uim_verify_pin_input_unref (input);
}

static void
send_pin (MMBaseSim           *_self,
          const gchar         *pin,
          GAsyncReadyCallback  callback,
          gpointer             user_data)
{
    GTask *task;
    MMSimQmi *self;

    self = MM_SIM_QMI (_self);
    task = g_task_new (self, NULL, callback, user_data);

    g_task_set_task_data (task, g_strdup (pin), g_free);

    mm_obj_dbg (self, "verifying PIN...");
    if (!self->priv->dms_uim_deprecated)
        dms_uim_verify_pin (self, task);
    else
        uim_verify_pin (self, task);
}

/*****************************************************************************/
/* Send PUK */

typedef struct {
    gchar    *puk;
    gchar    *new_pin;
} UnblockPinContext;

static void
unblock_pin_context_free (UnblockPinContext *ctx)
{
    g_free (ctx->puk);
    g_free (ctx->new_pin);
    g_slice_free (UnblockPinContext, ctx);
}

static gboolean
send_puk_finish (MMBaseSim     *self,
                 GAsyncResult  *res,
                 GError       **error)
{
    return g_task_propagate_boolean (G_TASK (res), error);
}

static void
uim_unblock_pin_ready (QmiClientUim *client,
                       GAsyncResult *res,
                       GTask        *task)
{
    QmiMessageUimUnblockPinOutput *output = NULL;
    GError *error = NULL;

    output = qmi_client_uim_unblock_pin_finish (client, res, &error);
    if (!output) {
        g_prefix_error (&error, "QMI operation failed: ");
        g_task_return_error (task, error);
    } else if (!qmi_message_uim_unblock_pin_output_get_result (output, &error)) {
        g_prefix_error (&error, "Couldn't unblock PIN: ");
        g_task_return_error (task, pin_qmi_error_to_mobile_equipment_error (error));
    } else
        g_task_return_boolean (task, TRUE);

    if (output)
        qmi_message_uim_unblock_pin_output_unref (output);
    g_object_unref (task);
}

static void
uim_unblock_pin (MMSimQmi *self,
                 GTask    *task)
{
    QmiMessageUimUnblockPinInput *input;
    QmiClient *client = NULL;
    UnblockPinContext *ctx;
    GArray *aid;

    if (!ensure_qmi_client (task,
                            self,
                            QMI_SERVICE_UIM, &client))
        return;

    ctx = g_task_get_task_data (task);

    input = qmi_message_uim_unblock_pin_input_new ();
    qmi_message_uim_unblock_pin_input_set_info (
        input,
        QMI_UIM_PIN_ID_PIN1,
        ctx->puk,
        ctx->new_pin,
        NULL);
    aid = g_array_new (FALSE, FALSE, sizeof (guint8)); /* empty AID */
    qmi_message_uim_unblock_pin_input_set_session (
        input,
        QMI_UIM_SESSION_TYPE_CARD_SLOT_1,
        aid,
        NULL);
    g_array_unref (aid);
    qmi_client_uim_unblock_pin (QMI_CLIENT_UIM (client),
                                input,
                                5,
                                NULL,
                                (GAsyncReadyCallback) uim_unblock_pin_ready,
                                task);
    qmi_message_uim_unblock_pin_input_unref (input);
}

static void
dms_uim_unblock_pin_ready (QmiClientDms *client,
                           GAsyncResult *res,
                           GTask        *task)
{
    QmiMessageDmsUimUnblockPinOutput *output = NULL;
    GError *error = NULL;

    output = qmi_client_dms_uim_unblock_pin_finish (client, res, &error);
    if (!output) {
        g_prefix_error (&error, "QMI operation failed: ");
        g_task_return_error (task, error);
    } else if (!qmi_message_dms_uim_unblock_pin_output_get_result (output, &error)) {
        g_prefix_error (&error, "Couldn't unblock PIN: ");
        g_task_return_error (task, pin_qmi_error_to_mobile_equipment_error (error));
    } else
        g_task_return_boolean (task, TRUE);

    if (output)
        qmi_message_dms_uim_unblock_pin_output_unref (output);
    g_object_unref (task);
}

static void
dms_uim_unblock_pin (MMSimQmi *self,
                     GTask    *task)
{
    QmiMessageDmsUimUnblockPinInput *input;
    QmiClient *client = NULL;
    UnblockPinContext *ctx;

    if (!ensure_qmi_client (NULL,
                            self,
                            QMI_SERVICE_DMS, &client)) {
        /* Very unlikely that this will ever happen, but anyway, try with
         * UIM service instead */
        uim_unblock_pin (self, task);
        return;
    }

    ctx = g_task_get_task_data (task);

    input = qmi_message_dms_uim_unblock_pin_input_new ();
    qmi_message_dms_uim_unblock_pin_input_set_info (
        input,
        QMI_DMS_UIM_PIN_ID_PIN,
        ctx->puk,
        ctx->new_pin,
        NULL);
    qmi_client_dms_uim_unblock_pin (QMI_CLIENT_DMS (client),
                                    input,
                                    5,
                                    NULL,
                                    (GAsyncReadyCallback)dms_uim_unblock_pin_ready,
                                    task);
    qmi_message_dms_uim_unblock_pin_input_unref (input);
}

static void
send_puk (MMBaseSim           *_self,
          const gchar         *puk,
          const gchar         *new_pin,
          GAsyncReadyCallback  callback,
          gpointer             user_data)
{
    GTask *task;
    UnblockPinContext *ctx;
    MMSimQmi *self;

    self = MM_SIM_QMI (_self);
    task = g_task_new (self, NULL, callback, user_data);

    ctx = g_slice_new (UnblockPinContext);
    ctx->puk = g_strdup (puk);
    ctx->new_pin = g_strdup (new_pin);
    g_task_set_task_data (task, ctx, (GDestroyNotify) unblock_pin_context_free);

    mm_obj_dbg (self, "unblocking PIN...");
    if (!self->priv->dms_uim_deprecated)
        dms_uim_unblock_pin (self, task);
    else
        uim_unblock_pin (self, task);
}

/*****************************************************************************/
/* Change PIN */

typedef struct {
    gchar    *old_pin;
    gchar    *new_pin;
} ChangePinContext;

static void
change_pin_context_free (ChangePinContext *ctx)
{
    g_free (ctx->old_pin);
    g_free (ctx->new_pin);
    g_slice_free (ChangePinContext, ctx);
}

static gboolean
change_pin_finish (MMBaseSim     *self,
                   GAsyncResult  *res,
                   GError       **error)
{
    return g_task_propagate_boolean (G_TASK (res), error);
}

static void
uim_change_pin_ready (QmiClientUim *client,
                      GAsyncResult *res,
                      GTask        *task)
{
    QmiMessageUimChangePinOutput *output = NULL;
    GError *error = NULL;

    output = qmi_client_uim_change_pin_finish (client, res, &error);
    if (!output) {
        g_prefix_error (&error, "QMI operation failed: ");
        g_task_return_error (task, error);
    } else if (!qmi_message_uim_change_pin_output_get_result (output, &error)) {
        g_prefix_error (&error, "Couldn't change PIN: ");
        g_task_return_error (task, pin_qmi_error_to_mobile_equipment_error (error));
    } else
        g_task_return_boolean (task, TRUE);

    if (output)
        qmi_message_uim_change_pin_output_unref (output);
    g_object_unref (task);
}

static void
uim_change_pin (MMSimQmi *self,
                GTask    *task)
{
    QmiMessageUimChangePinInput *input;
    QmiClient *client = NULL;
    ChangePinContext *ctx;
    GArray *aid;

    if (!ensure_qmi_client (task,
                            self,
                            QMI_SERVICE_UIM, &client))
        return;

    ctx = g_task_get_task_data (task);

    input = qmi_message_uim_change_pin_input_new ();
    qmi_message_uim_change_pin_input_set_info (
        input,
        QMI_UIM_PIN_ID_PIN1,
        ctx->old_pin,
        ctx->new_pin,
        NULL);
    aid = g_array_new (FALSE, FALSE, sizeof (guint8)); /* empty AID */
    qmi_message_uim_change_pin_input_set_session (
        input,
        QMI_UIM_SESSION_TYPE_CARD_SLOT_1,
        aid,
        NULL);
    g_array_unref (aid);
    qmi_client_uim_change_pin (QMI_CLIENT_UIM (client),
                               input,
                               5,
                               NULL,
                               (GAsyncReadyCallback) uim_change_pin_ready,
                               task);
    qmi_message_uim_change_pin_input_unref (input);
}

static void
dms_uim_change_pin_ready (QmiClientDms *client,
                          GAsyncResult *res,
                          GTask        *task)
{
    QmiMessageDmsUimChangePinOutput *output = NULL;
    GError *error = NULL;

    output = qmi_client_dms_uim_change_pin_finish (client, res, &error);
    if (!output) {
        g_prefix_error (&error, "QMI operation failed: ");
        g_task_return_error (task, error);
    } else if (!qmi_message_dms_uim_change_pin_output_get_result (output, &error)) {
        g_prefix_error (&error, "Couldn't change PIN: ");
        g_task_return_error (task, pin_qmi_error_to_mobile_equipment_error (error));
    } else
        g_task_return_boolean (task, TRUE);

    if (output)
        qmi_message_dms_uim_change_pin_output_unref (output);
    g_object_unref (task);
}

static void
dms_uim_change_pin (MMSimQmi *self,
                    GTask    *task)
{
    QmiMessageDmsUimChangePinInput *input;
    QmiClient *client = NULL;
    ChangePinContext *ctx;

    if (!ensure_qmi_client (NULL,
                            self,
                            QMI_SERVICE_DMS, &client)) {
        /* Very unlikely that this will ever happen, but anyway, try with
         * UIM service instead */
        uim_change_pin (self, task);
        return;
    }

    ctx = g_task_get_task_data (task);

    input = qmi_message_dms_uim_change_pin_input_new ();
    qmi_message_dms_uim_change_pin_input_set_info (
        input,
        QMI_DMS_UIM_PIN_ID_PIN,
        ctx->old_pin,
        ctx->new_pin,
        NULL);
    qmi_client_dms_uim_change_pin (QMI_CLIENT_DMS (client),
                                   input,
                                   5,
                                   NULL,
                                   (GAsyncReadyCallback) dms_uim_change_pin_ready,
                                   task);
    qmi_message_dms_uim_change_pin_input_unref (input);
}

static void
change_pin (MMBaseSim           *_self,
            const gchar         *old_pin,
            const gchar         *new_pin,
            GAsyncReadyCallback  callback,
            gpointer             user_data)
{
    GTask *task;
    ChangePinContext *ctx;
    MMSimQmi *self;

    self = MM_SIM_QMI (_self);
    task = g_task_new (self, NULL, callback, user_data);

    ctx = g_slice_new (ChangePinContext);
    ctx->old_pin = g_strdup (old_pin);
    ctx->new_pin = g_strdup (new_pin);
    g_task_set_task_data (task, ctx, (GDestroyNotify) change_pin_context_free);

    mm_obj_dbg (self, "changing PIN...");
    if (!self->priv->dms_uim_deprecated)
        dms_uim_change_pin (self, task);
    else
        uim_change_pin (self, task);
}

/*****************************************************************************/
/* Enable PIN */

typedef struct {
    gchar    *pin;
    gboolean  enabled;
} EnablePinContext;

static void
enable_pin_context_free (EnablePinContext *ctx)
{
    g_free (ctx->pin);
    g_slice_free (EnablePinContext, ctx);
}

static gboolean
enable_pin_finish (MMBaseSim *self,
                   GAsyncResult *res,
                   GError **error)
{
    return g_task_propagate_boolean (G_TASK (res), error);
}

static void
uim_set_pin_protection_ready (QmiClientUim *client,
                              GAsyncResult *res,
                              GTask        *task)
{
    QmiMessageUimSetPinProtectionOutput *output = NULL;
    GError *error = NULL;

    output = qmi_client_uim_set_pin_protection_finish (client, res, &error);
    if (!output) {
        g_prefix_error (&error, "QMI operation failed: ");
        g_task_return_error (task, error);
    } else if (!qmi_message_uim_set_pin_protection_output_get_result (output, &error)) {
        g_prefix_error (&error, "Couldn't enable PIN: ");
        g_task_return_error (task, pin_qmi_error_to_mobile_equipment_error (error));
    } else
        g_task_return_boolean (task, TRUE);

    if (output)
        qmi_message_uim_set_pin_protection_output_unref (output);
    g_object_unref (task);
}

static void
uim_enable_pin (MMSimQmi *self,
                GTask    *task)
{
    QmiMessageUimSetPinProtectionInput *input;
    QmiClient *client = NULL;
    EnablePinContext *ctx;
    GArray *aid;

    if (!ensure_qmi_client (task,
                            MM_SIM_QMI (self),
                            QMI_SERVICE_UIM, &client))
        return;

    ctx = g_task_get_task_data (task);

    input = qmi_message_uim_set_pin_protection_input_new ();
    qmi_message_uim_set_pin_protection_input_set_info (
        input,
        QMI_UIM_PIN_ID_PIN1,
        ctx->enabled,
        ctx->pin,
        NULL);
    aid = g_array_new (FALSE, FALSE, sizeof (guint8)); /* empty AID */
    qmi_message_uim_set_pin_protection_input_set_session (
        input,
        QMI_UIM_SESSION_TYPE_CARD_SLOT_1,
        aid,
        NULL);
    g_array_unref (aid);
    qmi_client_uim_set_pin_protection (QMI_CLIENT_UIM (client),
                                       input,
                                       5,
                                       NULL,
                                       (GAsyncReadyCallback)uim_set_pin_protection_ready,
                                       task);
    qmi_message_uim_set_pin_protection_input_unref (input);
}

static void
dms_uim_set_pin_protection_ready (QmiClientDms *client,
                                  GAsyncResult *res,
                                  GTask        *task)
{
    QmiMessageDmsUimSetPinProtectionOutput *output = NULL;
    GError *error = NULL;

    output = qmi_client_dms_uim_set_pin_protection_finish (client, res, &error);
    if (!output) {
        g_prefix_error (&error, "QMI operation failed: ");
        g_task_return_error (task, error);
    } else if (!qmi_message_dms_uim_set_pin_protection_output_get_result (output, &error)) {
        g_prefix_error (&error, "Couldn't enable PIN: ");
        g_task_return_error (task, pin_qmi_error_to_mobile_equipment_error (error));
    } else
        g_task_return_boolean (task, TRUE);

    if (output)
        qmi_message_dms_uim_set_pin_protection_output_unref (output);
    g_object_unref (task);
}

static void
dms_uim_enable_pin (MMSimQmi *self,
                    GTask    *task)
{
    QmiMessageDmsUimSetPinProtectionInput *input;
    QmiClient *client = NULL;
    EnablePinContext *ctx;

    if (!ensure_qmi_client (NULL,
                            MM_SIM_QMI (self),
                            QMI_SERVICE_DMS, &client)) {
        /* Very unlikely that this will ever happen, but anyway, try with
         * UIM service instead */
        uim_enable_pin (self, task);
        return;
    }

    ctx = g_task_get_task_data (task);

    input = qmi_message_dms_uim_set_pin_protection_input_new ();
    qmi_message_dms_uim_set_pin_protection_input_set_info (
        input,
        QMI_DMS_UIM_PIN_ID_PIN,
        ctx->enabled,
        ctx->pin,
        NULL);
    qmi_client_dms_uim_set_pin_protection (QMI_CLIENT_DMS (client),
                                           input,
                                           5,
                                           NULL,
                                           (GAsyncReadyCallback)dms_uim_set_pin_protection_ready,
                                           task);
    qmi_message_dms_uim_set_pin_protection_input_unref (input);
}

static void
enable_pin (MMBaseSim           *_self,
            const gchar         *pin,
            gboolean             enabled,
            GAsyncReadyCallback  callback,
            gpointer             user_data)
{
    GTask *task;
    EnablePinContext *ctx;
    MMSimQmi *self;

    self = MM_SIM_QMI (_self);
    task = g_task_new (self, NULL, callback, user_data);

    ctx = g_slice_new (EnablePinContext);
    ctx->pin = g_strdup (pin);
    ctx->enabled = enabled;
    g_task_set_task_data (task, ctx, (GDestroyNotify) enable_pin_context_free);

    mm_obj_dbg (self, "%s PIN...", enabled ? "enabling" : "disabling");
    if (!self->priv->dms_uim_deprecated)
        dms_uim_enable_pin (self, task);
    else
        uim_enable_pin (self, task);
}

/*****************************************************************************/

MMBaseSim *
mm_sim_qmi_new_finish (GAsyncResult  *res,
                       GError       **error)
{
    GObject *source;
    GObject *sim;

    source = g_async_result_get_source_object (res);
    sim = g_async_initable_new_finish (G_ASYNC_INITABLE (source), res, error);
    g_object_unref (source);

    if (!sim)
        return NULL;

    /* Only export valid SIMs */
    mm_base_sim_export (MM_BASE_SIM (sim));

    return MM_BASE_SIM (sim);
}

void
mm_sim_qmi_new (MMBaseModem         *modem,
                gboolean             dms_uim_deprecated,
                GCancellable        *cancellable,
                GAsyncReadyCallback  callback,
                gpointer             user_data)
{
    g_async_initable_new_async (MM_TYPE_SIM_QMI,
                                G_PRIORITY_DEFAULT,
                                cancellable,
                                callback,
                                user_data,
                                MM_BASE_SIM_MODEM, modem,
                                MM_SIM_QMI_DMS_UIM_DEPRECATED, dms_uim_deprecated,
                                "active", TRUE, /* by default always active */
                                NULL);
}

MMBaseSim *
mm_sim_qmi_new_initialized (MMBaseModem *modem,
                            gboolean     dms_uim_deprecated,
                            guint        slot_number,
                            gboolean     active,
                            const gchar *sim_identifier,
                            const gchar *imsi,
                            const gchar *eid,
                            const gchar *operator_identifier,
                            const gchar *operator_name,
                            const GStrv  emergency_numbers)
{
    MMBaseSim *sim;

    sim = MM_BASE_SIM (g_object_new (MM_TYPE_SIM_QMI,
                                     MM_BASE_SIM_MODEM,             modem,
                                     MM_SIM_QMI_DMS_UIM_DEPRECATED, dms_uim_deprecated,
                                     MM_BASE_SIM_SLOT_NUMBER,       slot_number,
                                     "active",                      active,
                                     "sim-identifier",              sim_identifier,
                                     "imsi",                        imsi,
                                     "eid",                         eid,
                                     "operator-identifier",         operator_identifier,
                                     "operator-name",               operator_name,
                                     "emergency-numbers",           emergency_numbers,
                                     NULL));

    mm_base_sim_export (sim);
    return sim;
}

/*****************************************************************************/

static void
mm_sim_qmi_init (MMSimQmi *self)
{
    /* Initialize private data */
    self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
                                              MM_TYPE_SIM_QMI,
                                              MMSimQmiPrivate);
}

static void
set_property (GObject      *object,
              guint         prop_id,
              const GValue *value,
              GParamSpec   *pspec)
{
    MMSimQmi *self = MM_SIM_QMI (object);

    switch (prop_id) {
    case PROP_DMS_UIM_DEPRECATED:
        self->priv->dms_uim_deprecated = g_value_get_boolean (value);
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
        break;
    }
}

static void
get_property (GObject    *object,
              guint       prop_id,
              GValue     *value,
              GParamSpec *pspec)
{
    MMSimQmi *self = MM_SIM_QMI (object);

    switch (prop_id) {
    case PROP_DMS_UIM_DEPRECATED:
        g_value_set_boolean (value, self->priv->dms_uim_deprecated);
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
        break;
    }
}

static void
mm_sim_qmi_class_init (MMSimQmiClass *klass)
{
    GObjectClass *object_class = G_OBJECT_CLASS (klass);
    MMBaseSimClass *base_sim_class = MM_BASE_SIM_CLASS (klass);

    g_type_class_add_private (object_class, sizeof (MMSimQmiPrivate));

    object_class->get_property = get_property;
    object_class->set_property = set_property;

    base_sim_class->wait_sim_ready = wait_sim_ready;
    base_sim_class->wait_sim_ready_finish = wait_sim_ready_finish;
    base_sim_class->load_sim_identifier = load_sim_identifier;
    base_sim_class->load_sim_identifier_finish = load_sim_identifier_finish;
    base_sim_class->load_imsi = load_imsi;
    base_sim_class->load_imsi_finish = load_imsi_finish;
    base_sim_class->load_operator_identifier = load_operator_identifier;
    base_sim_class->load_operator_identifier_finish = load_operator_identifier_finish;
    base_sim_class->load_operator_name = load_operator_name;
    base_sim_class->load_operator_name_finish = load_operator_name_finish;
    base_sim_class->load_gid1 = load_gid1;
    base_sim_class->load_gid1_finish = load_gid1_finish;
    base_sim_class->load_gid2 = load_gid2;
    base_sim_class->load_gid2_finish = load_gid2_finish;
    base_sim_class->load_preferred_networks = load_preferred_networks;
    base_sim_class->load_preferred_networks_finish = load_preferred_networks_finish;
    base_sim_class->set_preferred_networks = set_preferred_networks;
    base_sim_class->set_preferred_networks_finish = set_preferred_networks_finish;
    base_sim_class->send_pin = send_pin;
    base_sim_class->send_pin_finish = send_pin_finish;
    base_sim_class->send_puk = send_puk;
    base_sim_class->send_puk_finish = send_puk_finish;
    base_sim_class->change_pin = change_pin;
    base_sim_class->change_pin_finish = change_pin_finish;
    base_sim_class->enable_pin = enable_pin;
    base_sim_class->enable_pin_finish = enable_pin_finish;

    properties[PROP_DMS_UIM_DEPRECATED] =
        g_param_spec_boolean (MM_SIM_QMI_DMS_UIM_DEPRECATED,
                              "DMS UIM deprecated",
                              "Whether DMS UIM commands should be skipped",
                              FALSE,
                              G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
    g_object_class_install_property (object_class, PROP_DMS_UIM_DEPRECATED, properties[PROP_DMS_UIM_DEPRECATED]);
}
